From ab7543ae1dfd0bb9a7a1ad5e768f63f066edcde7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 1 Jul 2026 19:47:27 +0200 Subject: [PATCH] feat(providers): add Claude Sonnet 5 --- packages/cli/tests/15-provider.test.ts | 7 +++- .../agent/providers/claude/agent.test.ts | 3 ++ .../agent/providers/claude/models.test.ts | 4 +++ .../server/agent/providers/claude/models.ts | 32 ++++++++++++------- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/cli/tests/15-provider.test.ts b/packages/cli/tests/15-provider.test.ts index aaf0bf0f1a..05dc782d78 100644 --- a/packages/cli/tests/15-provider.test.ts +++ b/packages/cli/tests/15-provider.test.ts @@ -60,6 +60,11 @@ const EXPECTED_CLAUDE_MODELS = [ model: "Opus 4.8", descriptionFragment: "Latest release", }, + { + id: "claude-sonnet-5", + model: "Sonnet 5", + descriptionFragment: "Efficient for routine tasks", + }, { id: "claude-opus-4-7[1m]", model: "Opus 4.7 1M", @@ -392,7 +397,7 @@ try { "--quiet should print the current Claude catalog IDs", ); assert( - claudeModelsFromJson.some((m) => m.id === "claude-sonnet-4-6"), + claudeModelsFromJson.some((m) => m.id === "claude-sonnet-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 9000fbc934..f57f4ab284 100644 --- a/packages/server/src/server/agent/providers/claude/agent.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.test.ts @@ -416,6 +416,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => { "claude-fable-5", "claude-opus-4-8[1m]", "claude-opus-4-8", + "claude-sonnet-5", "claude-opus-4-7[1m]", "claude-opus-4-7", "claude-opus-4-6[1m]", @@ -457,6 +458,8 @@ describe("ClaudeAgentClient.fetchCatalog", () => { expect(getThinkingIds("claude-fable-5")).toContain("ultracode"); expect(getThinkingIds("claude-opus-4-8[1m]")).toContain("ultracode"); expect(getThinkingIds("claude-opus-4-8")).toContain("ultracode"); + expect(getThinkingIds("claude-sonnet-5")).toContain("xhigh"); + expect(getThinkingIds("claude-sonnet-5")).not.toContain("ultracode"); expect(getThinkingIds("claude-opus-4-7")).not.toContain("ultracode"); expect(getThinkingIds("claude-sonnet-4-6")).not.toContain("ultracode"); } finally { 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 3ed41aa046..7264052d23 100644 --- a/packages/server/src/server/agent/providers/claude/models.test.ts +++ b/packages/server/src/server/agent/providers/claude/models.test.ts @@ -38,6 +38,7 @@ describe("getClaudeModels", () => { "claude-fable-5", "claude-opus-4-8[1m]", "claude-opus-4-8", + "claude-sonnet-5", "claude-opus-4-7[1m]", "claude-opus-4-7", "claude-opus-4-6[1m]", @@ -203,12 +204,14 @@ describe("normalizeClaudeRuntimeModelId", () => { 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-5")).toBe("claude-sonnet-5"); expect(normalizeClaudeRuntimeModelId("claude-sonnet-4-6")).toBe("claude-sonnet-4-6"); expect(normalizeClaudeRuntimeModelId("claude-haiku-4-5")).toBe("claude-haiku-4-5"); }); it("normalizes dated model IDs to base model", () => { expect(normalizeClaudeRuntimeModelId("claude-fable-5-20260301")).toBe("claude-fable-5"); + expect(normalizeClaudeRuntimeModelId("claude-sonnet-5-20260101")).toBe("claude-sonnet-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"); @@ -216,6 +219,7 @@ describe("normalizeClaudeRuntimeModelId", () => { it("preserves [1m] suffix from runtime model strings", () => { expect(normalizeClaudeRuntimeModelId("claude-opus-4-6[1m]")).toBe("claude-opus-4-6[1m]"); + expect(normalizeClaudeRuntimeModelId("claude-sonnet-5[1m]")).toBe("claude-sonnet-5"); }); it("returns null for empty/null/undefined", () => { diff --git a/packages/server/src/server/agent/providers/claude/models.ts b/packages/server/src/server/agent/providers/claude/models.ts index 7c8ea10e87..df9d59a9b4 100644 --- a/packages/server/src/server/agent/providers/claude/models.ts +++ b/packages/server/src/server/agent/providers/claude/models.ts @@ -12,7 +12,7 @@ const CLAUDE_THINKING_OPTIONS = [ { id: "max", label: "Max" }, ] as const; -const CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS = [ +const CLAUDE_EXTENDED_THINKING_OPTIONS = [ { id: "low", label: "Low" }, { id: "medium", label: "Medium" }, { id: "high", label: "High" }, @@ -21,7 +21,7 @@ const CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS = [ ] as const; const CLAUDE_ULTRACODE_THINKING_OPTIONS = [ - ...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS, + ...CLAUDE_EXTENDED_THINKING_OPTIONS, { id: "ultracode", label: "Ultracode" }, ] as const; @@ -48,19 +48,26 @@ const CLAUDE_MODELS: AgentModelDefinition[] = [ isDefault: true, thinkingOptions: [...CLAUDE_ULTRACODE_THINKING_OPTIONS], }, + { + provider: "claude", + id: "claude-sonnet-5", + label: "Sonnet 5", + description: "Sonnet 5 · Efficient for routine tasks", + thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS], + }, { provider: "claude", id: "claude-opus-4-7[1m]", label: "Opus 4.7 1M", description: "Opus 4.7 with 1M context window", - thinkingOptions: [...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS], + thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS], }, { provider: "claude", id: "claude-opus-4-7", label: "Opus 4.7", description: "Opus 4.7 · Previous release", - thinkingOptions: [...CLAUDE_OPUS_EXTENDED_THINKING_OPTIONS], + thinkingOptions: [...CLAUDE_EXTENDED_THINKING_OPTIONS], }, { provider: "claude", @@ -218,13 +225,16 @@ 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]}`; + // Fable and Sonnet 5 use single-segment versions, not the {major}-{minor} + // scheme of older opus/sonnet/haiku models. They are natively 1M, so map + // dated or suffixed runtime strings back to the one catalog ID. + const singleSegmentMatch = trimmed.match(/(?:claude-)?(fable|sonnet)[-_ ]+(\d+)/i); + if (singleSegmentMatch) { + const family = singleSegmentMatch[1].toLowerCase(); + const major = singleSegmentMatch[2]; + if (family === "fable" || (family === "sonnet" && major === "5")) { + return `claude-${family}-${major}`; + } } // Match: claude-{family}-{major}-{minor}[1m]? possibly followed by a date suffix