From 12cd5345d2c3ec087402873187a57d3104a5dca1 Mon Sep 17 00:00:00 2001 From: Qi Date: Mon, 28 Sep 2026 14:15:24 -0700 Subject: [PATCH] feat(claude): add Sonnet 5.5 with its model capabilities (#5583) Claude Code 2.1.284 introduces Sonnet 5.5 with a native 1M context window, medium default effort, and adaptive thinking that cannot be turned off, so it takes the same shape as Opus 5.5 in the manifest. Sonnet 5.5 rejects `thinking: {"type": "disabled"}` with a 400, so its entry omits supportsThinkingDisabled and its thinking options have no off. It also takes the Sonnet family tagline, which moves Sonnet 5 to Previous release. The catalog gates the entry on Claude Code 2.1.284, the version that recognizes the model, so older installs keep their current catalog. The existing major-minor runtime matching already resolves dashed gateway IDs such as us.anthropic.claude-sonnet-5-5-20260928-v1:0 to the new entry without collapsing them onto Sonnet 5. (cherry picked from commit 4340607093de709578e3fbc9d352a707e469ea9c) --- packages/cli/tests/15-provider.test.ts | 9 ++- .../agent/providers/claude/agent.test.ts | 24 +++++++- .../agent/providers/claude/model-manifest.ts | 11 +++- .../agent/providers/claude/models.test.ts | 56 ++++++++++++++++++- 4 files changed, 95 insertions(+), 5 deletions(-) diff --git a/packages/cli/tests/15-provider.test.ts b/packages/cli/tests/15-provider.test.ts index 57ace59248..bc1183de81 100644 --- a/packages/cli/tests/15-provider.test.ts +++ b/packages/cli/tests/15-provider.test.ts @@ -82,10 +82,15 @@ const EXPECTED_CLAUDE_MODELS = [ model: "Opus 4.8", descriptionFragment: "Previous release", }, + { + id: "claude-sonnet-5-5", + model: "Sonnet 5.5", + descriptionFragment: "Best for everyday tasks", + }, { id: "claude-sonnet-5", model: "Sonnet 5", - descriptionFragment: "Best for everyday tasks", + descriptionFragment: "Previous release", }, { id: "claude-opus-4-7[1m]", @@ -430,7 +435,7 @@ try { "--quiet should print the same ordered model IDs returned by --json", ); assert( - claudeModelsFromJson.some((m) => m.id === "claude-sonnet-5"), + claudeModelsFromJson.some((m) => m.id === "claude-sonnet-5-5"), "captured --json output should include the current Claude everyday model id", ); console.log("✓ provider models --quiet outputs model IDs only\n"); 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 c908e17cf5..e30ba2962f 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -496,7 +496,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => { const client = new ClaudeAgentClient({ logger, resolveBinary: async () => "/test/claude/bin", - resolveVersion: async () => "2.1.219", + resolveVersion: async () => "2.1.284", runtimeSettings: { env: { CLAUDE_CONFIG_DIR: emptyConfigDir } }, }); const { models } = await client.fetchCatalog({ @@ -515,6 +515,8 @@ describe("ClaudeAgentClient.fetchCatalog", () => { expect(getThinkingIds("claude-opus-4-8")).toContain("ultracode"); expect(getThinkingIds("claude-sonnet-5")).toContain("xhigh"); expect(getThinkingIds("claude-sonnet-5")).toContain("ultracode"); + expect(getThinkingIds("claude-sonnet-5-5")).toContain("xhigh"); + expect(getThinkingIds("claude-sonnet-5-5")).not.toContain("off"); expect(getThinkingIds("claude-opus-4-7[1m]")).toContain("ultracode"); expect(getThinkingIds("claude-opus-4-7")).toContain("ultracode"); expect(getThinkingIds("claude-sonnet-4-6")).not.toContain("ultracode"); @@ -1238,6 +1240,26 @@ describe("ClaudeAgentSession features", () => { await session.close(); }); + test("rejects disabled thinking on Sonnet 5.5, which only runs with adaptive thinking", async () => { + const { queryFactory } = createQueryMock(); + const client = new ClaudeAgentClient({ + logger, + queryFactory, + resolveBinary: async () => "/test/claude/bin", + }); + const session = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + model: "claude-sonnet-5-5", + }); + + await expect(session.setThinkingOption?.("off")).rejects.toThrow( + "Thinking option 'off' is not available for model 'claude-sonnet-5-5'", + ); + + await session.close(); + }); + test("rejects an initial disabled-thinking config for an unsupported model", async () => { const { queryFactory } = createQueryMock(); const client = new ClaudeAgentClient({ 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 c18196c5da..e2e64f28fd 100644 --- a/packages/server/src/server/agent/providers/claude/model-manifest.ts +++ b/packages/server/src/server/agent/providers/claude/model-manifest.ts @@ -93,10 +93,19 @@ export const CLAUDE_MODEL_MANIFEST = [ supportsThinkingDisabled: true, supportsFastMode: true, }, + { + id: "claude-sonnet-5-5", + label: "Sonnet 5.5", + description: "Sonnet 5.5 · Best for everyday tasks", + minimumClaudeCodeVersion: "2.1.284", + defaultThinkingOptionId: "medium", + contextWindowMaxTokens: 1_000_000, + effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, + }, { id: "claude-sonnet-5", label: "Sonnet 5", - description: "Sonnet 5 · Best for everyday tasks", + description: "Sonnet 5 · Previous release", contextWindowMaxTokens: 200_000, effortLevels: CLAUDE_EFFORT_LEVELS.xhigh, supportsThinkingDisabled: true, 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 af725ca047..0e187734b8 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -39,7 +39,7 @@ async function createClaudeConfigDirWithRawSettings(settings: string): Promise claudeCodeVersion, @@ -57,6 +57,7 @@ describe("getClaudeModels", () => { "claude-fable-5[1m]", "claude-opus-4-8[1m]", "claude-opus-4-8", + "claude-sonnet-5-5", "claude-sonnet-5", "claude-sonnet-5[1m]", "claude-opus-4-7[1m]", @@ -90,6 +91,7 @@ describe("getClaudeModels", () => { ["claude-fable-5[1m]", 1_000_000], ["claude-opus-4-8[1m]", 1_000_000], ["claude-opus-4-8", 200_000], + ["claude-sonnet-5-5", 1_000_000], ["claude-sonnet-5", 200_000], ["claude-sonnet-5[1m]", 1_000_000], ["claude-opus-4-7[1m]", 1_000_000], @@ -116,6 +118,9 @@ describe("getClaudeModels", () => { expect(getClaudeModels("2.1.279").find((model) => model.isDefault)?.id).toBe("claude-opus-5"); expect(getClaudeModels("2.1.280").map((model) => model.id)).toContain("claude-opus-5-5"); expect(getClaudeModels("2.1.280").find((model) => model.isDefault)?.id).toBe("claude-opus-5-5"); + + expect(getClaudeModels("2.1.283").map((model) => model.id)).not.toContain("claude-sonnet-5-5"); + expect(getClaudeModels("2.1.284").map((model) => model.id)).toContain("claude-sonnet-5-5"); }); it("derives thinking options from model effort capabilities", () => { @@ -178,6 +183,8 @@ describe("getClaudeModels", () => { it.each([ ["claude-opus-5-5", false, "medium"], ["claude-opus-5-5-20260401", false, "medium"], + ["claude-sonnet-5-5", false, "medium"], + ["claude-sonnet-5-5-20260928", false, "medium"], ["claude-opus-5", true, "high"], ["claude-opus-5-20260724", true, "high"], ["claude-sonnet-5", true, "high"], @@ -467,6 +474,16 @@ describe("normalizeClaudeRuntimeModelId", () => { "claude-opus-5", ); }); + + it("does not collapse a prefixed Sonnet 5.5 onto Sonnet 5", () => { + expect(normalizeClaudeRuntimeModelId("anthropic/claude-sonnet-5-5")).toBe("claude-sonnet-5-5"); + expect(normalizeClaudeRuntimeModelId("us.anthropic.claude-sonnet-5-5-20260928-v1:0")).toBe( + "claude-sonnet-5-5", + ); + expect(normalizeClaudeRuntimeModelId("us.anthropic.claude-sonnet-5-20260101-v1:0")).toBe( + "claude-sonnet-5", + ); + }); }); describe("parseClaudeCodeVersion", () => { @@ -600,6 +617,42 @@ describe("Claude Opus 5.5 catalog", () => { }); }); +describe("Claude Sonnet 5.5 catalog", () => { + it("offers one Sonnet 5.5 entry with a 1M context window", () => { + const sonnet55Models = getClaudeModels() + .filter((model) => model.id.startsWith("claude-sonnet-5-5")) + .map(({ id, label, contextWindowMaxTokens }) => ({ id, label, contextWindowMaxTokens })); + + expect(sonnet55Models).toEqual([ + { id: "claude-sonnet-5-5", label: "Sonnet 5.5", contextWindowMaxTokens: 1_000_000 }, + ]); + }); + + it("offers every effort level except off, because Sonnet 5.5 cannot disable thinking", () => { + const sonnet55 = getClaudeModels().find((model) => model.id === "claude-sonnet-5-5"); + + expect(sonnet55?.thinkingOptions?.map((option) => option.id)).toEqual([ + "low", + "medium", + "high", + "xhigh", + "max", + CLAUDE_ULTRACODE_THINKING_OPTION_ID, + ]); + expect(sonnet55?.defaultThinkingOptionId).toBe("medium"); + expect( + sonnet55?.thinkingOptions?.filter((option) => option.isDefault).map((option) => option.id), + ).toEqual(["medium"]); + }); + + it("resolves suffixed and dated Sonnet 5.5 IDs to the single catalog entry", () => { + expect(findClaudeModel("claude-sonnet-5-5[1m]")?.id).toBe("claude-sonnet-5-5"); + expect(findClaudeModel("claude-sonnet-5-5-20260928")?.id).toBe("claude-sonnet-5-5"); + expect(findClaudeModel("claude-sonnet-5-5-20260928[1m]")?.id).toBe("claude-sonnet-5-5"); + expect(findClaudeModel("claude-sonnet-5-5[1m]")?.contextWindowMaxTokens).toBe(1_000_000); + }); +}); + describe("claudeManifestModelSupportsFastMode", () => { it("keeps fast mode strict to first-party manifest model IDs", () => { expect(normalizeClaudeManifestModelId("openrouter/anthropic/claude-opus-4-8")).toBeNull(); @@ -610,6 +663,7 @@ describe("claudeManifestModelSupportsFastMode", () => { it("supports fast mode on Opus 5 but not on other Claude 5 models", () => { expect(claudeManifestModelSupportsFastMode("claude-opus-5-5")).toBe(true); expect(claudeManifestModelSupportsFastMode("claude-opus-5")).toBe(true); + expect(claudeManifestModelSupportsFastMode("claude-sonnet-5-5")).toBe(false); expect(claudeManifestModelSupportsFastMode("claude-sonnet-5")).toBe(false); expect(claudeManifestModelSupportsFastMode("claude-fable-5")).toBe(false); expect(claudeManifestModelSupportsFastMode("claude-fable-5-1")).toBe(false);