From 039e59fa75f025d9c9e9ae3989d6261a5a7b3c09 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 22 Aug 2025 14:58:58 +0000 Subject: [PATCH] fix(openrouter): ensure GPT-5 reasoning effort passed and include_reasoning set for OpenRouter --- .../providers/__tests__/openrouter.spec.ts | 86 +++++++++++++++++++ src/api/providers/openrouter.ts | 13 ++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/api/providers/__tests__/openrouter.spec.ts b/src/api/providers/__tests__/openrouter.spec.ts index ae36fc1399..4634c11040 100644 --- a/src/api/providers/__tests__/openrouter.spec.ts +++ b/src/api/providers/__tests__/openrouter.spec.ts @@ -9,6 +9,7 @@ import OpenAI from "openai" import { OpenRouterHandler } from "../openrouter" import { ApiHandlerOptions } from "../../../shared/api" import { Package } from "../../../shared/package" +import { getModels } from "../fetchers/modelCache" // Mock dependencies vitest.mock("openai") @@ -44,6 +45,9 @@ vitest.mock("../fetchers/modelCache", () => ({ }) }), })) +vitest.mock("../fetchers/modelEndpointCache", () => ({ + getModelEndpoints: vitest.fn().mockResolvedValue({}), +})) describe("OpenRouterHandler", () => { const mockOptions: ApiHandlerOptions = { @@ -267,6 +271,88 @@ describe("OpenRouterHandler", () => { const generator = handler.createMessage("test", []) await expect(generator.next()).rejects.toThrow("OpenRouter API Error 500: API Error") }) + + it("passes reasoning effort and include_reasoning for GPT-5 models via OpenRouter", async () => { + ;(getModels as any).mockResolvedValueOnce({ + "openai/gpt-5-2025-08-07": { + maxTokens: 8192, + contextWindow: 128000, + supportsPromptCache: false, + supportsReasoningEffort: true, + description: "GPT-5 via OpenRouter", + }, + }) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + id: "openai/gpt-5-2025-08-07", + choices: [{ delta: { reasoning: "Thinking...", content: "Hello" } }], + usage: { prompt_tokens: 1, completion_tokens: 2, cost: 0.0 }, + } + }, + } + + const mockCreate = vitest.fn().mockResolvedValue(mockStream) + ;(OpenAI as any).prototype.chat = { completions: { create: mockCreate } } as any + + const handler = new OpenRouterHandler({ + openRouterApiKey: "test-key", + openRouterModelId: "openai/gpt-5-2025-08-07", + enableReasoningEffort: true, + reasoningEffort: "minimal" as any, + }) + + const gen = handler.createMessage("sys", [{ role: "user", content: "hi" } as any]) + for await (const _ of gen) { + // drain + } + + const call = (mockCreate as any).mock.calls[0][0] + expect(call.model).toBe("openai/gpt-5-2025-08-07") + expect(call.include_reasoning).toBe(true) + expect(call.reasoning).toEqual({ effort: "minimal" }) + }) + + it('defaults GPT-5 reasoning effort to "medium" when enabled but not specified', async () => { + ;(getModels as any).mockResolvedValueOnce({ + "openai/gpt-5-2025-08-07": { + maxTokens: 8192, + contextWindow: 128000, + supportsPromptCache: false, + supportsReasoningEffort: true, + description: "GPT-5 via OpenRouter", + }, + }) + + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + id: "openai/gpt-5-2025-08-07", + choices: [{ delta: { content: "Hi" } }], + usage: { prompt_tokens: 1, completion_tokens: 2, cost: 0.0 }, + } + }, + } + + const mockCreate = vitest.fn().mockResolvedValue(mockStream) + ;(OpenAI as any).prototype.chat = { completions: { create: mockCreate } } as any + + const handler = new OpenRouterHandler({ + openRouterApiKey: "test-key", + openRouterModelId: "openai/gpt-5-2025-08-07", + enableReasoningEffort: true, + }) + + const gen = handler.createMessage("sys", [{ role: "user", content: "hi" } as any]) + for await (const _ of gen) { + // drain + } + + const call = (mockCreate as any).mock.calls[0][0] + expect(call.include_reasoning).toBe(true) + expect(call.reasoning).toEqual({ effort: "medium" }) + }) }) describe("completePrompt", () => { diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 6565daa238..0dcfb3d4eb 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -121,6 +121,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH messages: openAiMessages, stream: true, stream_options: { include_usage: true }, + // For GPT-5 via OpenRouter, request reasoning content in the stream explicitly + ...(modelId.startsWith("openai/gpt-5") && { include_reasoning: true }), // Only include provider if openRouterSpecificProvider is not "[default]". ...(this.options.openRouterSpecificProvider && this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME && { @@ -208,7 +210,14 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH defaultTemperature: isDeepSeekR1 ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0, }) - return { id, info, topP: isDeepSeekR1 ? 0.95 : undefined, ...params } + // Apply GPT-5 defaults for OpenRouter: default reasoning effort to "medium" when enabled + let adjustedParams = params + if (id.startsWith("openai/gpt-5") && !params.reasoning && this.options.enableReasoningEffort !== false) { + const effort = (this.options.reasoningEffort as any) ?? "medium" + adjustedParams = { ...params, reasoning: { effort } as OpenRouterReasoningParams } + } + + return { id, info, topP: isDeepSeekR1 ? 0.95 : undefined, ...adjustedParams } } async completePrompt(prompt: string) { @@ -220,6 +229,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH temperature, messages: [{ role: "user", content: prompt }], stream: false, + // For GPT-5 via OpenRouter, request reasoning details explicitly as well + ...(modelId.startsWith("openai/gpt-5") && { include_reasoning: true }), // Only include provider if openRouterSpecificProvider is not "[default]". ...(this.options.openRouterSpecificProvider && this.options.openRouterSpecificProvider !== OPENROUTER_DEFAULT_PROVIDER_NAME && {