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,