Add Anthropic Claude Sonnet 4.6 support across providers (#11509)

* Add Anthropic Claude Sonnet 4.6 support across providers

Add model definitions and capability flags for Anthropic, Bedrock,
Vertex, OpenRouter, and Vercel AI Gateway.

Update Anthropic handler and UI model selection logic to support Claude
Sonnet 4.6 1M context behavior and tier pricing.

Add focused tests for provider handlers, fetchers, and selected-model
hooks.

Keep Bedrock UI tier pricing parity as-is because this is a
pre-existing issue for Opus 4.6 and will be handled separately.

Reference:
- https://www.anthropic.com/news/claude-sonnet-4-6
- https://platform.claude.com/docs/en/about-claude/models/overview#latest-models-comparison

* Delete .changeset/soft-carpets-hunt.md

---------

Co-authored-by: Hannes Rudolph <hrudolph@gmail.com>
This commit is contained in:
Peter Dave Hello 2026-02-18 03:28:18 +08:00 committed by GitHub
parent dc243e4cf9
commit 90e2451ad1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 225 additions and 4 deletions

View file

@ -7,6 +7,27 @@ export type AnthropicModelId = keyof typeof anthropicModels
export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-4-5"
export const anthropicModels = {
"claude-sonnet-4-6": {
maxTokens: 64_000, // Overridden to 8k if `enableReasoningEffort` is false.
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheReadsPrice: 0.3, // $0.30 per million tokens
supportsReasoningBudget: true,
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
tiers: [
{
contextWindow: 1_000_000, // 1M tokens with beta flag
inputPrice: 6.0, // $6 per million input tokens (>200K context)
outputPrice: 22.5, // $22.50 per million output tokens (>200K context)
cacheWritesPrice: 7.5, // $7.50 per million tokens (>200K context)
cacheReadsPrice: 0.6, // $0.60 per million tokens (>200K context)
},
],
},
"claude-sonnet-4-5": {
maxTokens: 64_000, // Overridden to 8k if `enableReasoningEffort` is false.
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'

View file

@ -27,6 +27,30 @@ export const bedrockModels = {
maxCachePoints: 4,
cachableFields: ["system", "messages", "tools"],
},
"anthropic.claude-sonnet-4-6-20260114-v1:0": {
maxTokens: 8192,
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'
supportsImages: true,
supportsPromptCache: true,
supportsReasoningBudget: true,
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheReadsPrice: 0.3, // $0.30 per million tokens
minTokensPerCachePoint: 1024,
maxCachePoints: 4,
cachableFields: ["system", "messages", "tools"],
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
tiers: [
{
contextWindow: 1_000_000, // 1M tokens with beta flag
inputPrice: 6.0, // $6 per million input tokens (>200K context)
outputPrice: 22.5, // $22.50 per million output tokens (>200K context)
cacheWritesPrice: 7.5, // $7.50 per million tokens (>200K context)
cacheReadsPrice: 0.6, // $0.60 per million tokens (>200K context)
},
],
},
"amazon.nova-pro-v1:0": {
maxTokens: 5000,
contextWindow: 300_000,
@ -499,6 +523,7 @@ export const BEDROCK_REGIONS = [
export const BEDROCK_1M_CONTEXT_MODEL_IDS = [
"anthropic.claude-sonnet-4-20250514-v1:0",
"anthropic.claude-sonnet-4-5-20250929-v1:0",
"anthropic.claude-sonnet-4-6-20260114-v1:0",
"anthropic.claude-opus-4-6-v1",
] as const
@ -506,12 +531,14 @@ export const BEDROCK_1M_CONTEXT_MODEL_IDS = [
// As of Nov 2025, AWS supports Global Inference for:
// - Claude Sonnet 4
// - Claude Sonnet 4.5
// - Claude Sonnet 4.6
// - Claude Haiku 4.5
// - Claude Opus 4.5
// - Claude Opus 4.6
export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [
"anthropic.claude-sonnet-4-20250514-v1:0",
"anthropic.claude-sonnet-4-5-20250929-v1:0",
"anthropic.claude-sonnet-4-6-20260114-v1:0",
"anthropic.claude-haiku-4-5-20251001-v1:0",
"anthropic.claude-opus-4-5-20251101-v1:0",
"anthropic.claude-opus-4-6-v1",

View file

@ -38,6 +38,7 @@ export const OPEN_ROUTER_PROMPT_CACHING_MODELS = new Set([
"anthropic/claude-3.7-sonnet:thinking",
"anthropic/claude-sonnet-4",
"anthropic/claude-sonnet-4.5",
"anthropic/claude-sonnet-4.6",
"anthropic/claude-opus-4",
"anthropic/claude-opus-4.1",
"anthropic/claude-opus-4.5",
@ -75,6 +76,7 @@ export const OPEN_ROUTER_REASONING_BUDGET_MODELS = new Set([
"anthropic/claude-opus-4.6",
"anthropic/claude-sonnet-4",
"anthropic/claude-sonnet-4.5",
"anthropic/claude-sonnet-4.6",
"anthropic/claude-haiku-4.5",
"google/gemini-2.5-pro-preview",
"google/gemini-2.5-pro",

View file

@ -14,6 +14,7 @@ export const VERCEL_AI_GATEWAY_PROMPT_CACHING_MODELS = new Set([
"anthropic/claude-opus-4.5",
"anthropic/claude-opus-4.6",
"anthropic/claude-sonnet-4",
"anthropic/claude-sonnet-4.6",
"openai/gpt-4.1",
"openai/gpt-4.1-mini",
"openai/gpt-4.1-nano",
@ -55,6 +56,7 @@ export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([
"anthropic/claude-opus-4.5",
"anthropic/claude-opus-4.6",
"anthropic/claude-sonnet-4",
"anthropic/claude-sonnet-4.6",
"google/gemini-1.5-flash",
"google/gemini-1.5-pro",
"google/gemini-2.0-flash",

View file

@ -263,6 +263,27 @@ export const vertexModels = {
},
],
},
"claude-sonnet-4-6@20260114": {
maxTokens: 8192,
contextWindow: 200_000, // Default 200K, extendable to 1M with beta flag 'context-1m-2025-08-07'
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3.0, // $3 per million input tokens (≤200K context)
outputPrice: 15.0, // $15 per million output tokens (≤200K context)
cacheWritesPrice: 3.75, // $3.75 per million tokens
cacheReadsPrice: 0.3, // $0.30 per million tokens
supportsReasoningBudget: true,
// Tiered pricing for extended context (requires beta flag 'context-1m-2025-08-07')
tiers: [
{
contextWindow: 1_000_000, // 1M tokens with beta flag
inputPrice: 6.0, // $6 per million input tokens (>200K context)
outputPrice: 22.5, // $22.50 per million output tokens (>200K context)
cacheWritesPrice: 7.5, // $7.50 per million tokens (>200K context)
cacheReadsPrice: 0.6, // $0.60 per million tokens (>200K context)
},
],
},
"claude-haiku-4-5@20251001": {
maxTokens: 8192,
contextWindow: 200_000,
@ -491,6 +512,7 @@ export const vertexModels = {
export const VERTEX_1M_CONTEXT_MODEL_IDS = [
"claude-sonnet-4@20250514",
"claude-sonnet-4-5@20250929",
"claude-sonnet-4-6@20260114",
"claude-opus-4-6",
] as const

View file

@ -899,6 +899,21 @@ describe("VertexHandler", () => {
expect(model.betas).toContain("context-1m-2025-08-07")
})
it("should enable 1M context for Claude Sonnet 4.6 when beta flag is set", () => {
const handler = new AnthropicVertexHandler({
apiModelId: "claude-sonnet-4-6@20260114",
vertexProjectId: "test-project",
vertexRegion: "us-central1",
vertex1MContext: true,
})
const model = handler.getModel()
expect(model.info.contextWindow).toBe(1_000_000)
expect(model.info.inputPrice).toBe(6.0)
expect(model.info.outputPrice).toBe(22.5)
expect(model.betas).toContain("context-1m-2025-08-07")
})
it("should not enable 1M context when flag is disabled", () => {
const handler = new AnthropicVertexHandler({
apiModelId: VERTEX_1M_CONTEXT_MODEL_IDS[0],

View file

@ -187,6 +187,28 @@ describe("AnthropicHandler", () => {
// Verify API
expect(mockCreate).toHaveBeenCalled()
})
it("should include 1M context beta header for Claude Sonnet 4.6 when enabled", async () => {
const sonnet46Handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-sonnet-4-6",
anthropicBeta1MContext: true,
})
const stream = sonnet46Handler.createMessage(systemPrompt, [
{
role: "user",
content: [{ type: "text" as const, text: "Hello" }],
},
])
for await (const _chunk of stream) {
// Consume stream
}
const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1]
expect(requestOptions?.headers?.["anthropic-beta"]).toContain("context-1m-2025-08-07")
})
})
describe("completePrompt", () => {
@ -286,6 +308,18 @@ describe("AnthropicHandler", () => {
expect(model.info.supportsReasoningBudget).toBe(true)
})
it("should handle Claude 4.6 Sonnet model correctly", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-sonnet-4-6",
})
const model = handler.getModel()
expect(model.id).toBe("claude-sonnet-4-6")
expect(model.info.maxTokens).toBe(64000)
expect(model.info.contextWindow).toBe(200000)
expect(model.info.supportsReasoningBudget).toBe(true)
})
it("should enable 1M context for Claude 4.5 Sonnet when beta flag is set", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
@ -297,6 +331,18 @@ describe("AnthropicHandler", () => {
expect(model.info.inputPrice).toBe(6.0)
expect(model.info.outputPrice).toBe(22.5)
})
it("should enable 1M context for Claude 4.6 Sonnet when beta flag is set", () => {
const handler = new AnthropicHandler({
apiKey: "test-api-key",
apiModelId: "claude-sonnet-4-6",
anthropicBeta1MContext: true,
})
const model = handler.getModel()
expect(model.info.contextWindow).toBe(1000000)
expect(model.info.inputPrice).toBe(6.0)
expect(model.info.outputPrice).toBe(22.5)
})
})
describe("reasoning block filtering", () => {

View file

@ -701,6 +701,21 @@ describe("AwsBedrockHandler", () => {
expect(model.info.contextWindow).toBe(1_000_000)
})
it("should apply 1M tier pricing when awsBedrock1MContext is true for Claude Sonnet 4.6", () => {
const handler = new AwsBedrockHandler({
apiModelId: "anthropic.claude-sonnet-4-6-20260114-v1:0",
awsAccessKey: "test",
awsSecretKey: "test",
awsRegion: "us-east-1",
awsBedrock1MContext: true,
})
const model = handler.getModel()
expect(model.info.contextWindow).toBe(1_000_000)
expect(model.info.inputPrice).toBe(6.0)
expect(model.info.outputPrice).toBe(22.5)
})
it("should use default context window when awsBedrock1MContext is false for Claude Sonnet 4", () => {
const handler = new AwsBedrockHandler({
apiModelId: BEDROCK_1M_CONTEXT_MODEL_IDS[0],

View file

@ -64,10 +64,11 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
// Filter out non-Anthropic blocks (reasoning, thoughtSignature, etc.) before sending to the API
const sanitizedMessages = filterNonAnthropicBlocks(messages)
// Add 1M context beta flag if enabled for supported models (Claude Sonnet 4/4.5, Opus 4.6)
// Add 1M context beta flag if enabled for supported models (Claude Sonnet 4/4.5/4.6, Opus 4.6)
if (
(modelId === "claude-sonnet-4-20250514" ||
modelId === "claude-sonnet-4-5" ||
modelId === "claude-sonnet-4-6" ||
modelId === "claude-opus-4-6") &&
this.options.anthropicBeta1MContext
) {
@ -80,6 +81,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
}
switch (modelId) {
case "claude-sonnet-4-6":
case "claude-sonnet-4-5":
case "claude-sonnet-4-20250514":
case "claude-opus-4-6":
@ -145,6 +147,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
// Then check for models that support prompt caching
switch (modelId) {
case "claude-sonnet-4-6":
case "claude-sonnet-4-5":
case "claude-sonnet-4-20250514":
case "claude-opus-4-6":
@ -336,7 +339,10 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa
// If 1M context beta is enabled for supported models, update the model info
if (
(id === "claude-sonnet-4-20250514" || id === "claude-sonnet-4-5" || id === "claude-opus-4-6") &&
(id === "claude-sonnet-4-20250514" ||
id === "claude-sonnet-4-5" ||
id === "claude-sonnet-4-6" ||
id === "claude-opus-4-6") &&
this.options.anthropicBeta1MContext
) {
// Use the tier pricing for 1M context

View file

@ -266,6 +266,30 @@ describe("OpenRouter API", () => {
})
describe("parseOpenRouterModel", () => {
it("sets claude-sonnet-4.6 model to Anthropic max tokens", () => {
const mockModel = {
name: "Claude Sonnet 4.6",
description: "Test model",
context_length: 200000,
max_completion_tokens: 8192,
pricing: {
prompt: "0.000003",
completion: "0.000015",
},
}
const result = parseOpenRouterModel({
id: "anthropic/claude-sonnet-4.6",
model: mockModel,
inputModality: ["text"],
outputModality: ["text"],
maxTokens: 8192,
})
expect(result.maxTokens).toBe(64000)
expect(result.contextWindow).toBe(200000)
})
it("sets horizon-alpha model to 32k max tokens", () => {
const mockModel = {
name: "Horizon Alpha",

View file

@ -243,6 +243,11 @@ export const parseOpenRouterModel = ({
modelInfo.maxTokens = anthropicModels["claude-3-7-sonnet-20250219:thinking"].maxTokens
}
// Set claude-sonnet-4.6 model to use the correct configuration
if (id === "anthropic/claude-sonnet-4.6") {
modelInfo.maxTokens = anthropicModels["claude-sonnet-4-6"].maxTokens
}
// Set claude-opus-4.1 model to use the correct configuration
if (id === "anthropic/claude-opus-4.1") {
modelInfo.maxTokens = anthropicModels["claude-opus-4-1-20250805"].maxTokens

View file

@ -26,6 +26,7 @@ export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: Anthro
const supports1MContextBeta =
selectedModel?.id === "claude-sonnet-4-20250514" ||
selectedModel?.id === "claude-sonnet-4-5" ||
selectedModel?.id === "claude-sonnet-4-6" ||
selectedModel?.id === "claude-opus-4-6"
const handleInputChange = useCallback(

View file

@ -402,6 +402,38 @@ describe("useSelectedModel", () => {
})
})
describe("anthropic provider with 1M context", () => {
beforeEach(() => {
mockUseRouterModels.mockReturnValue({
data: undefined,
isLoading: false,
isError: false,
} as any)
mockUseOpenRouterModelProviders.mockReturnValue({
data: undefined,
isLoading: false,
isError: false,
} as any)
})
it("should apply 1M pricing tier for Claude Sonnet 4.6 when enabled", () => {
const apiConfiguration: ProviderSettings = {
apiProvider: "anthropic",
apiModelId: "claude-sonnet-4-6",
anthropicBeta1MContext: true,
}
const wrapper = createWrapper()
const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper })
expect(result.current.id).toBe("claude-sonnet-4-6")
expect(result.current.info?.contextWindow).toBe(1_000_000)
expect(result.current.info?.inputPrice).toBe(6.0)
expect(result.current.info?.outputPrice).toBe(22.5)
})
})
describe("bedrock provider with 1M context", () => {
beforeEach(() => {
mockUseRouterModels.mockReturnValue({

View file

@ -341,11 +341,14 @@ function getSelectedModel({
// Apply 1M context beta tier pricing for supported Claude 4 models
if (
provider === "anthropic" &&
(id === "claude-sonnet-4-20250514" || id === "claude-sonnet-4-5" || id === "claude-opus-4-6") &&
(id === "claude-sonnet-4-20250514" ||
id === "claude-sonnet-4-5" ||
id === "claude-sonnet-4-6" ||
id === "claude-opus-4-6") &&
apiConfiguration.anthropicBeta1MContext &&
baseInfo
) {
// Type assertion since we know claude-sonnet-4-20250514 and claude-sonnet-4-5 have tiers
// Type assertion since supported Claude 4 models include 1M context pricing tiers.
const modelWithTiers = baseInfo as typeof baseInfo & {
tiers?: Array<{
contextWindow: number