feat: add Claude Fable 5 model mapping across Claude Code and Desktop

- Wire claude-fable-5 as a fourth tier on both proxy paths, with a
  fable -> opus -> default fallback mirroring the official downgrade.
- Whitelist the fable- prefix for the Desktop 1.12603.1+ validator.
- Clarify fallbackModelHint (zh/en/ja/zh-TW): a blank tier on
  third-party endpoints forwards the literal model name and 404s.

Refs #3980, #4026, #4049.
This commit is contained in:
Jason
2026-06-13 19:11:59 +08:00
parent 6e519a7496
commit 4f8a79c273
13 changed files with 343 additions and 41 deletions
@@ -97,7 +97,7 @@ describe("ClaudeDesktopProviderForm", () => {
expect(document.activeElement).toBe(currentInput);
});
it("代理模式始终渲染 Sonnet / Opus / Haiku 档(即使只配了一档)", () => {
it("代理模式始终渲染 Sonnet / Opus / Fable / Haiku 档(即使只配了一档)", () => {
renderForm({
name: "Proxy Provider",
settingsConfig: {
@@ -114,13 +114,13 @@ describe("ClaudeDesktopProviderForm", () => {
},
});
// 固定档:每档各一个「菜单显示名」输入框,无论初始只配了几档。
expect(screen.getAllByPlaceholderText("DeepSeek V4 Pro")).toHaveLength(3);
// 固定档:每档各一个「菜单显示名」输入框,无论初始只配了几档。
expect(screen.getAllByPlaceholderText("DeepSeek V4 Pro")).toHaveLength(4);
});
it("代理模式初始无路由且默认路由未就绪时不渲染空档", () => {
it("代理模式初始无路由且默认路由未就绪时不渲染空档", () => {
// mock 的 getClaudeDesktopDefaultRoutes 返回 [],模拟默认路由尚未就绪。
// 修复前:normalizeProxyRows([]) 会渲染 3 条空行并把 routes.length 撑到 3
// 修复前:normalizeProxyRows([]) 会渲染空行并把 routes.length 撑起来
// 永久挡住 seed effect 的默认路由回填。修复后应保持空、等待 seed。
renderForm({
name: "Proxy Provider",
@@ -139,7 +139,7 @@ describe("ClaudeDesktopProviderForm", () => {
expect(screen.queryAllByPlaceholderText("DeepSeek V4 Pro")).toHaveLength(0);
});
it("保存模型映射时补齐固定档并把留空档回填为 Sonnet 模型", async () => {
it("保存模型映射时补齐固定档并把留空档回填为 Sonnet 模型", async () => {
const onSubmit = vi.fn();
renderForm(
{
@@ -166,19 +166,25 @@ describe("ClaudeDesktopProviderForm", () => {
await waitFor(() => expect(onSubmit).toHaveBeenCalled());
const submitted = onSubmit.mock.calls[0][0];
// claude-old 迁移到 Sonnet;留空的 Opus / Haiku 回填为 Sonnet 的上游模型,
// 保证落库档齐全,子 agent 调用的 Haiku 始终可解析。
// claude-old 迁移到 Sonnet;留空的 Opus / Fable / Haiku 回填为 Sonnet 的
// 上游模型,保证落库档齐全,子 agent 调用的各档始终可解析。
expect(submitted.meta.claudeDesktopModelRoutes).toMatchObject({
"claude-sonnet-4-6": {
model: "upstream-old",
labelOverride: "upstream-old",
},
"claude-opus-4-8": { model: "upstream-old" },
"claude-fable-5": { model: "upstream-old" },
"claude-haiku-4-5": { model: "upstream-old" },
});
expect(
Object.keys(submitted.meta.claudeDesktopModelRoutes).sort(),
).toEqual(["claude-haiku-4-5", "claude-opus-4-8", "claude-sonnet-4-6"]);
).toEqual([
"claude-fable-5",
"claude-haiku-4-5",
"claude-opus-4-8",
"claude-sonnet-4-6",
]);
});
it("回填空档时继承 Sonnet 的 1M 声明", async () => {
@@ -83,6 +83,8 @@ const renderCopilotForm = (overrides: Partial<ClaudeFormFieldsProps> = {}) => {
defaultSonnetModelName: "Claude Sonnet",
defaultOpusModel: "",
defaultOpusModelName: "",
defaultFableModel: "",
defaultFableModelName: "",
onModelChange: vi.fn(),
speedTestEndpoints: [],
apiFormat: "anthropic",