Skip to content

Commit 5006092

Browse files
committed
fix: improve Claude Sonnet 4 1M context window beta header handling
- Fixed beta header array initialization to prevent mutation of model betas - Added proper deduplication of beta headers to avoid duplicates - Ensured context-1m-2025-08-07 beta is properly combined with prompt caching beta - Added comprehensive tests for 1M context window feature Fixes #7229
1 parent b06005d commit 5006092

File tree

2 files changed

+147
-4
lines changed

2 files changed

+147
-4
lines changed

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

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,5 +264,138 @@ describe("AnthropicHandler", () => {
264264
expect(result.reasoningBudget).toBeUndefined()
265265
expect(result.temperature).toBe(0)
266266
})
267+
268+
it("should enable 1M context window when anthropicBeta1MContext is true for Claude Sonnet 4", () => {
269+
const handler = new AnthropicHandler({
270+
apiKey: "test-api-key",
271+
apiModelId: "claude-sonnet-4-20250514",
272+
anthropicBeta1MContext: true,
273+
})
274+
275+
const model = handler.getModel()
276+
277+
// Should have 1M context window when enabled
278+
expect(model.info.contextWindow).toBe(1_000_000)
279+
// Should use tier pricing for >200K context
280+
expect(model.info.inputPrice).toBe(6.0)
281+
expect(model.info.outputPrice).toBe(22.5)
282+
expect(model.info.cacheWritesPrice).toBe(7.5)
283+
expect(model.info.cacheReadsPrice).toBe(0.6)
284+
})
285+
286+
it("should use default context window when anthropicBeta1MContext is false for Claude Sonnet 4", () => {
287+
const handler = new AnthropicHandler({
288+
apiKey: "test-api-key",
289+
apiModelId: "claude-sonnet-4-20250514",
290+
anthropicBeta1MContext: false,
291+
})
292+
293+
const model = handler.getModel()
294+
295+
// Should use default context window (200k)
296+
expect(model.info.contextWindow).toBe(200_000)
297+
// Should use default pricing for ≤200K context
298+
expect(model.info.inputPrice).toBe(3.0)
299+
expect(model.info.outputPrice).toBe(15.0)
300+
expect(model.info.cacheWritesPrice).toBe(3.75)
301+
expect(model.info.cacheReadsPrice).toBe(0.3)
302+
})
303+
304+
it("should not affect context window for non-Claude Sonnet 4 models", () => {
305+
const handler = new AnthropicHandler({
306+
apiKey: "test-api-key",
307+
apiModelId: "claude-3-5-sonnet-20241022",
308+
anthropicBeta1MContext: true,
309+
})
310+
311+
const model = handler.getModel()
312+
313+
// Should use default context window for non-Sonnet 4 models
314+
expect(model.info.contextWindow).toBe(200_000)
315+
})
316+
})
317+
318+
describe("createMessage with 1M context beta", () => {
319+
it("should include context-1m-2025-08-07 beta header when enabled for Claude Sonnet 4", async () => {
320+
const handler = new AnthropicHandler({
321+
apiKey: "test-api-key",
322+
apiModelId: "claude-sonnet-4-20250514",
323+
anthropicBeta1MContext: true,
324+
})
325+
326+
const systemPrompt = "You are a helpful assistant."
327+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
328+
329+
const stream = handler.createMessage(systemPrompt, messages)
330+
const chunks: any[] = []
331+
for await (const chunk of stream) {
332+
chunks.push(chunk)
333+
}
334+
335+
// Verify that the create method was called with the correct beta headers
336+
expect(mockCreate).toHaveBeenCalledWith(
337+
expect.objectContaining({
338+
model: "claude-sonnet-4-20250514",
339+
}),
340+
expect.objectContaining({
341+
headers: expect.objectContaining({
342+
"anthropic-beta": expect.stringContaining("context-1m-2025-08-07"),
343+
}),
344+
}),
345+
)
346+
347+
// Verify that both betas are included
348+
const callArgs = mockCreate.mock.calls[0]
349+
const headers = callArgs[1]?.headers
350+
expect(headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
351+
expect(headers?.["anthropic-beta"]).toContain("context-1m-2025-08-07")
352+
})
353+
354+
it("should not include context-1m beta header when disabled for Claude Sonnet 4", async () => {
355+
const handler = new AnthropicHandler({
356+
apiKey: "test-api-key",
357+
apiModelId: "claude-sonnet-4-20250514",
358+
anthropicBeta1MContext: false,
359+
})
360+
361+
const systemPrompt = "You are a helpful assistant."
362+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
363+
364+
const stream = handler.createMessage(systemPrompt, messages)
365+
const chunks: any[] = []
366+
for await (const chunk of stream) {
367+
chunks.push(chunk)
368+
}
369+
370+
// Verify that the create method was called without the 1M context beta
371+
const callArgs = mockCreate.mock.calls[0]
372+
const headers = callArgs[1]?.headers
373+
expect(headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
374+
expect(headers?.["anthropic-beta"]).not.toContain("context-1m-2025-08-07")
375+
})
376+
377+
it("should handle thinking models with 1M context beta correctly", async () => {
378+
const handler = new AnthropicHandler({
379+
apiKey: "test-api-key",
380+
apiModelId: "claude-3-7-sonnet-20250219:thinking",
381+
anthropicBeta1MContext: true, // This shouldn't affect non-Sonnet 4 models
382+
})
383+
384+
const systemPrompt = "You are a helpful assistant."
385+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
386+
387+
const stream = handler.createMessage(systemPrompt, messages)
388+
const chunks: any[] = []
389+
for await (const chunk of stream) {
390+
chunks.push(chunk)
391+
}
392+
393+
// Verify that the 1M context beta is NOT included for non-Sonnet 4 models
394+
const callArgs = mockCreate.mock.calls[0]
395+
const headers = callArgs[1]?.headers
396+
expect(headers?.["anthropic-beta"]).toContain("output-128k-2025-02-19")
397+
expect(headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
398+
expect(headers?.["anthropic-beta"]).not.toContain("context-1m-2025-08-07")
399+
})
267400
})
268401
})

src/api/providers/anthropic.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,17 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
4343
): ApiStream {
4444
let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
4545
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
46-
let { id: modelId, betas = [], maxTokens, temperature, reasoning: thinking } = this.getModel()
46+
let { id: modelId, betas: modelBetas, maxTokens, temperature, reasoning: thinking } = this.getModel()
47+
48+
// Initialize betas array properly
49+
const betas: string[] = modelBetas ? [...modelBetas] : []
4750

4851
// Add 1M context beta flag if enabled for Claude Sonnet 4
4952
if (modelId === "claude-sonnet-4-20250514" && this.options.anthropicBeta1MContext) {
50-
betas.push("context-1m-2025-08-07")
53+
// Only add if not already present
54+
if (!betas.includes("context-1m-2025-08-07")) {
55+
betas.push("context-1m-2025-08-07")
56+
}
5157
}
5258

5359
switch (modelId) {
@@ -118,8 +124,12 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
118124
case "claude-3-5-haiku-20241022":
119125
case "claude-3-opus-20240229":
120126
case "claude-3-haiku-20240307":
121-
betas.push("prompt-caching-2024-07-31")
122-
return { headers: { "anthropic-beta": betas.join(",") } }
127+
// Only add prompt caching beta if not already present
128+
if (!betas.includes("prompt-caching-2024-07-31")) {
129+
betas.push("prompt-caching-2024-07-31")
130+
}
131+
// Only set headers if we have betas to include
132+
return betas.length > 0 ? { headers: { "anthropic-beta": betas.join(",") } } : undefined
123133
default:
124134
return undefined
125135
}

0 commit comments

Comments
 (0)