Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions RELEASE_NOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
- **Vertex model listing.** Vertex model-list/model-get now route OpenAI clients through Gemini response conversion and send empty GET bodies to Google, fixing local 500s when pulling publisher models.
- **Structured-output conversion cleanup.** OpenAI-to-Claude transforms drop deprecated `output_format`, avoid unsupported permissive JSON-object shims, and keep schema serialization strict.
- **TOML export for rewrite rules.** Model alias/suffix rewrite rules no longer export empty filter dimensions as JSON null, avoiding `unsupported unit type` during config export (#94).
- **Console rewrite-rule deletion persists.** Deleting parameter rewrite rules from the console now saves the fresh `rewrite_rules` JSON immediately, so removed rules do not reappear after reload (#96).
- **Console cache-breakpoint TTL display.** The cache breakpoint editor now reads API-returned `ttl5m` / `ttl1h` values as `5m` / `1h` instead of rendering them as `auto` (#97).
- **Responses/image stream schema tolerance.** Responses keepalive events and partial image-generation output items are accepted instead of turning valid upstream streams into local 500s.

Expand All @@ -48,6 +49,7 @@
- **Vertex 模型列表.** Vertex 的 model-list/model-get 现在会把 OpenAI 客户端路由到 Gemini 响应转换,并向 Google 发送空 GET body,修复拉取 publisher models 时本地 500 的问题。
- **结构化输出转换清理.** OpenAI → Claude 转换删除废弃的 `output_format`,避免生成上游不支持的宽松 JSON-object shim,并保持 schema 序列化严格。
- **rewrite rules TOML 导出.** 模型别名 / 后缀变体自动生成的 rewrite rules 不再把空 filter 维度导出成 JSON null,避免配置导出时报 `unsupported unit type`(#94)。
- **控制台删除 rewrite rule 会持久化.** 在控制台删除参数改写规则时,现在会立刻保存最新的 `rewrite_rules` JSON,删除后的规则不会刷新后又出现(#96)。
- **控制台缓存断点 TTL 显示修复.** cache breakpoint 编辑器现在会把 API 返回的 `ttl5m` / `ttl1h` 识别为 `5m` / `1h`,不再显示成 `auto`(#97)。
- **Responses / image stream schema 兼容.** Responses keepalive 事件和 image-generation 的 partial output item 现在会被接受,不再把有效上游流误转成本地 500。

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// @vitest-environment jsdom

import { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, expect, it, vi } from "vitest";

import { I18nProvider } from "../../../app/i18n";
import { RewriteRulesTab } from "./RewriteRulesTab";
import type { ProviderFormState } from "./index";

// @ts-expect-error React's test-only act flag is intentionally set on globalThis.
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

function getButtonByText(container: HTMLElement, text: string): HTMLButtonElement {
const button = Array.from(container.querySelectorAll("button")).find(
(item): item is HTMLButtonElement => item.textContent?.trim() === text,
);
if (!button) {
throw new Error(`button not found: ${text}`);
}
return button;
}

function getButtonContaining(container: HTMLElement, text: string): HTMLButtonElement {
const button = Array.from(container.querySelectorAll("button")).find((item): item is HTMLButtonElement =>
item.textContent?.includes(text) ?? false,
);
if (!button) {
throw new Error(`button containing text not found: ${text}`);
}
return button;
}

const initialRules = [
{ path: "temperature", action: { type: "set" as const, value: 0.2 } },
{ path: "metadata.trace", action: { type: "remove" as const } },
];

const baseForm: ProviderFormState = {
id: "1",
name: "anthropic",
label: "Anthropic",
channel: "anthropic",
settings: { rewrite_rules: JSON.stringify(initialRules) },
routingRules: [],
};

describe("RewriteRulesTab", () => {
it("persists the fresh rewrite_rules JSON after deleting an existing rule", async () => {
const container = document.createElement("div");
const root = createRoot(container);
const onSave = vi.fn();

try {
await act(async () => {
root.render(
<I18nProvider>
<RewriteRulesTab
form={baseForm}
onChange={() => {}}
onSave={onSave}
notify={() => {}}
/>
</I18nProvider>,
);
});

await act(async () => {
getButtonContaining(container, "temperature").click();
});

await act(async () => {
getButtonByText(container, "Delete").click();
});

expect(onSave).toHaveBeenCalledWith(JSON.stringify([initialRules[1]]));
} finally {
await act(async () => {
root.unmount();
});
}
});
});
14 changes: 9 additions & 5 deletions frontend/console/src/modules/admin/providers/RewriteRulesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ export function RewriteRulesTab({
);

const commit = (next: RewriteRule[]) => {
const nextJson = JSON.stringify(next);
onChange({
settings: { ...form.settings, rewrite_rules: JSON.stringify(next) },
settings: { ...form.settings, rewrite_rules: nextJson },
});
return nextJson;
};

const batch = useBatchSelection<RewriteRule, string>({
Expand All @@ -57,9 +59,10 @@ export function RewriteRulesTab({
onBatchDelete: async (keys) => {
const keySet = new Set(keys);
const next = rules.filter((_, idx) => !keySet.has(String(idx)));
commit(next);
const nextJson = commit(next);
setSelectedIdx(null);
setDraft(null);
onSave(nextJson);
},
onSuccess: (count) => {
notify("success", t("batch.deleted", { count }));
Expand All @@ -76,9 +79,11 @@ export function RewriteRulesTab({
};

const remove = (idx: number) => {
commit(rules.filter((_, i) => i !== idx));
const next = rules.filter((_, i) => i !== idx);
const nextJson = commit(next);
if (selectedIdx === idx) setSelectedIdx(null);
else if (selectedIdx != null && selectedIdx > idx) setSelectedIdx(selectedIdx - 1);
onSave(nextJson);
};

/// Current rule being edited (either draft or an existing one).
Expand All @@ -104,8 +109,7 @@ export function RewriteRulesTab({
const save = () => {
if (isDraft && draft) {
const next = [...rules, draft];
const nextJson = JSON.stringify(next);
commit(next);
const nextJson = commit(next);
// After this render cycle, `rules` will include the new entry and we
// want the list to highlight it. Use a timeout so the commit propagates
// through the parent and our `rules` memo re-runs with the new array.
Expand Down
Loading