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
This commit is contained in:
Roo Code 2025-08-19 20:11:20 +00:00
parent b06005d321
commit 5006092f08
2 changed files with 147 additions and 4 deletions

View file

@ -264,5 +264,138 @@ describe("AnthropicHandler", () => {
expect(result.reasoningBudget).toBeUndefined()
expect(result.temperature).toBe(0)
})
it("should enable 1M context window when anthropicBeta1MContext is true for Claude Sonnet 4", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-sonnet-4-20250514",
anthropicBeta1MContext: true,
})
const model = handler.getModel()
// Should have 1M context window when enabled
expect(model.info.contextWindow).toBe(1_000_000)
// Should use tier pricing for >200K context
expect(model.info.inputPrice).toBe(6.0)
expect(model.info.outputPrice).toBe(22.5)
expect(model.info.cacheWritesPrice).toBe(7.5)
expect(model.info.cacheReadsPrice).toBe(0.6)
})
it("should use default context window when anthropicBeta1MContext is false for Claude Sonnet 4", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-sonnet-4-20250514",
anthropicBeta1MContext: false,
})
const model = handler.getModel()
// Should use default context window (200k)
expect(model.info.contextWindow).toBe(200_000)
// Should use default pricing for ≤200K context
expect(model.info.inputPrice).toBe(3.0)
expect(model.info.outputPrice).toBe(15.0)
expect(model.info.cacheWritesPrice).toBe(3.75)
expect(model.info.cacheReadsPrice).toBe(0.3)
})
it("should not affect context window for non-Claude Sonnet 4 models", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-3-5-sonnet-20241022",
anthropicBeta1MContext: true,
})
const model = handler.getModel()
// Should use default context window for non-Sonnet 4 models
expect(model.info.contextWindow).toBe(200_000)
})
})
describe("createMessage with 1M context beta", () => {
it("should include context-1m-2025-08-07 beta header when enabled for Claude Sonnet 4", async () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-sonnet-4-20250514",
anthropicBeta1MContext: true,
})
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
// Verify that the create method was called with the correct beta headers
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: "claude-sonnet-4-20250514",
}),
expect.objectContaining({
headers: expect.objectContaining({
"anthropic-beta": expect.stringContaining("context-1m-2025-08-07"),
}),
}),
)
// Verify that both betas are included
const callArgs = mockCreate.mock.calls[0]
const headers = callArgs[1]?.headers
expect(headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
expect(headers?.["anthropic-beta"]).toContain("context-1m-2025-08-07")
})
it("should not include context-1m beta header when disabled for Claude Sonnet 4", async () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-sonnet-4-20250514",
anthropicBeta1MContext: false,
})
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
// Verify that the create method was called without the 1M context beta
const callArgs = mockCreate.mock.calls[0]
const headers = callArgs[1]?.headers
expect(headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
expect(headers?.["anthropic-beta"]).not.toContain("context-1m-2025-08-07")
})
it("should handle thinking models with 1M context beta correctly", async () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-3-7-sonnet-20250219:thinking",
anthropicBeta1MContext: true, // This shouldn't affect non-Sonnet 4 models
})
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
// Verify that the 1M context beta is NOT included for non-Sonnet 4 models
const callArgs = mockCreate.mock.calls[0]
const headers = callArgs[1]?.headers
expect(headers?.["anthropic-beta"]).toContain("output-128k-2025-02-19")
expect(headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31")
expect(headers?.["anthropic-beta"]).not.toContain("context-1m-2025-08-07")
})
})
})

View file

@ -43,11 +43,17 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
): ApiStream {
let stream: AnthropicStream<Anthropic.Messages.RawMessageStreamEvent>
const cacheControl: CacheControlEphemeral = { type: "ephemeral" }
let { id: modelId, betas = [], maxTokens, temperature, reasoning: thinking } = this.getModel()
let { id: modelId, betas: modelBetas, maxTokens, temperature, reasoning: thinking } = this.getModel()
// Initialize betas array properly
const betas: string[] = modelBetas ? [...modelBetas] : []
// Add 1M context beta flag if enabled for Claude Sonnet 4
if (modelId === "claude-sonnet-4-20250514" && this.options.anthropicBeta1MContext) {
betas.push("context-1m-2025-08-07")
// Only add if not already present
if (!betas.includes("context-1m-2025-08-07")) {
betas.push("context-1m-2025-08-07")
}
}
switch (modelId) {
@ -118,8 +124,12 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
case "claude-3-5-haiku-20241022":
case "claude-3-opus-20240229":
case "claude-3-haiku-20240307":
betas.push("prompt-caching-2024-07-31")
return { headers: { "anthropic-beta": betas.join(",") } }
// Only add prompt caching beta if not already present
if (!betas.includes("prompt-caching-2024-07-31")) {
betas.push("prompt-caching-2024-07-31")
}
// Only set headers if we have betas to include
return betas.length > 0 ? { headers: { "anthropic-beta": betas.join(",") } } : undefined
default:
return undefined
}