diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index ae36fc1399..7a0f664b25 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -321,3 +321,71 @@ describe("OpenRouterHandler", () => { }) }) }) + +describe("reasoning effort mapping (OpenRouter)", () => { + it("passes 'minimal' through in reasoning.effort for OpenRouter requests", async () => { + const handler = new OpenRouterHandler({ + openRouterApiKey: "test-key", + openRouterModelId: "openai/o1-pro", + reasoningEffort: "minimal", + } as ApiHandlerOptions) + + // Prepare a model that supports reasoning effort (not budget) + ;(handler as any).models = { + "openai/o1-pro": { + maxTokens: 8192, + contextWindow: 200000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.0, + outputPrice: 0.0, + description: "o1-pro test", + supportsReasoningEffort: true, + }, + } + + // Ensure endpoints map is empty so base model info is used + ;(handler as any).endpoints = {} + + // Mock OpenAI client call + const mockCreate = vitest.fn().mockResolvedValue({ + async *[Symbol.asyncIterator]() { + yield { + id: "openai/o1-pro", + choices: [{ delta: { content: "ok" } }], + } + yield { + id: "usage-id", + choices: [{ delta: {} }], + usage: { prompt_tokens: 1, completion_tokens: 1, cost: 0 }, + } + }, + }) + ;(OpenAI as any).prototype.chat = { + completions: { create: mockCreate }, + } as any + + const systemPrompt = "system" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "hello" }] + + // Stub fetchModel to use the handler's getModel (which applies getModelParams -> getOpenRouterReasoning) + const realGetModel = (handler as any).getModel.bind(handler) + ;(handler as any).fetchModel = vitest.fn().mockImplementation(async () => realGetModel()) + + // Trigger a request + const gen = handler.createMessage(systemPrompt, messages) + // Drain iterator to ensure call is made + for await (const _ of gen) { + // noop + } + + // Verify the API call included the normalized effort + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: "openai/o1-pro", + reasoning: { effort: "minimal" }, // 'minimal' should be preserved for OpenRouter + stream: true, + }), + ) + }) +}) diff --git a/src/api/transform/reasoning.ts b/src/api/transform/reasoning.ts index 100b1c2684..fff288e3f0 100644 --- a/src/api/transform/reasoning.ts +++ b/src/api/transform/reasoning.ts @@ -30,14 +30,21 @@ export const getOpenRouterReasoning = ({ reasoningBudget, reasoningEffort, settings, -}: GetModelReasoningOptions): OpenRouterReasoningParams | undefined => - shouldUseReasoningBudget({ model, settings }) - ? { max_tokens: reasoningBudget } - : shouldUseReasoningEffort({ model, settings }) - ? reasoningEffort - ? { effort: reasoningEffort } - : undefined - : undefined +}: GetModelReasoningOptions): OpenRouterReasoningParams | undefined => { + // If the model uses a budget-style reasoning config on OpenRouter, pass it through. + if (shouldUseReasoningBudget({ model, settings })) { + return { max_tokens: reasoningBudget! } + } + + // Otherwise, if we support traditional reasoning effort, pass through the effort. + // Note: Some models (e.g., GPT‑5 via OpenRouter) may support "minimal". + if (shouldUseReasoningEffort({ model, settings })) { + if (!reasoningEffort) return undefined + return { effort: reasoningEffort } + } + + return undefined +} export const getAnthropicReasoning = ({ model,