From 56b1def06e82db5112d36afdef14f5fce5c0f96e Mon Sep 17 00:00:00 2001 From: Captain <1304725924@qq.com> Date: Wed, 10 Jun 2026 14:48:50 +0800 Subject: [PATCH] Add Claude Fable 5 to the model catalog (#1443) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Claude Fable 5 to the model catalog Fable 5 (`claude-fable-5`) is the new top-tier Claude model above Opus, but the Claude provider's model list is hardcoded in `CLAUDE_MODELS` and was not fetched dynamically, so it never appeared in the selector. - Add `claude-fable-5` to `CLAUDE_MODELS` (non-default; Opus 4.8 stays the default). Uses the Opus extended-thinking effort levels (low/medium/high/ xhigh/max). No `[1m]` variant: Fable 5 is natively 1M context, unlike the Opus/Sonnet entries whose `[1m]` opts a 200K-default model into 1M. - Teach `normalizeClaudeRuntimeModelId` the Fable family. Its version is a single segment (`fable-5`) rather than the `{major}-{minor}` scheme, so it needs its own match to map dated runtime IDs back to the catalog ID. - Update the model-list and normalization test expectations, plus the CLI provider e2e catalog fixture. Fast Mode is correctly unaffected โ€” its allowlist is Opus 4.6/4.7/4.8 only. Co-Authored-By: Claude Opus 4.8 (1M context) * Simplify Fable runtime-ID normalization Drop the unused `[1m]` capture from the Fable match branch. There is no `claude-fable-5[1m]` catalog entry โ€” Fable 5 is natively 1M context, so there is no 200K-default model to opt into 1M โ€” which made that path dead code (and an untested one). Keep the `[-_ ]+` separator class for consistency with the sibling opus/sonnet/haiku regex right below it. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: lumingjun Co-authored-by: Claude Opus 4.8 (1M context) --- packages/cli/tests/15-provider.test.ts | 5 +++++ .../server/agent/providers/claude/agent.test.ts | 1 + .../server/agent/providers/claude/models.test.ts | 3 +++ .../src/server/agent/providers/claude/models.ts | 16 ++++++++++++++++ 4 files changed, 25 insertions(+) diff --git a/packages/cli/tests/15-provider.test.ts b/packages/cli/tests/15-provider.test.ts index 0d9ea4672..aaf0bf0f1 100644 --- a/packages/cli/tests/15-provider.test.ts +++ b/packages/cli/tests/15-provider.test.ts @@ -45,6 +45,11 @@ interface ProviderListRow { } const EXPECTED_CLAUDE_MODELS = [ + { + id: "claude-fable-5", + model: "Fable 5", + descriptionFragment: "Most powerful", + }, { id: "claude-opus-4-8[1m]", model: "Opus 4.8 1M", diff --git a/packages/server/src/server/agent/providers/claude/agent.test.ts b/packages/server/src/server/agent/providers/claude/agent.test.ts index 6264a0ad2..6368e0398 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -408,6 +408,7 @@ describe("ClaudeAgentClient.listModels", () => { const models = await client.listModels({ cwd: "/tmp/claude-models", force: false }); expect(models.map((m) => m.id)).toEqual([ + "claude-fable-5", "claude-opus-4-8[1m]", "claude-opus-4-8", "claude-opus-4-7[1m]", diff --git a/packages/server/src/server/agent/providers/claude/models.test.ts b/packages/server/src/server/agent/providers/claude/models.test.ts index f4b335688..864e414f6 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -35,6 +35,7 @@ describe("getClaudeModels", () => { it("returns all claude models", () => { const models = getClaudeModels(); expect(models.map((m) => m.id)).toEqual([ + "claude-fable-5", "claude-opus-4-8[1m]", "claude-opus-4-8", "claude-opus-4-7[1m]", @@ -179,6 +180,7 @@ describe("ClaudeAgentClient.listModels", () => { describe("normalizeClaudeRuntimeModelId", () => { it("returns exact match for known model IDs", () => { + expect(normalizeClaudeRuntimeModelId("claude-fable-5")).toBe("claude-fable-5"); expect(normalizeClaudeRuntimeModelId("claude-opus-4-6")).toBe("claude-opus-4-6"); expect(normalizeClaudeRuntimeModelId("claude-opus-4-6[1m]")).toBe("claude-opus-4-6[1m]"); expect(normalizeClaudeRuntimeModelId("claude-sonnet-4-6")).toBe("claude-sonnet-4-6"); @@ -186,6 +188,7 @@ describe("normalizeClaudeRuntimeModelId", () => { }); it("normalizes dated model IDs to base model", () => { + expect(normalizeClaudeRuntimeModelId("claude-fable-5-20260301")).toBe("claude-fable-5"); expect(normalizeClaudeRuntimeModelId("claude-opus-4-6-20260101")).toBe("claude-opus-4-6"); expect(normalizeClaudeRuntimeModelId("claude-sonnet-4-6-20260101")).toBe("claude-sonnet-4-6"); expect(normalizeClaudeRuntimeModelId("claude-haiku-4-5-20251001")).toBe("claude-haiku-4-5"); diff --git a/packages/server/src/server/agent/providers/claude/models.ts b/packages/server/src/server/agent/providers/claude/models.ts index 31c2cc77f..d14a43e1d 100644 --- a/packages/server/src/server/agent/providers/claude/models.ts +++ b/packages/server/src/server/agent/providers/claude/models.ts @@ -21,6 +21,13 @@ const CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS = [ ] as const; const CLAUDE_MODELS: AgentModelDefinition[] = [ + { + provider: "claude", + id: "claude-fable-5", + label: "Fable 5", + description: "Fable 5 ยท Most powerful model", + thinkingOptions: [...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS], + }, { provider: "claude", id: "claude-opus-4-8[1m]", @@ -206,6 +213,15 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined): return trimmed; } + // Fable uses a single-segment version (claude-fable-5), not the {major}-{minor} + // scheme of opus/sonnet/haiku, so match it separately. This maps dated runtime + // strings (e.g. claude-fable-5-20260301) back to the catalog ID. No [1m] variant: + // Fable 5 is natively 1M, so there is no 200K-default model to opt into 1M. + const fableMatch = trimmed.match(/(?:claude-)?fable[-_ ]+(\d+)/i); + if (fableMatch) { + return `claude-fable-${fableMatch[1]}`; + } + // Match: claude-{family}-{major}-{minor}[1m]? possibly followed by a date suffix const runtimeMatch = trimmed.match( /(?:claude-)?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i,