Skip to content

Commit e4fdf27

Browse files
committed
feat: implement Claude Sonnet 5 support in Zoo Code
Add full Claude Sonnet 5 support across all provider paths with correct model definitions, adaptive-thinking handling, fetcher overrides, and accurate introductory pricing. Model definitions (anthropic.ts, bedrock.ts, vertex.ts): - 1M native context window (no beta header required) - Adaptive-thinking binary toggle (supportsReasoningBudget/Binary) - supportsTemperature: false (sampling params return a 400) - Introductory pricing through Aug 31, 2026: inputPrice 2.0, outputPrice 10.0, cacheWrites 2.5, cacheReads 0.2 (standard $3/$15 rates take effect Sep 1, 2026) Provider handling: - Add sonnet-5 to the Bedrock adaptive-thinking guard - Add claude-sonnet-5 to OpenRouter and Vercel AI Gateway allow-lists - Apply Sonnet 5 overrides (maxTokens, reasoning budget, temperature) in the openrouter, requesty, and vercel-ai-gateway fetchers Tests: - Add Sonnet 5 coverage across provider and fetcher test suites Closes #777
1 parent 9a2e8d8 commit e4fdf27

19 files changed

Lines changed: 399 additions & 8 deletions

packages/types/src/providers/anthropic.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,25 @@ export const anthropicModels = {
2828
},
2929
],
3030
},
31+
"claude-sonnet-5": {
32+
maxTokens: 128_000, // Overridden to 8k if `enableReasoningEffort` is false.
33+
contextWindow: 1_000_000, // 1M context window native (no beta header required)
34+
supportsImages: true,
35+
supportsPromptCache: true,
36+
inputPrice: 2.0, // $2 per million input tokens (introductory pricing through Aug 31, 2026)
37+
outputPrice: 10.0, // $10 per million output tokens (introductory pricing through Aug 31, 2026)
38+
cacheWritesPrice: 2.5, // $2.50 per million tokens (introductory pricing through Aug 31, 2026)
39+
cacheReadsPrice: 0.2, // $0.20 per million tokens (introductory pricing through Aug 31, 2026)
40+
// Sonnet 5 uses the same adaptive-thinking / binary-toggle convention as
41+
// Opus 4.7+ and Fable 5 on the direct Anthropic provider path. Manual
42+
// extended thinking (budget_tokens) is removed and returns a 400, and
43+
// setting sampling parameters (temperature/top_p/top_k) returns a 400.
44+
supportsReasoningBudget: true,
45+
supportsReasoningBinary: true,
46+
supportsTemperature: false,
47+
description:
48+
"Claude Sonnet 5 is the best combination of speed and intelligence, optimized for coding, tool use, and agentic workflows.",
49+
},
3150
"claude-sonnet-4-5": {
3251
maxTokens: 64_000, // Overridden to 8k if `enableReasoningEffort` is false.
3352
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'

packages/types/src/providers/bedrock.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ export const bedrockModels = {
5151
},
5252
],
5353
},
54+
"anthropic.claude-sonnet-5": {
55+
maxTokens: 8192,
56+
contextWindow: 1_000_000, // 1M context window native (no beta header required)
57+
supportsImages: true,
58+
supportsPromptCache: true,
59+
supportsReasoningBudget: true,
60+
supportsReasoningBinary: true,
61+
supportsTemperature: false,
62+
inputPrice: 2.0, // $2 per million input tokens (introductory pricing through Aug 31, 2026)
63+
outputPrice: 10.0, // $10 per million output tokens (introductory pricing through Aug 31, 2026)
64+
cacheWritesPrice: 2.5, // $2.50 per million tokens (introductory pricing through Aug 31, 2026)
65+
cacheReadsPrice: 0.2, // $0.20 per million tokens (introductory pricing through Aug 31, 2026)
66+
minTokensPerCachePoint: 1024,
67+
maxCachePoints: 4,
68+
cachableFields: ["system", "messages", "tools"],
69+
description:
70+
"Claude Sonnet 5 is the best combination of speed and intelligence, optimized for coding, tool use, and agentic workflows.",
71+
},
5472
"amazon.nova-pro-v1:0": {
5573
maxTokens: 5000,
5674
contextWindow: 300_000,
@@ -602,6 +620,7 @@ export const BEDROCK_1M_CONTEXT_MODEL_IDS = [
602620
// - Claude Sonnet 4
603621
// - Claude Sonnet 4.5
604622
// - Claude Sonnet 4.6
623+
// - Claude Sonnet 5
605624
// - Claude Haiku 4.5
606625
// - Claude Opus 4.5
607626
// - Claude Opus 4.6
@@ -611,6 +630,7 @@ export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [
611630
"anthropic.claude-sonnet-4-20250514-v1:0",
612631
"anthropic.claude-sonnet-4-5-20250929-v1:0",
613632
"anthropic.claude-sonnet-4-6",
633+
"anthropic.claude-sonnet-5",
614634
"anthropic.claude-haiku-4-5-20251001-v1:0",
615635
"anthropic.claude-opus-4-5-20251101-v1:0",
616636
"anthropic.claude-opus-4-6-v1",

packages/types/src/providers/openrouter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const OPEN_ROUTER_PROMPT_CACHING_MODELS = new Set([
3939
"anthropic/claude-sonnet-4",
4040
"anthropic/claude-sonnet-4.5",
4141
"anthropic/claude-sonnet-4.6",
42+
"anthropic/claude-sonnet-5",
4243
"anthropic/claude-opus-4",
4344
"anthropic/claude-opus-4.1",
4445
"anthropic/claude-opus-4.5",
@@ -79,6 +80,7 @@ export const OPEN_ROUTER_REASONING_BUDGET_MODELS = new Set([
7980
"anthropic/claude-sonnet-4",
8081
"anthropic/claude-sonnet-4.5",
8182
"anthropic/claude-sonnet-4.6",
83+
"anthropic/claude-sonnet-5",
8284
"anthropic/claude-haiku-4.5",
8385
"google/gemini-2.5-pro-preview",
8486
"google/gemini-2.5-pro",

packages/types/src/providers/vercel-ai-gateway.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS = new Set([
1616
"anthropic/claude-fable-5",
1717
"anthropic/claude-sonnet-4",
1818
"anthropic/claude-sonnet-4.6",
19+
"anthropic/claude-sonnet-5",
1920
"openai/gpt-4.1",
2021
"openai/gpt-4.1-mini",
2122
"openai/gpt-4.1-nano",
@@ -59,6 +60,7 @@ export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([
5960
"anthropic/claude-fable-5",
6061
"anthropic/claude-sonnet-4",
6162
"anthropic/claude-sonnet-4.6",
63+
"anthropic/claude-sonnet-5",
6264
"google/gemini-1.5-flash",
6365
"google/gemini-1.5-pro",
6466
"google/gemini-2.0-flash",

packages/types/src/providers/vertex.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,21 @@ export const vertexModels = {
355355
},
356356
],
357357
},
358+
"claude-sonnet-5": {
359+
maxTokens: 8192,
360+
contextWindow: 1_000_000, // 1M context window native (no beta header required)
361+
supportsImages: true,
362+
supportsPromptCache: true,
363+
inputPrice: 2.0, // $2 per million input tokens (introductory pricing through Aug 31, 2026)
364+
outputPrice: 10.0, // $10 per million output tokens (introductory pricing through Aug 31, 2026)
365+
cacheWritesPrice: 2.5, // $2.50 per million tokens (introductory pricing through Aug 31, 2026)
366+
cacheReadsPrice: 0.2, // $0.20 per million tokens (introductory pricing through Aug 31, 2026)
367+
supportsReasoningBudget: true,
368+
supportsReasoningBinary: true,
369+
supportsTemperature: false,
370+
description:
371+
"Claude Sonnet 5 is the best combination of speed and intelligence, optimized for coding, tool use, and agentic workflows.",
372+
},
358373
"claude-haiku-4-5@20251001": {
359374
maxTokens: 8192,
360375
contextWindow: 200_000,

src/api/providers/__tests__/anthropic-vertex.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,23 @@ describe("VertexHandler", () => {
10331033
expect(model.info.supportsTemperature).toBe(false)
10341034
})
10351035

1036+
it("should return Claude Sonnet 5 model info", () => {
1037+
const handler = new AnthropicVertexHandler({
1038+
apiModelId: "claude-sonnet-5",
1039+
vertexProjectId: "test-project",
1040+
vertexRegion: "us-central1",
1041+
})
1042+
1043+
const model = handler.getModel()
1044+
expect(model.id).toBe("claude-sonnet-5")
1045+
expect(model.info.maxTokens).toBe(8192)
1046+
expect(model.info.contextWindow).toBe(1_000_000)
1047+
expect(model.info.supportsReasoningBinary).toBe(true)
1048+
expect(model.info.supportsReasoningBudget).toBe(true)
1049+
expect(model.info.supportsPromptCache).toBe(true)
1050+
expect(model.info.supportsTemperature).toBe(false)
1051+
})
1052+
10361053
it("should not enable 1M context when flag is disabled", () => {
10371054
const handler = new AnthropicVertexHandler({
10381055
apiModelId: VERTEX_1M_CONTEXT_MODEL_IDS[0],
@@ -1309,6 +1326,35 @@ describe("VertexHandler", () => {
13091326
expect(request.thinking).not.toHaveProperty("budget_tokens")
13101327
expect(request.temperature).toBeUndefined()
13111328
})
1329+
1330+
it("should use adaptive thinking for Claude Sonnet 5", async () => {
1331+
const sonnetHandler = new AnthropicVertexHandler({
1332+
apiModelId: "claude-sonnet-5",
1333+
vertexProjectId: "test-project",
1334+
vertexRegion: "us-central1",
1335+
enableReasoningEffort: true,
1336+
})
1337+
1338+
const mockCreate = vitest.fn().mockImplementation(async () => ({
1339+
async *[Symbol.asyncIterator]() {
1340+
yield { type: "message_start", message: { usage: { input_tokens: 10, output_tokens: 5 } } }
1341+
},
1342+
}))
1343+
;(sonnetHandler["client"].messages as any).create = mockCreate
1344+
1345+
await sonnetHandler.createMessage("You are a helpful assistant", [{ role: "user", content: "Hello" }]).next()
1346+
1347+
expect(mockCreate).toHaveBeenCalledWith(
1348+
expect.objectContaining({
1349+
thinking: { type: "adaptive" },
1350+
}),
1351+
undefined,
1352+
)
1353+
1354+
const request = mockCreate.mock.calls[0][0]
1355+
expect(request.thinking).not.toHaveProperty("budget_tokens")
1356+
expect(request.temperature).toBeUndefined()
1357+
})
13121358
})
13131359

13141360
describe("native tool calling", () => {

src/api/providers/__tests__/anthropic.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,33 @@ describe("AnthropicHandler", () => {
428428
expect(requestBody?.max_tokens).toBe(32768)
429429
expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
430430
})
431+
432+
it("should use adaptive thinking for Claude Sonnet 5 when reasoning is enabled", async () => {
433+
const sonnetHandler = new AnthropicHandler({
434+
apiKey: "test-api-key",
435+
apiModelId: "claude-sonnet-5",
436+
enableReasoningEffort: true,
437+
modelMaxTokens: 32768,
438+
})
439+
440+
const stream = sonnetHandler.createMessage(systemPrompt, [
441+
{
442+
role: "user",
443+
content: [{ type: "text" as const, text: "Hello" }],
444+
},
445+
])
446+
447+
for await (const _chunk of stream) {
448+
// Consume stream
449+
}
450+
451+
const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0]
452+
const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1]
453+
expect(requestBody?.thinking).toEqual({ type: "adaptive" })
454+
expect(requestBody?.temperature).toBeUndefined()
455+
expect(requestBody?.max_tokens).toBe(32768)
456+
expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
457+
})
431458
})
432459

433460
describe("completePrompt", () => {
@@ -589,6 +616,23 @@ describe("AnthropicHandler", () => {
589616
expect(model.reasoningBudget).toBeUndefined()
590617
})
591618

619+
it("should handle Claude Sonnet 5 model correctly", () => {
620+
const handler = new AnthropicHandler({
621+
apiKey: "test-api-key",
622+
apiModelId: "claude-sonnet-5",
623+
})
624+
const model = handler.getModel()
625+
expect(model.id).toBe("claude-sonnet-5")
626+
expect(model.info.maxTokens).toBe(128000)
627+
expect(model.info.contextWindow).toBe(1000000)
628+
expect(model.maxTokens).toBe(8192)
629+
expect(model.info.supportsReasoningBinary).toBe(true)
630+
expect(model.info.supportsReasoningBudget).toBe(true)
631+
expect(model.info.supportsPromptCache).toBe(true)
632+
expect(model.info.supportsTemperature).toBe(false)
633+
expect(model.reasoningBudget).toBeUndefined()
634+
})
635+
592636
it("should enable 1M context for Claude 4.5 Sonnet when beta flag is set", () => {
593637
const handler = new AnthropicHandler({
594638
apiKey: "test-api-key",

src/api/providers/__tests__/bedrock.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,37 @@ describe("AwsBedrockHandler", () => {
724724
const model = handler.getModel()
725725
expect(model.id).toBe("global.anthropic.claude-fable-5")
726726
})
727+
728+
it("should return Claude Sonnet 5 model info", () => {
729+
const handler = new AwsBedrockHandler({
730+
apiModelId: "anthropic.claude-sonnet-5",
731+
awsAccessKey: "test",
732+
awsSecretKey: "test",
733+
awsRegion: "us-east-1",
734+
})
735+
736+
const model = handler.getModel()
737+
expect(model.id).toBe("anthropic.claude-sonnet-5")
738+
expect(model.info.contextWindow).toBe(1_000_000)
739+
expect(model.info.supportsReasoningBinary).toBe(true)
740+
expect(model.info.supportsReasoningBudget).toBe(true)
741+
expect(model.info.supportsPromptCache).toBe(true)
742+
expect(model.info.supportsTemperature).toBe(false)
743+
expect(model.maxTokens).toBe(8192)
744+
})
745+
746+
it("should apply global inference prefix for Claude Sonnet 5 when awsUseGlobalInference is true", () => {
747+
const handler = new AwsBedrockHandler({
748+
apiModelId: "anthropic.claude-sonnet-5",
749+
awsAccessKey: "test",
750+
awsSecretKey: "test",
751+
awsRegion: "us-east-1",
752+
awsUseGlobalInference: true,
753+
})
754+
755+
const model = handler.getModel()
756+
expect(model.id).toBe("global.anthropic.claude-sonnet-5")
757+
})
727758
})
728759

729760
describe("1M context beta feature", () => {
@@ -1558,6 +1589,7 @@ describe("AwsBedrockHandler", () => {
15581589
expect(isAdaptiveThinkingModel("anthropic.claude-opus-4-7")).toBe(true)
15591590
expect(isAdaptiveThinkingModel("anthropic.claude-opus-4-8")).toBe(true)
15601591
expect(isAdaptiveThinkingModel("anthropic.claude-fable-5")).toBe(true)
1592+
expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-5")).toBe(true)
15611593
// Future-proof Sonnet patterns — guarded even before a registry entry exists.
15621594
expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-4-7")).toBe(true)
15631595
expect(isAdaptiveThinkingModel("anthropic.claude-sonnet-4-8")).toBe(true)
@@ -1566,6 +1598,7 @@ describe("AwsBedrockHandler", () => {
15661598
it("returns true when the id carries a cross-region or global prefix", () => {
15671599
expect(isAdaptiveThinkingModel("us.anthropic.claude-opus-4-8")).toBe(true)
15681600
expect(isAdaptiveThinkingModel("global.anthropic.claude-fable-5")).toBe(true)
1601+
expect(isAdaptiveThinkingModel("global.anthropic.claude-sonnet-5")).toBe(true)
15691602
expect(isAdaptiveThinkingModel("eu.anthropic.claude-sonnet-4-7")).toBe(true)
15701603
expect(isAdaptiveThinkingModel("global.anthropic.claude-opus-4-8")).toBe(true)
15711604
})

src/api/providers/__tests__/requesty.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ vitest.mock("../fetchers/modelCache", () => ({
6464
cacheReadsPrice: 1,
6565
description: "Claude Fable 5",
6666
},
67+
"anthropic/claude-sonnet-5": {
68+
maxTokens: 128000,
69+
contextWindow: 1000000,
70+
supportsImages: true,
71+
supportsPromptCache: true,
72+
supportsReasoningBudget: true,
73+
supportsReasoningBinary: true,
74+
supportsTemperature: false,
75+
inputPrice: 3,
76+
outputPrice: 15,
77+
cacheWritesPrice: 3.75,
78+
cacheReadsPrice: 0.3,
79+
description: "Claude Sonnet 5",
80+
},
6781
})
6882
}),
6983
}))
@@ -254,6 +268,39 @@ describe("RequestyHandler", () => {
254268
)
255269
})
256270

271+
it("uses adaptive thinking for Claude Sonnet 5 when reasoning is enabled", async () => {
272+
const handler = new RequestyHandler({
273+
requestyApiKey: "test-key",
274+
requestyModelId: "anthropic/claude-sonnet-5",
275+
enableReasoningEffort: true,
276+
modelMaxTokens: 32768,
277+
})
278+
279+
const mockStream = {
280+
async *[Symbol.asyncIterator]() {
281+
yield {
282+
id: "test-id",
283+
choices: [{ delta: {} }],
284+
usage: { prompt_tokens: 10, completion_tokens: 20 },
285+
}
286+
},
287+
}
288+
289+
mockCreate.mockResolvedValue(mockStream)
290+
291+
const generator = handler.createMessage("test system prompt", [{ role: "user" as const, content: "test" }])
292+
await generator.next()
293+
294+
expect(mockCreate).toHaveBeenCalledWith(
295+
expect.objectContaining({
296+
model: "anthropic/claude-sonnet-5",
297+
max_tokens: 32768,
298+
thinking: { type: "adaptive" },
299+
temperature: undefined,
300+
}),
301+
)
302+
})
303+
257304
it("handles API errors", async () => {
258305
const handler = new RequestyHandler(mockOptions)
259306
const mockError = new Error("API Error")
@@ -523,6 +570,23 @@ describe("RequestyHandler", () => {
523570
})
524571
})
525572

573+
it("omits temperature for Claude Sonnet 5 in completePrompt", async () => {
574+
const handler = new RequestyHandler({
575+
requestyApiKey: "test-key",
576+
requestyModelId: "anthropic/claude-sonnet-5",
577+
})
578+
mockCreate.mockResolvedValue({ choices: [{ message: { content: "test completion" } }] })
579+
580+
await handler.completePrompt("test prompt")
581+
582+
expect(mockCreate).toHaveBeenCalledWith({
583+
model: "anthropic/claude-sonnet-5",
584+
max_tokens: 8192,
585+
messages: [{ role: "system", content: "test prompt" }],
586+
temperature: undefined,
587+
})
588+
})
589+
526590
it("handles API errors", async () => {
527591
const handler = new RequestyHandler(mockOptions)
528592
const mockError = new Error("API Error")

0 commit comments

Comments
 (0)