From 3377ffed29e1ddc4e9571b894d6aec69fecfe77b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 1 Sep 2026 20:06:08 +0200 Subject: [PATCH] feat(claude): add Fable 5.1 model Expose the new 1M-context model through the curated Claude catalog and normalize its dated and context-suffixed runtime IDs back to one canonical entry. --- .../agent/providers/claude/agent.test.ts | 2 ++ .../agent/providers/claude/model-manifest.ts | 25 +++++++++++++---- .../agent/providers/claude/models.test.ts | 27 ++++++++++++++++++- 3 files changed, 48 insertions(+), 6 deletions(-) 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 5e860732bc..56a4b2473f 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -424,6 +424,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => { }); expect(models.map((m) => m.id)).toEqual([ + "claude-fable-5-1", "claude-opus-5", "claude-fable-5", "claude-fable-5[1m]", @@ -495,6 +496,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => { }; expect(getThinkingIds("claude-opus-5")).toContain("ultracode"); + expect(getThinkingIds("claude-fable-5-1")).toContain("ultracode"); expect(getThinkingIds("claude-fable-5")).toContain("ultracode"); expect(getThinkingIds("claude-opus-4-8[1m]")).toContain("ultracode"); expect(getThinkingIds("claude-opus-4-8")).toContain("ultracode"); diff --git a/packages/server/src/server/agent/providers/claude/model-manifest.ts b/packages/server/src/server/agent/providers/claude/model-manifest.ts index c51fb70a6d..1aa7a16a1f 100644 --- a/packages/server/src/server/agent/providers/claude/model-manifest.ts +++ b/packages/server/src/server/agent/providers/claude/model-manifest.ts @@ -34,6 +34,13 @@ export const CLAUDE_DISABLED_THINKING_OPTION_ID = "off"; export const CLAUDE_ULTRACODE_THINKING_OPTION_ID = "ultracode"; export const CLAUDE_MODEL_MANIFEST = [ + { + id: "claude-fable-5-1", + label: "Fable 5.1", + description: "Fable 5.1 · Most powerful model", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + }, { id: "claude-opus-5", label: "Opus 5", @@ -50,7 +57,7 @@ export const CLAUDE_MODEL_MANIFEST = [ // COMPAT(claudeFable5OneMillionId): added in v0.3.0, remove after 2027-02-06 once pre-v0.3.0 app preferences are outside support. aliases: ["claude-fable-5[1m]"], label: "Fable 5", - description: "Fable 5 · Most powerful model", + description: "Fable 5 · Previous release", minimumClaudeCodeVersion: "2.1.169", contextWindowMaxTokens: 1_000_000, effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, @@ -326,7 +333,7 @@ export function normalizeClaudeManifestModelId(value: string | null | undefined) } const runtimeMatch = trimmed.match( - /^(?:claude[-_ ])?(opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i, + /^(?:claude[-_ ])?(fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(?:\[1m\])?(?:[-_ ]+\d{8})?(?:\[1m\])?$/i, ); if (!runtimeMatch) { return null; @@ -371,7 +378,7 @@ export function normalizeClaudeRuntimeModelId(value: string | null | undefined): } const runtimeMatch = trimmed.match( - /claude[-_ ](opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i, + /claude[-_ ](fable|opus|sonnet|haiku)[-_ ]+(\d+)[-.](\d+)(\[1m\])?/i, ); if (!runtimeMatch) { return null; @@ -417,6 +424,14 @@ function normalizeMajorMinorClaudeModelId( ): string | null { const family = familyValue.toLowerCase(); const suffix = hasOneMillionContext ? "[1m]" : ""; - const candidate = `claude-${family}-${major}-${minor}${suffix}`; - return isClaudeManifestModelId(candidate) ? candidate : null; + const candidates = [ + `claude-${family}-${major}-${minor}${suffix}`, + `claude-${family}-${major}-${minor}`, + ]; + for (const candidate of candidates) { + if (isClaudeManifestModelId(candidate)) { + return candidate; + } + } + return null; } 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 c379ab998f..c09af2cd7a 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -50,6 +50,7 @@ describe("getClaudeModels", () => { it("returns all claude models", () => { const models = getClaudeModels(); expect(models.map((m) => m.id)).toEqual([ + "claude-fable-5-1", "claude-opus-5", "claude-fable-5", "claude-fable-5[1m]", @@ -81,6 +82,7 @@ describe("getClaudeModels", () => { expect(contextWindows).toEqual( new Map([ + ["claude-fable-5-1", 1_000_000], ["claude-opus-5", 1_000_000], ["claude-fable-5", 1_000_000], ["claude-fable-5[1m]", 1_000_000], @@ -160,6 +162,9 @@ describe("getClaudeModels", () => { expect(models.get("claude-fable-5")?.thinkingOptions?.map((option) => option.id)).not.toContain( CLAUDE_DISABLED_THINKING_OPTION_ID, ); + expect( + models.get("claude-fable-5-1")?.thinkingOptions?.map((option) => option.id), + ).not.toContain(CLAUDE_DISABLED_THINKING_OPTION_ID); expect(models.get("claude-haiku-4-5")?.thinkingOptions).toBeUndefined(); }); @@ -170,6 +175,7 @@ describe("getClaudeModels", () => { ["claude-sonnet-5[1m]", true, "high"], ["claude-sonnet-5-20260101", true, "high"], ["claude-fable-5", false, "high"], + ["claude-fable-5-1", false, "high"], ["claude-haiku-4-5", false, undefined], ["openrouter/anthropic/claude-opus-4-8", false, undefined], [null, false, undefined], @@ -458,7 +464,7 @@ describe("Claude Opus 5 catalog", () => { describe("Claude Fable 5 catalog", () => { it("offers one selectable Fable 5 entry and a compatibility entry for old apps", () => { const fable5Models = getClaudeModels() - .filter((model) => model.id.startsWith("claude-fable-5")) + .filter((model) => model.id === "claude-fable-5" || model.id === "claude-fable-5[1m]") .map(({ id, aliases, isSelectable, label, contextWindowMaxTokens }) => ({ id, aliases, @@ -491,6 +497,24 @@ describe("Claude Fable 5 catalog", () => { }); }); +describe("Claude Fable 5.1 catalog", () => { + it("offers one Fable 5.1 entry with a 1M context window", () => { + const fable51Models = getClaudeModels() + .filter((model) => model.id.startsWith("claude-fable-5-1")) + .map(({ id, label, contextWindowMaxTokens }) => ({ id, label, contextWindowMaxTokens })); + + expect(fable51Models).toEqual([ + { id: "claude-fable-5-1", label: "Fable 5.1", contextWindowMaxTokens: 1_000_000 }, + ]); + }); + + it("resolves suffixed and dated Fable 5.1 IDs to the catalog entry", () => { + expect(findClaudeModel("claude-fable-5-1[1m]")?.id).toBe("claude-fable-5-1"); + expect(findClaudeModel("claude-fable-5-1-20260901")?.id).toBe("claude-fable-5-1"); + expect(findClaudeModel("claude-fable-5-1-20260901[1m]")?.id).toBe("claude-fable-5-1"); + }); +}); + describe("claudeManifestModelSupportsFastMode", () => { it("keeps fast mode strict to first-party manifest model IDs", () => { expect(normalizeClaudeManifestModelId("openrouter/anthropic/claude-opus-4-8")).toBeNull(); @@ -502,5 +526,6 @@ describe("claudeManifestModelSupportsFastMode", () => { expect(claudeManifestModelSupportsFastMode("claude-opus-5")).toBe(true); expect(claudeManifestModelSupportsFastMode("claude-sonnet-5")).toBe(false); expect(claudeManifestModelSupportsFastMode("claude-fable-5")).toBe(false); + expect(claudeManifestModelSupportsFastMode("claude-fable-5-1")).toBe(false); }); });