From a79b0fa3d8636946da750a4ec38c15499da5733f Mon Sep 17 00:00:00 2001 From: gengjiawen Date: Tue, 10 Nov 2026 08:15:29 +0800 Subject: [PATCH] feat(claude): add Fable 5.2 and Mythos 5.2 to the model catalog Claude Code 2.1.321 ships Fable 5.2 as the default Fable model and names Mythos 5.2 as its current Mythos model. Without manifest entries Paseo listed neither, and normalized a runtime claude-fable-5-2 to claude-fable-5, so a session on Fable 5.2 was recorded and labeled as Fable 5. Fable 5.2 is gated on Claude Code 2.1.321. Mythos 5.2 stays behind discovery like Mythos 5.1. Both get a 1M context window, always-on thinking with high default effort, and fast mode, matching what Claude Code advertises. Fable 5.1 becomes the previous release. --- packages/cli/tests/15-provider.test.ts | 16 ++- .../agent/providers/claude/agent.test.ts | 1 + .../agent/providers/claude/model-manifest.ts | 20 ++- .../agent/providers/claude/models.test.ts | 123 +++++++++++++++++- 4 files changed, 147 insertions(+), 13 deletions(-) diff --git a/packages/cli/tests/15-provider.test.ts b/packages/cli/tests/15-provider.test.ts index 3a4f33031c..5037a65ad9 100644 --- a/packages/cli/tests/15-provider.test.ts +++ b/packages/cli/tests/15-provider.test.ts @@ -62,10 +62,15 @@ const EXPECTED_CLAUDE_MODELS = [ model: "Opus 5", descriptionFragment: "Previous release", }, + { + id: "claude-fable-5-2", + model: "Fable 5.2", + descriptionFragment: "Most powerful", + }, { id: "claude-fable-5-1", model: "Fable 5.1", - descriptionFragment: "Most powerful", + descriptionFragment: "Previous release", }, { id: "claude-fable-5", @@ -199,11 +204,10 @@ function assertClaudeModels(data: ProviderModel[]): void { ); } - const fable51Index = data.findIndex((model) => model.id === "claude-fable-5-1"); - const fable5Index = data.findIndex((model) => model.id === "claude-fable-5"); - assert.strictEqual( - fable5Index, - fable51Index + 1, + const fable52Index = data.findIndex((model) => model.id === "claude-fable-5-2"); + assert.deepStrictEqual( + data.slice(fable52Index, fable52Index + 3).map((model) => model.id), + ["claude-fable-5-2", "claude-fable-5-1", "claude-fable-5"], "Fable models should stay adjacent and newest-first", ); assert( 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 e47fcb6da7..b654745519 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -1614,6 +1614,7 @@ describe("ClaudeAgentSession features", () => { test.each([ ["supported model", "claude-opus-4-8", { type: "disabled" }, undefined], ["Mythos 5.1", "claude-mythos-5-1", { type: "adaptive", display: "summarized" }, "high"], + ["Fable 5.2", "claude-fable-5-2", { type: "adaptive", display: "summarized" }, "high"], ["Haiku 5.5", "claude-haiku-5-5", { type: "disabled" }, undefined], ["unsupported model", "claude-fable-5", { type: "adaptive", display: "summarized" }, "high"], ["custom model", "openrouter/anthropic/claude-opus-4-8", undefined, undefined], 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 45a5441109..686be00258 100644 --- a/packages/server/src/server/agent/providers/claude/model-manifest.ts +++ b/packages/server/src/server/agent/providers/claude/model-manifest.ts @@ -58,6 +58,15 @@ export const CLAUDE_MODEL_MANIFEST = [ supportsThinkingDisabled: true, supportsFastMode: true, }, + { + id: "claude-mythos-5-2", + label: "Mythos 5.2", + requiresDiscovery: true, + description: "Mythos 5.2 · Requires verified access", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsFastMode: true, + }, { id: "claude-mythos-5-1", label: "Mythos 5.1", @@ -66,10 +75,19 @@ export const CLAUDE_MODEL_MANIFEST = [ contextWindowMaxTokens: 1_000_000, effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, }, + { + id: "claude-fable-5-2", + label: "Fable 5.2", + description: "Fable 5.2 · Most powerful model", + minimumClaudeCodeVersion: "2.1.321", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + supportsFastMode: true, + }, { id: "claude-fable-5-1", label: "Fable 5.1", - description: "Fable 5.1 · Most powerful model", + description: "Fable 5.1 · Previous release", contextWindowMaxTokens: 1_000_000, effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, }, 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 464964cf3a..d7c57bdd98 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -61,17 +61,23 @@ async function createClaudeConfigDirWithRawSettings(settings: string): Promise claudeCodeVersion, discoverModels: async () => [ { value: "mythos", - resolvedModel: "claude-mythos-5-1", + resolvedModel: "claude-mythos-5-2", displayName: "Mythos", description: "", }, + { + value: "claude-mythos-5-1", + resolvedModel: "claude-mythos-5-1", + displayName: "Mythos 5.1", + description: "", + }, ], }); } @@ -82,7 +88,9 @@ describe("getClaudeModels", () => { expect(models.map((m) => m.id)).toEqual([ "claude-opus-5-5", "claude-opus-5", + "claude-mythos-5-2", "claude-mythos-5-1", + "claude-fable-5-2", "claude-fable-5-1", "claude-fable-5", "claude-fable-5[1m]", @@ -118,7 +126,9 @@ describe("getClaudeModels", () => { new Map([ ["claude-opus-5-5", 1_000_000], ["claude-opus-5", 1_000_000], + ["claude-mythos-5-2", 1_000_000], ["claude-mythos-5-1", 1_000_000], + ["claude-fable-5-2", 1_000_000], ["claude-fable-5-1", 1_000_000], ["claude-fable-5", 1_000_000], ["claude-fable-5[1m]", 1_000_000], @@ -163,6 +173,12 @@ describe("getClaudeModels", () => { expect(getClaudeModels("2.1.293").find((model) => model.isDefault)?.id).toBe("claude-opus-5-5"); }); + it("gates Fable 5.2 on Claude Code 2.1.321 without changing the default", () => { + expect(getClaudeModels("2.1.320").map((model) => model.id)).not.toContain("claude-fable-5-2"); + expect(getClaudeModels("2.1.321").map((model) => model.id)).toContain("claude-fable-5-2"); + expect(getClaudeModels("2.1.321").find((model) => model.isDefault)?.id).toBe("claude-opus-5-5"); + }); + it("derives thinking options from model effort capabilities", () => { const models = new Map(getClaudeModels().map((model) => [model.id, model])); @@ -232,6 +248,8 @@ describe("getClaudeModels", () => { ["claude-sonnet-5-20260101", true, "high"], ["claude-fable-5", false, "high"], ["claude-fable-5-1", false, "high"], + ["claude-fable-5-2", false, "high"], + ["claude-mythos-5-2", false, "high"], ["claude-haiku-5-5", true, "medium"], ["claude-haiku-5-5-20261007", true, "medium"], ["claude-haiku-4-5", false, undefined], @@ -284,6 +302,31 @@ describe("ClaudeAgentClient.fetchCatalog", () => { }, ); + it("offers Mythos 5.2 in place of Mythos 5.1 when Claude Code resolves mythos to 5.2", async () => { + const configDir = await createClaudeConfigDir({}); + const client = new ClaudeAgentClient({ + logger: createTestLogger(), + resolveVersion: async () => "2.1.321", + runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } }, + discoverModels: async () => [ + { + value: "mythos", + resolvedModel: "claude-mythos-5-2", + displayName: "Mythos", + description: "", + }, + ], + }); + const { models } = await client.fetchCatalog({ + scope: "workspace", + cwd: configDir, + force: false, + }); + expect(models.filter((model) => model.id.startsWith("claude-mythos-"))).toEqual([ + findClaudeModel("claude-mythos-5-2"), + ]); + }); + it("preserves the curated catalog and explicitly configured models after discovery failure", async () => { const configDir = await createClaudeConfigDir({ model: "claude-mythos-5-1" }); const client = new ClaudeAgentClient({ @@ -299,8 +342,8 @@ describe("ClaudeAgentClient.fetchCatalog", () => { cwd: configDir, force: false, }); - expect(models.filter((model) => model.id !== "claude-mythos-5-1")).toEqual( - getClaudeModels("2.1.293").filter((model) => model.id !== "claude-mythos-5-1"), + expect(models.filter((model) => !model.id.startsWith("claude-mythos-"))).toEqual( + getClaudeModels("2.1.293").filter((model) => !model.id.startsWith("claude-mythos-")), ); expect(models.find((model) => model.id === "claude-mythos-5-1")).toEqual({ ...findClaudeModel("claude-mythos-5-1"), @@ -360,7 +403,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => { }); expect(models.map((model) => model.id)).toEqual( getClaudeModels("2.1.293") - .filter((model) => model.id !== "claude-mythos-5-1") + .filter((model) => !model.id.startsWith("claude-mythos-")) .map((model) => model.id), ); }); @@ -785,6 +828,44 @@ describe("Claude Fable 5.1 catalog", () => { }); }); +describe("Claude Fable 5.2 catalog", () => { + it("offers one 1M model with high default effort, always-on thinking, and fast mode", () => { + const models = getClaudeModels().filter((model) => model.id.startsWith("claude-fable-5-2")); + expect(models).toHaveLength(1); + expect(models[0]).toMatchObject({ + id: "claude-fable-5-2", + label: "Fable 5.2", + contextWindowMaxTokens: 1_000_000, + defaultThinkingOptionId: "high", + }); + expect(models[0].thinkingOptions?.map((option) => option.id)).toEqual([ + "low", + "medium", + "high", + "xhigh", + "max", + CLAUDE_ULTRACODE_THINKING_OPTION_ID, + ]); + expect(claudeManifestModelSupportsFastMode("claude-fable-5-2")).toBe(true); + }); + + it.each([ + "claude-fable-5-2[1m]", + "claude-fable-5-2-20261110", + "claude-fable-5-2-20261110[1m]", + "Fable 5.2", + ])("resolves %s to the single catalog entry", (id) => { + expect(findClaudeModel(id)?.id).toBe("claude-fable-5-2"); + }); + + it.each(["claude-fable-5-2", "anthropic/claude-fable-5-2", "us.anthropic.claude-fable-5-2-v1:0"])( + "keeps runtime model %s from collapsing onto Fable 5", + (id) => { + expect(normalizeClaudeRuntimeModelId(id)).toBe("claude-fable-5-2"); + }, + ); +}); + describe("Claude Opus 5.5 catalog", () => { it("offers one Opus 5.5 entry with a 1M context window", () => { const opus55Models = getClaudeModels() @@ -920,7 +1001,7 @@ describe("claudeManifestModelSupportsFastMode", () => { describe("Claude Mythos 5.1 catalog", () => { it("offers one 1M model with high default effort and always-on thinking", () => { - const models = getClaudeModels().filter((model) => model.id.startsWith("claude-mythos")); + const models = getClaudeModels().filter((model) => model.id.startsWith("claude-mythos-5-1")); expect(models).toHaveLength(1); expect(models[0]).toMatchObject({ id: "claude-mythos-5-1", @@ -965,6 +1046,36 @@ describe("Claude Mythos 5.1 catalog", () => { ); }); +describe("Claude Mythos 5.2 catalog", () => { + it("offers one 1M model with high default effort, always-on thinking, and fast mode", () => { + const models = getClaudeModels().filter((model) => model.id.startsWith("claude-mythos-5-2")); + expect(models).toHaveLength(1); + expect(models[0]).toMatchObject({ + id: "claude-mythos-5-2", + label: "Mythos 5.2", + contextWindowMaxTokens: 1_000_000, + defaultThinkingOptionId: "high", + }); + expect(models[0].thinkingOptions?.map((option) => option.id)).toEqual([ + "low", + "medium", + "high", + "xhigh", + "max", + CLAUDE_ULTRACODE_THINKING_OPTION_ID, + ]); + expect(claudeManifestModelSupportsFastMode("claude-mythos-5-2")).toBe(true); + }); + + it.each(["anthropic/claude-mythos-5-2", "us.anthropic.claude-mythos-5-2-v1:0"])( + "normalizes runtime identity without granting first-party capabilities: %s", + (id) => { + expect(normalizeClaudeRuntimeModelId(id)).toBe("claude-mythos-5-2"); + expect(normalizeClaudeManifestModelId(id)).toBeNull(); + }, + ); +}); + describe("Claude model discovery process", () => { it("does not send a prompt or persist a session and closes after discovery", async () => { let closed = false;