diff --git a/packages/types/src/providers/anthropic.ts b/packages/types/src/providers/anthropic.ts index 971b92211a..5c57970dbd 100644 --- a/packages/types/src/providers/anthropic.ts +++ b/packages/types/src/providers/anthropic.ts @@ -164,6 +164,23 @@ export const anthropicModels = { supportsTemperature: false, description: "Claude Opus 5 is Anthropic's most capable model for complex agentic coding and enterprise work.", }, + "claude-fable-5-1": { + maxTokens: 128_000, // Overridden to 8k if `enableReasoningEffort` is false. + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 10.0, // $10 per million input tokens + outputPrice: 50.0, // $50 per million output tokens + cacheWritesPrice: 12.5, // $12.50 per million tokens (5-minute cache) + cacheReadsPrice: 0.25, // $0.25 per million tokens (2.5% of base input price) + // Fable 5.1 only accepts adaptive thinking (or an omitted thinking field). + // Manual budget_tokens and non-default sampling parameters return a 400. + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + description: + "Claude Fable 5.1 extends Fable 5 with stronger long-running agentic coding, multistep research, and document work.", + }, "claude-fable-5": { maxTokens: 128_000, // Overridden to 8k if `enableReasoningEffort` is false. contextWindow: 1_000_000, diff --git a/packages/types/src/providers/bedrock.ts b/packages/types/src/providers/bedrock.ts index 78fea48845..7f5d6b214b 100644 --- a/packages/types/src/providers/bedrock.ts +++ b/packages/types/src/providers/bedrock.ts @@ -252,6 +252,24 @@ export const bedrockModels = { cachableFields: ["system", "messages", "tools"], description: "Claude Opus 5 is Anthropic's most capable model for complex agentic coding and enterprise work.", }, + "anthropic.claude-fable-5-1": { + maxTokens: 128_000, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + inputPrice: 10.0, + outputPrice: 50.0, + cacheWritesPrice: 12.5, + cacheReadsPrice: 0.25, + minTokensPerCachePoint: 512, + maxCachePoints: 4, + cachableFields: ["system", "messages", "tools"], + description: + "Claude Fable 5.1 extends Fable 5 with stronger long-running agentic coding, multistep research, and document work.", + }, "anthropic.claude-fable-5": { maxTokens: 8192, contextWindow: 1_000_000, @@ -643,7 +661,7 @@ export const BEDROCK_1M_CONTEXT_MODEL_IDS = [ // - Claude Opus 4.6 // - Claude Opus 4.7 // - Claude Opus 5 -// - Claude Fable 5 (cross-region inference only — can only be used through an inference profile) +// - Claude Fable 5 and 5.1 (cross-region inference only — can only be used through an inference profile) export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [ "anthropic.claude-sonnet-4-20250514-v1:0", "anthropic.claude-sonnet-4-5-20250929-v1:0", @@ -655,6 +673,7 @@ export const BEDROCK_GLOBAL_INFERENCE_MODEL_IDS = [ "anthropic.claude-opus-4-7", "anthropic.claude-opus-4-8", "anthropic.claude-opus-5", + "anthropic.claude-fable-5-1", "anthropic.claude-fable-5", ] as const diff --git a/packages/types/src/providers/openrouter.ts b/packages/types/src/providers/openrouter.ts index 9f146a304b..058881c5e3 100644 --- a/packages/types/src/providers/openrouter.ts +++ b/packages/types/src/providers/openrouter.ts @@ -45,6 +45,7 @@ export const OPEN_ROUTER_PROMPT_CACHING_MODELS = new Set([ "anthropic/claude-opus-4.5", "anthropic/claude-opus-4.6", "anthropic/claude-opus-5", + "anthropic/claude-fable-5.1", "anthropic/claude-fable-5", "anthropic/claude-haiku-4.5", "google/gemini-2.5-flash-preview", @@ -78,6 +79,7 @@ export const OPEN_ROUTER_REASONING_BUDGET_MODELS = new Set([ "anthropic/claude-opus-4.5", "anthropic/claude-opus-4.6", "anthropic/claude-opus-5", + "anthropic/claude-fable-5.1", "anthropic/claude-fable-5", "anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.5", diff --git a/packages/types/src/providers/vercel-ai-gateway.ts b/packages/types/src/providers/vercel-ai-gateway.ts index f44df88656..21994e0749 100644 --- a/packages/types/src/providers/vercel-ai-gateway.ts +++ b/packages/types/src/providers/vercel-ai-gateway.ts @@ -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-opus-5", + "anthropic/claude-fable-5.1", "anthropic/claude-fable-5", "anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.6", @@ -60,6 +61,7 @@ export const VERCEL_AI_GATEWAY_VISION_AND_TOOLS_MODELS = new Set([ "anthropic/claude-opus-4.5", "anthropic/claude-opus-4.6", "anthropic/claude-opus-5", + "anthropic/claude-fable-5.1", "anthropic/claude-fable-5", "anthropic/claude-sonnet-4", "anthropic/claude-sonnet-4.5", diff --git a/packages/types/src/providers/vertex.ts b/packages/types/src/providers/vertex.ts index 1509f40469..645631c6ee 100644 --- a/packages/types/src/providers/vertex.ts +++ b/packages/types/src/providers/vertex.ts @@ -489,6 +489,21 @@ export const vertexModels = { supportsTemperature: false, description: "Claude Opus 5 is Anthropic's most capable model for complex agentic coding and enterprise work.", }, + "claude-fable-5-1": { + maxTokens: 128_000, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 10.0, + outputPrice: 50.0, + cacheWritesPrice: 12.5, + cacheReadsPrice: 0.25, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + description: + "Claude Fable 5.1 extends Fable 5 with stronger long-running agentic coding, multistep research, and document work.", + }, "claude-fable-5": { maxTokens: 8192, contextWindow: 1_000_000, diff --git a/src/api/providers/__tests__/anthropic-vertex.spec.ts b/src/api/providers/__tests__/anthropic-vertex.spec.ts index 3e98f3ec5b..10824c44e2 100644 --- a/src/api/providers/__tests__/anthropic-vertex.spec.ts +++ b/src/api/providers/__tests__/anthropic-vertex.spec.ts @@ -992,6 +992,24 @@ describe("VertexHandler", () => { expect(model.info.supportsTemperature).toBe(false) }) + it("should return Claude Fable 5.1 model info", () => { + const handler = new AnthropicVertexHandler({ + apiModelId: "claude-fable-5-1", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + + const model = handler.getModel() + expect(model.id).toBe("claude-fable-5-1") + expect(model.info.maxTokens).toBe(128_000) + expect(model.info.contextWindow).toBe(1_000_000) + expect(model.info.cacheReadsPrice).toBe(0.25) + expect(model.info.supportsReasoningBinary).toBe(true) + expect(model.info.supportsReasoningBudget).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.supportsTemperature).toBe(false) + }) + it("should return Claude Sonnet 5 model info", () => { const handler = new AnthropicVertexHandler({ apiModelId: "claude-sonnet-5", @@ -1266,6 +1284,37 @@ describe("VertexHandler", () => { enableReasoningEffort: true, }) + const mockCreate = vitest + .fn() + .mockImplementation(async () => + asyncStreamFrom([ + { type: "message_start", message: { usage: { input_tokens: 10, output_tokens: 5 } } }, + ]), + ) + fableHandler["client"].messages.create = mockCreate + + await fableHandler.createMessage("You are a helpful assistant", [{ role: "user", content: "Hello" }]).next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + thinking: { type: "adaptive" }, + }), + undefined, + ) + + const request = mockCreate.mock.calls[0][0] + expect(request.thinking).not.toHaveProperty("budget_tokens") + expect(request.temperature).toBeUndefined() + }) + + it("should use adaptive thinking for Claude Fable 5.1", async () => { + const fableHandler = new AnthropicVertexHandler({ + apiModelId: "claude-fable-5-1", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + enableReasoningEffort: true, + }) + const mockCreate = vitest .fn() .mockImplementation(async () => diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index 21d2816ec7..7d54116a38 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -406,6 +406,31 @@ describe("AnthropicHandler", () => { expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31") }) + it("should use adaptive thinking for Claude Fable 5.1 when reasoning is enabled", async () => { + const fableHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-fable-5-1", + enableReasoningEffort: true, + modelMaxTokens: 32768, + }) + + const stream = fableHandler.createMessage(systemPrompt, [ + { + role: "user", + content: [{ type: "text" as const, text: "Hello" }], + }, + ]) + + await collectStream(stream) + + const requestBody = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[0] + const requestOptions = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]?.[1] + expect(requestBody?.thinking).toEqual({ type: "adaptive" }) + expect(requestBody?.temperature).toBeUndefined() + expect(requestBody?.max_tokens).toBe(32768) + expect(requestOptions?.headers?.["anthropic-beta"]).toContain("prompt-caching-2024-07-31") + }) + it("should use adaptive thinking for Claude Sonnet 5 when reasoning is enabled", async () => { const sonnetHandler = new AnthropicHandler({ apiKey: "test-api-key", @@ -637,6 +662,27 @@ describe("AnthropicHandler", () => { expect(model.reasoningBudget).toBeUndefined() }) + it("should handle Claude Fable 5.1 model correctly", () => { + const handler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-fable-5-1", + }) + const model = handler.getModel() + expect(model.id).toBe("claude-fable-5-1") + expect(model.info.maxTokens).toBe(128000) + expect(model.info.contextWindow).toBe(1000000) + expect(model.maxTokens).toBe(8192) + expect(model.info.inputPrice).toBe(10) + expect(model.info.outputPrice).toBe(50) + expect(model.info.cacheWritesPrice).toBe(12.5) + expect(model.info.cacheReadsPrice).toBe(0.25) + expect(model.info.supportsReasoningBinary).toBe(true) + expect(model.info.supportsReasoningBudget).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.supportsTemperature).toBe(false) + expect(model.reasoningBudget).toBeUndefined() + }) + it("should handle Claude Sonnet 5 model correctly", () => { const handler = new AnthropicHandler({ apiKey: "test-api-key", @@ -952,7 +998,7 @@ describe("AnthropicHandler", () => { ) }) - it("should set tool_choice to undefined when tool_choice is 'none' (tools are still passed)", async () => { + it("should set tool_choice to 'none' when tool_choice is 'none' (tools are still passed)", async () => { // Handler uses native protocol by default const stream = handler.createMessage(systemPrompt, messages, { taskId: "test-task", @@ -964,12 +1010,21 @@ describe("AnthropicHandler", () => { await collectStream(stream) // Tools are now always present (minimum 6 from ALWAYS_AVAILABLE_TOOLS) - // When tool_choice is 'none', the converter returns undefined for tool_choice - // but tools are still passed since they're always present + // Explicitly disable tool use while preserving the available tool definitions. expect(mockCreate).toHaveBeenCalledWith( expect.objectContaining({ - tools: expect.any(Array), - tool_choice: undefined, + tools: [ + { + name: "get_weather", + description: "Get the current weather", + input_schema: { + type: "object", + properties: { location: { type: "string" } }, + required: ["location"], + }, + }, + ], + tool_choice: { type: "none" }, }), expect.anything(), ) @@ -994,6 +1049,133 @@ describe("AnthropicHandler", () => { ) }) + it.each([ + ["required with default parallel calls", "required" as const, undefined, false], + [ + "named with default parallel calls", + { type: "function" as const, function: { name: "get_weather" } }, + undefined, + false, + ], + ["required with parallel calls", "required" as const, true, false], + [ + "named with parallel calls", + { type: "function" as const, function: { name: "get_weather" } }, + true, + false, + ], + ["required without parallel calls", "required" as const, false, true], + [ + "named without parallel calls", + { type: "function" as const, function: { name: "get_weather" } }, + false, + true, + ], + ])( + "should normalize %s tool_choice to auto for Claude Fable 5.1", + async (_, toolChoice, parallelToolCalls, disableParallelToolUse) => { + const fableHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-fable-5-1", + }) + const stream = fableHandler.createMessage(systemPrompt, messages, { + taskId: "test-task", + tools: mockTools, + tool_choice: toolChoice, + parallelToolCalls, + }) + + await collectStream(stream) + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + tool_choice: { type: "auto", disable_parallel_tool_use: disableParallelToolUse }, + }), + expect.anything(), + ) + }, + ) + + it("should preserve an omitted tool_choice for Claude Fable 5.1", async () => { + const fableHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-fable-5-1", + }) + const stream = fableHandler.createMessage(systemPrompt, messages, { + taskId: "test-task", + tools: mockTools, + }) + + await collectStream(stream) + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ tool_choice: undefined }), + expect.anything(), + ) + }) + + it("should disable parallel tool calls when tool_choice is omitted for Claude Fable 5.1", async () => { + const fableHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-fable-5-1", + }) + const stream = fableHandler.createMessage(systemPrompt, messages, { + taskId: "test-task", + tools: mockTools, + parallelToolCalls: false, + }) + + await collectStream(stream) + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + tool_choice: { type: "auto", disable_parallel_tool_use: true }, + }), + expect.anything(), + ) + }) + + it("should preserve an explicit none tool_choice for Claude Fable 5.1", async () => { + const fableHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-fable-5-1", + }) + const stream = fableHandler.createMessage(systemPrompt, messages, { + taskId: "test-task", + tools: mockTools, + tool_choice: "none", + }) + + await collectStream(stream) + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ tool_choice: { type: "none" } }), + expect.anything(), + ) + }) + + it.each([ + ["required", "required" as const, { type: "any", disable_parallel_tool_use: false }], + [ + "named", + { type: "function" as const, function: { name: "get_weather" } }, + { type: "tool", name: "get_weather", disable_parallel_tool_use: false }, + ], + ])("should preserve %s tool_choice for non-Fable models", async (_, toolChoice, expectedToolChoice) => { + const stream = handler.createMessage(systemPrompt, messages, { + taskId: "test-task", + tools: mockTools, + tool_choice: toolChoice, + }) + + await collectStream(stream) + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ tool_choice: expectedToolChoice }), + expect.anything(), + ) + }) + it("should enable parallel tool calls when parallelToolCalls is true", async () => { // Handler uses native protocol by default const stream = handler.createMessage(systemPrompt, messages, { diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index 7733aa1031..917bccfa00 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -829,6 +829,40 @@ describe("AwsBedrockHandler", () => { expect(model.maxTokens).toBe(8192) }) + it("should return Claude Fable 5.1 model info", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-fable-5-1", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const model = handler.getModel() + expect(model.id).toBe("anthropic.claude-fable-5-1") + expect(model.info.maxTokens).toBe(128_000) + expect(model.info.contextWindow).toBe(1_000_000) + expect(model.info.cacheReadsPrice).toBe(0.25) + expect(model.info.minTokensPerCachePoint).toBe(512) + expect(model.info.supportsReasoningBinary).toBe(true) + expect(model.info.supportsReasoningBudget).toBe(true) + expect(model.info.supportsPromptCache).toBe(true) + expect(model.info.supportsTemperature).toBe(false) + expect(model.maxTokens).toBe(8192) + }) + + it("should apply global inference prefix for Claude Fable 5.1 when awsUseGlobalInference is true", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "anthropic.claude-fable-5-1", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + awsUseGlobalInference: true, + }) + + const model = handler.getModel() + expect(model.id).toBe("global.anthropic.claude-fable-5-1") + }) + it("should apply global inference prefix for Claude Fable 5 when awsUseGlobalInference is true", () => { const handler = new AwsBedrockHandler({ apiModelId: "anthropic.claude-fable-5", diff --git a/src/api/providers/__tests__/requesty.spec.ts b/src/api/providers/__tests__/requesty.spec.ts index 57feb39636..f256e5b1c1 100644 --- a/src/api/providers/__tests__/requesty.spec.ts +++ b/src/api/providers/__tests__/requesty.spec.ts @@ -66,6 +66,20 @@ vitest.mock("../fetchers/modelCache", () => ({ cacheReadsPrice: 1, description: "Claude Fable 5", }, + "anthropic/claude-fable-5.1": { + maxTokens: 128000, + contextWindow: 1000000, + supportsImages: true, + supportsPromptCache: true, + supportsReasoningBudget: true, + supportsReasoningBinary: true, + supportsTemperature: false, + inputPrice: 10, + outputPrice: 50, + cacheWritesPrice: 12.5, + cacheReadsPrice: 0.25, + description: "Claude Fable 5.1", + }, "anthropic/claude-sonnet-5": { maxTokens: 128000, contextWindow: 1000000, @@ -281,6 +295,39 @@ describe("RequestyHandler", () => { ) }) + it("uses adaptive thinking for Claude Fable 5.1 when reasoning is enabled", async () => { + const handler = new RequestyHandler( + makeApiHandlerOptions({ + requestyApiKey: "test-key", + requestyModelId: "anthropic/claude-fable-5.1", + enableReasoningEffort: true, + modelMaxTokens: 32768, + }), + ) + + mockCreate.mockResolvedValue( + asyncStreamFrom([ + { + id: "test-id", + choices: [{ delta: {} }], + usage: { prompt_tokens: 10, completion_tokens: 20 }, + }, + ]), + ) + + const generator = handler.createMessage("test system prompt", [{ role: "user" as const, content: "test" }]) + await generator.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "anthropic/claude-fable-5.1", + max_tokens: 32768, + thinking: { type: "adaptive" }, + temperature: undefined, + }), + ) + }) + it("uses adaptive thinking for Claude Sonnet 5 when reasoning is enabled", async () => { const handler = new RequestyHandler( makeApiHandlerOptions({ diff --git a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts index b238d72381..5374879845 100644 --- a/src/api/providers/__tests__/vercel-ai-gateway.spec.ts +++ b/src/api/providers/__tests__/vercel-ai-gateway.spec.ts @@ -51,6 +51,18 @@ vitest.mock("../fetchers/modelCache", () => ({ cacheReadsPrice: 1, description: "Claude Fable 5", }, + "anthropic/claude-fable-5.1": { + maxTokens: 128000, + contextWindow: 1000000, + supportsImages: true, + supportsPromptCache: true, + supportsTemperature: false, + inputPrice: 10, + outputPrice: 50, + cacheWritesPrice: 12.5, + cacheReadsPrice: 0.25, + description: "Claude Fable 5.1", + }, "anthropic/claude-sonnet-5": { maxTokens: 128000, contextWindow: 1000000, @@ -328,6 +340,24 @@ describe("VercelAiGatewayHandler", () => { ) }) + it("omits temperature for Claude Fable 5.1", async () => { + const handler = new VercelAiGatewayHandler( + makeApiHandlerOptions({ + ...mockOptions, + vercelAiGatewayModelId: "anthropic/claude-fable-5.1", + }), + ) + + await collectStream(handler.createMessage("system prompt", [{ role: "user", content: "test" }])) + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "anthropic/claude-fable-5.1", + temperature: undefined, + }), + ) + }) + it("omits temperature for Claude Sonnet 5", async () => { const handler = new VercelAiGatewayHandler( makeApiHandlerOptions({ diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index b55c8b3089..2ef70b78ea 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -93,9 +93,24 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa betas.push("context-1m-2025-08-07") } + const convertedToolChoice = convertOpenAIToolChoiceToAnthropic( + metadata?.tool_choice, + metadata?.parallelToolCalls, + ) + let toolChoice = convertedToolChoice + if (modelId === "claude-fable-5-1") { + if (metadata?.tool_choice === undefined && metadata?.parallelToolCalls !== false) { + toolChoice = undefined + } else if (metadata.tool_choice === "required" || typeof metadata.tool_choice === "object") { + toolChoice = { + type: "auto" as const, + disable_parallel_tool_use: metadata.parallelToolCalls === false, + } + } + } const nativeToolParams = { tools: convertOpenAIToolsToAnthropic(metadata?.tools ?? []), - tool_choice: convertOpenAIToolChoiceToAnthropic(metadata?.tool_choice, metadata?.parallelToolCalls), + tool_choice: toolChoice, } switch (modelId) { @@ -107,6 +122,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa case "claude-opus-4-7": case "claude-opus-4-8": case "claude-opus-5": + case "claude-fable-5-1": case "claude-fable-5": case "claude-opus-4-5-20251101": case "claude-opus-4-1-20250805": @@ -179,6 +195,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa case "claude-opus-4-7": case "claude-opus-4-8": case "claude-opus-5": + case "claude-fable-5-1": case "claude-fable-5": case "claude-opus-4-5-20251101": case "claude-opus-4-1-20250805": diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 27730aadaf..9c42005545 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -326,11 +326,11 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH * Detect models that require the adaptive-thinking API contract. * * Starting with Claude Opus 4.7 (and the matching Sonnet 4.7), and continuing - * in Opus 4.8 / Sonnet 4.8, Claude Fable 5, Claude Sonnet 5, and Claude Opus 5, + * in Opus 4.8 / Sonnet 4.8, Claude Fable 5/5.1, Claude Sonnet 5, and Claude Opus 5, * Anthropic removed sampling parameters (temperature/top_p/top_k) and replaced * budget_tokens-based thinking with `thinking.type: "adaptive"` plus * `output_config.effort`. The migration guide from 4.7 → 4.8 confirms there - * are no further breaking API changes, and Fable 5 / Sonnet 5 / Opus 5 keep the + * are no further breaking API changes, and Fable 5+ / Sonnet 5 / Opus 5 keep the * same adaptive-thinking contract, so a single guard matches all generations. * Shared by createMessage and completePrompt so both request paths omit * temperature for these models (sending it causes a 400). diff --git a/src/api/providers/fetchers/__tests__/openrouter.spec.ts b/src/api/providers/fetchers/__tests__/openrouter.spec.ts index 89169c9c56..dafb7a9a0f 100644 --- a/src/api/providers/fetchers/__tests__/openrouter.spec.ts +++ b/src/api/providers/fetchers/__tests__/openrouter.spec.ts @@ -318,6 +318,35 @@ describe("OpenRouter API", () => { expect(result.supportsReasoningBinary).toBe(true) }) + it("sets claude-fable-5.1 model to Anthropic max tokens and omits temperature", () => { + const result = parseOpenRouterModel({ + id: "anthropic/claude-fable-5.1", + model: { + name: "Claude Fable 5.1", + description: "Test model", + context_length: 1000000, + max_completion_tokens: 128000, + pricing: { + prompt: "0.00001", + completion: "0.00005", + input_cache_read: "0.00000025", + input_cache_write: "0.0000125", + }, + }, + inputModality: ["text", "image"], + outputModality: ["text"], + maxTokens: 128000, + supportedParameters: ["reasoning", "reasoning_effort", "include_reasoning"], + }) + + expect(result.maxTokens).toBe(128000) + expect(result.contextWindow).toBe(1000000) + expect(result.cacheReadsPrice).toBe(0.25) + expect(result.supportsTemperature).toBe(false) + expect(result.supportsReasoningBudget).toBe(true) + expect(result.supportsReasoningBinary).toBe(true) + }) + it("sets claude-sonnet-5 model to Anthropic max tokens and omits temperature", () => { const mockModel = { name: "Claude Sonnet 5", diff --git a/src/api/providers/fetchers/__tests__/requesty.spec.ts b/src/api/providers/fetchers/__tests__/requesty.spec.ts index 25d622b6ed..53891273a9 100644 --- a/src/api/providers/fetchers/__tests__/requesty.spec.ts +++ b/src/api/providers/fetchers/__tests__/requesty.spec.ts @@ -25,6 +25,32 @@ function makeRawModel(overrides: Record) { } describe("getRequestyModels", () => { + it("applies Fable 5.1 overrides when parsing anthropic/claude-fable-5.1", async () => { + const rawFable51 = makeRawModel({ + id: "anthropic/claude-fable-5.1", + max_output_tokens: 128000, + context_window: 1000000, + supports_caching: true, + supports_vision: true, + supports_reasoning: true, + input_price: "0.00001", + output_price: "0.00005", + caching_price: "0.0000125", + cached_price: "0.00000025", + }) + + mockAxiosGet.mockResolvedValueOnce({ data: { data: [rawFable51] } }) + + const models = await getRequestyModels() + const fable51 = models["anthropic/claude-fable-5.1"] + + expect(fable51).toBeDefined() + expect(fable51.cacheReadsPrice).toBe(0.25) + expect(fable51.supportsReasoningBudget).toBe(true) + expect(fable51.supportsReasoningBinary).toBe(true) + expect(fable51.supportsTemperature).toBe(false) + }) + it("applies Fable 5 overrides when parsing anthropic/claude-fable-5", async () => { const rawFable5 = makeRawModel({ id: "anthropic/claude-fable-5", diff --git a/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts b/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts index fa8e89e8d0..fb90cde4a6 100644 --- a/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts +++ b/src/api/providers/fetchers/__tests__/vercel-ai-gateway.spec.ts @@ -221,6 +221,29 @@ describe("Vercel AI Gateway Fetchers", () => { expect(result.supportsTemperature).toBe(false) }) + it("marks Claude Fable 5.1 as not supporting temperature and parses its cache pricing", () => { + const result = parseVercelAiGatewayModel({ + id: "anthropic/claude-fable-5.1", + model: { + ...baseModel, + id: "anthropic/claude-fable-5.1", + context_window: 1000000, + max_tokens: 128000, + pricing: { + input: "0.00001", + output: "0.00005", + input_cache_write: "0.0000125", + input_cache_read: "0.00000025", + }, + }, + }) + + expect(result.maxTokens).toBe(128000) + expect(result.contextWindow).toBe(1000000) + expect(result.cacheReadsPrice).toBe(0.25) + expect(result.supportsTemperature).toBe(false) + }) + it("marks Claude Sonnet 5 as not supporting temperature", () => { const result = parseVercelAiGatewayModel({ id: "anthropic/claude-sonnet-5", diff --git a/src/api/providers/fetchers/openrouter.ts b/src/api/providers/fetchers/openrouter.ts index a25c1b96fa..6b281bc27a 100644 --- a/src/api/providers/fetchers/openrouter.ts +++ b/src/api/providers/fetchers/openrouter.ts @@ -263,6 +263,15 @@ export const parseOpenRouterModel = ({ modelInfo.maxTokens = anthropicModels["claude-opus-4-6"].maxTokens } + // Set claude-fable-5.1 model to use the correct Anthropic configuration. + // OpenRouter uses a dotted version suffix, unlike Anthropic's direct API. + if (id === "anthropic/claude-fable-5.1") { + modelInfo.maxTokens = anthropicModels["claude-fable-5-1"].maxTokens + modelInfo.supportsReasoningBudget = true + modelInfo.supportsReasoningBinary = true + modelInfo.supportsTemperature = false + } + // Set claude-fable-5 model to use the correct Anthropic configuration if (id === "anthropic/claude-fable-5") { modelInfo.maxTokens = anthropicModels["claude-fable-5"].maxTokens diff --git a/src/api/providers/fetchers/requesty.ts b/src/api/providers/fetchers/requesty.ts index 861fbfb181..d4dd91388d 100644 --- a/src/api/providers/fetchers/requesty.ts +++ b/src/api/providers/fetchers/requesty.ts @@ -45,7 +45,7 @@ export async function getRequestyModels(baseUrl?: string, apiKey?: string): Prom cacheReadsPrice: parseApiPrice(rawModel.cached_price), } - if (rawModel.id === "anthropic/claude-fable-5") { + if (rawModel.id === "anthropic/claude-fable-5.1" || rawModel.id === "anthropic/claude-fable-5") { modelInfo.supportsReasoningBudget = true modelInfo.supportsReasoningBinary = true modelInfo.supportsTemperature = false diff --git a/src/api/providers/fetchers/vercel-ai-gateway.ts b/src/api/providers/fetchers/vercel-ai-gateway.ts index 41865ebf44..1d666ccf09 100644 --- a/src/api/providers/fetchers/vercel-ai-gateway.ts +++ b/src/api/providers/fetchers/vercel-ai-gateway.ts @@ -116,7 +116,7 @@ export const parseVercelAiGatewayModel = ({ id, model }: { id: string; model: Ve description: model.description ?? model.name, } - if (id === "anthropic/claude-fable-5") { + if (id === "anthropic/claude-fable-5.1" || id === "anthropic/claude-fable-5") { modelInfo.supportsTemperature = false } diff --git a/src/core/prompts/tools/native-tools/__tests__/converters.spec.ts b/src/core/prompts/tools/native-tools/__tests__/converters.spec.ts index dfef164659..c0c7a4f08f 100644 --- a/src/core/prompts/tools/native-tools/__tests__/converters.spec.ts +++ b/src/core/prompts/tools/native-tools/__tests__/converters.spec.ts @@ -157,9 +157,9 @@ describe("converters", () => { expect(result).toEqual({ type: "auto", disable_parallel_tool_use: true }) }) - it("should return undefined for 'none' tool choice", () => { + it("should return none for 'none' tool choice", () => { const result = convertOpenAIToolChoiceToAnthropic("none") - expect(result).toBeUndefined() + expect(result).toEqual({ type: "none" }) }) it("should return auto for 'auto' tool choice", () => { diff --git a/src/core/prompts/tools/native-tools/converters.ts b/src/core/prompts/tools/native-tools/converters.ts index 2496c81c80..f408b43419 100644 --- a/src/core/prompts/tools/native-tools/converters.ts +++ b/src/core/prompts/tools/native-tools/converters.ts @@ -52,7 +52,7 @@ export function convertOpenAIToolsToAnthropic(tools: OpenAI.Chat.ChatCompletionT * Converts OpenAI tool_choice to Anthropic ToolChoice format. * * Maps OpenAI's tool_choice parameter to Anthropic's equivalent format: - * - "none" → undefined (Anthropic doesn't have "none", just omit tools) + * - "none" → { type: "none" } * - "auto" → { type: "auto" } * - "required" → { type: "any" } * - { type: "function", function: { name } } → { type: "tool", name } @@ -86,7 +86,7 @@ export function convertOpenAIToolChoiceToAnthropic( if (typeof toolChoice === "string") { switch (toolChoice) { case "none": - return undefined // Anthropic doesn't have "none", just omit tools + return { type: "none" } case "auto": return { type: "auto", disable_parallel_tool_use: disableParallelToolUse } case "required":