diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index b1d0a2f6b3..1ce113ebdc 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -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") + }) }) }) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index cb48492b60..4dccbfb242 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -43,11 +43,17 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa ): ApiStream { let stream: AnthropicStream 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 }