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 4340607093)
This commit is contained in:
Qi
2026-09-28 23:36:50 +02:00
committed by Mohamed Boudra
parent 2c6d4e5283
commit 12cd5345d2
4 changed files with 95 additions and 5 deletions
+7 -2
View File
@@ -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");
@@ -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({
@@ -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,
@@ -39,7 +39,7 @@ async function createClaudeConfigDirWithRawSettings(settings: string): Promise<s
return configDir;
}
function createCatalogClient(claudeCodeVersion = "2.1.280"): ClaudeAgentClient {
function createCatalogClient(claudeCodeVersion = "2.1.284"): ClaudeAgentClient {
return new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => 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);