diff --git a/src/api/providers/fetchers/__tests__/openrouter.spec.ts b/src/api/providers/fetchers/__tests__/openrouter.spec.ts index 37cdc54439..b39cf07bd4 100644 --- a/src/api/providers/fetchers/__tests__/openrouter.spec.ts +++ b/src/api/providers/fetchers/__tests__/openrouter.spec.ts @@ -29,6 +29,7 @@ describe("OpenRouter API", () => { supportsReasoningBudget: false, supportsReasoningEffort: false, supportedParameters: ["max_tokens", "temperature", "reasoning", "include_reasoning"], + preserveReasoning: true, }) expect(models["anthropic/claude-3.7-sonnet:thinking"]).toEqual({ @@ -45,6 +46,7 @@ describe("OpenRouter API", () => { requiredReasoningBudget: true, supportsReasoningEffort: true, supportedParameters: ["max_tokens", "temperature", "reasoning", "include_reasoning"], + preserveReasoning: true, }) expect(models["google/gemini-2.5-flash-preview-05-20"].maxTokens).toEqual(65535) @@ -97,6 +99,7 @@ describe("OpenRouter API", () => { description: undefined, supportsReasoningEffort: undefined, supportedParameters: undefined, + preserveReasoning: true, }, "google-ai-studio": { maxTokens: 65536, @@ -111,6 +114,7 @@ describe("OpenRouter API", () => { description: undefined, supportsReasoningEffort: undefined, supportedParameters: undefined, + preserveReasoning: true, }, }) @@ -236,5 +240,122 @@ describe("OpenRouter API", () => { expect(textResult.maxTokens).toBe(64000) expect(imageResult.maxTokens).toBe(64000) }) + + describe("preserveReasoning flag", () => { + it("should set preserveReasoning to true for models with reasoning support", () => { + const mockModel = { + name: "Test Reasoning Model", + context_length: 128000, + max_completion_tokens: 8192, + pricing: { + prompt: "0.001", + completion: "0.002", + }, + } + + const result = parseOpenRouterModel({ + id: "test/reasoning-model", + model: mockModel, + inputModality: ["text"], + outputModality: ["text"], + maxTokens: 8192, + supportedParameters: ["reasoning", "temperature"], + }) + + expect(result.preserveReasoning).toBe(true) + }) + + it("should set preserveReasoning to true for models with include_reasoning support", () => { + const mockModel = { + name: "Test Include Reasoning Model", + context_length: 128000, + max_completion_tokens: 8192, + pricing: { + prompt: "0.001", + completion: "0.002", + }, + } + + const result = parseOpenRouterModel({ + id: "test/include-reasoning-model", + model: mockModel, + inputModality: ["text"], + outputModality: ["text"], + maxTokens: 8192, + supportedParameters: ["include_reasoning", "temperature"], + }) + + expect(result.preserveReasoning).toBe(true) + }) + + it("should set preserveReasoning to false for models without reasoning support", () => { + const mockModel = { + name: "Test Non-Reasoning Model", + context_length: 128000, + max_completion_tokens: 8192, + pricing: { + prompt: "0.001", + completion: "0.002", + }, + } + + const result = parseOpenRouterModel({ + id: "test/non-reasoning-model", + model: mockModel, + inputModality: ["text"], + outputModality: ["text"], + maxTokens: 8192, + supportedParameters: ["temperature", "max_tokens"], + }) + + expect(result.preserveReasoning).toBe(false) + }) + + it("should set preserveReasoning to true for claude-3.7-sonnet:thinking", () => { + const mockModel = { + name: "Claude 3.7 Sonnet Thinking", + context_length: 200000, + max_completion_tokens: 8192, + pricing: { + prompt: "0.003", + completion: "0.015", + }, + } + + const result = parseOpenRouterModel({ + id: "anthropic/claude-3.7-sonnet:thinking", + model: mockModel, + inputModality: ["text"], + outputModality: ["text"], + maxTokens: 8192, + supportedParameters: ["reasoning"], + }) + + expect(result.preserveReasoning).toBe(true) + }) + + it("should set preserveReasoning to true for claude-haiku-4.5", () => { + const mockModel = { + name: "Claude Haiku 4.5", + context_length: 200000, + max_completion_tokens: 8192, + pricing: { + prompt: "0.001", + completion: "0.005", + }, + } + + const result = parseOpenRouterModel({ + id: "anthropic/claude-haiku-4.5", + model: mockModel, + inputModality: ["text"], + outputModality: ["text"], + maxTokens: 8192, + supportedParameters: ["reasoning"], + }) + + expect(result.preserveReasoning).toBe(true) + }) + }) }) }) diff --git a/src/api/providers/fetchers/openrouter.ts b/src/api/providers/fetchers/openrouter.ts index b546c40a3c..96de223a67 100644 --- a/src/api/providers/fetchers/openrouter.ts +++ b/src/api/providers/fetchers/openrouter.ts @@ -205,6 +205,11 @@ export const parseOpenRouterModel = ({ const supportsPromptCache = typeof cacheReadsPrice !== "undefined" // some models support caching but don't charge a cacheWritesPrice, e.g. GPT-5 + // Check if the model supports reasoning (either through include_reasoning or reasoning parameters) + const supportsReasoning = supportedParameters + ? supportedParameters.includes("reasoning") || supportedParameters.includes("include_reasoning") + : false + const modelInfo: ModelInfo = { maxTokens: maxTokens || Math.ceil(model.context_length * 0.2), contextWindow: model.context_length, @@ -217,6 +222,8 @@ export const parseOpenRouterModel = ({ description: model.description, supportsReasoningEffort: supportedParameters ? supportedParameters.includes("reasoning") : undefined, supportedParameters: supportedParameters ? supportedParameters.filter(isModelParameter) : undefined, + // Preserve reasoning in conversation history for models that support reasoning + preserveReasoning: supportsReasoning, } if (OPEN_ROUTER_REASONING_BUDGET_MODELS.has(id)) { @@ -239,6 +246,8 @@ export const parseOpenRouterModel = ({ if (id === "anthropic/claude-3.7-sonnet:thinking") { modelInfo.maxTokens = anthropicModels["claude-3-7-sonnet-20250219:thinking"].maxTokens + // Ensure reasoning is preserved for thinking models + modelInfo.preserveReasoning = true } // Set claude-opus-4.1 model to use the correct configuration @@ -251,6 +260,14 @@ export const parseOpenRouterModel = ({ if (id === "anthropic/claude-haiku-4.5") { modelInfo.supportsReasoningBudget = true modelInfo.supportsReasoningEffort = false + // Preserve reasoning for Claude Haiku 4.5 + modelInfo.preserveReasoning = true + } + + // Ensure reasoning preservation for models that support reasoning budget + // This includes models like MiniMax-M2 and other reasoning-capable models + if (modelInfo.supportsReasoningBudget || modelInfo.requiredReasoningBudget) { + modelInfo.preserveReasoning = true } // Set horizon-alpha model to 32k max tokens