From 19bd598a780148b4125052ebf897171a70b953ef Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 28 Jan 2026 01:03:42 +0000 Subject: [PATCH] fix: add thinking parameter for OpenAI-compatible APIs when supportsReasoningBinary is true Fixes #11001 When using OpenAI-compatible APIs like volcengine Ark API with reasoning effort enabled, the API requires both: 1. reasoning_effort parameter (e.g., "medium") 2. thinking parameter set to { type: "enabled" } This change adds the thinking parameter when: - Reasoning effort is being used (reasoning object is present), AND - The model's supportsReasoningBinary flag is true The fix is applied to both: - createMessage method (for regular models with streaming) - handleO3FamilyMessage method (for O3 family models) Users can enable this by setting supportsReasoningBinary: true in their custom model info configuration. --- src/api/providers/__tests__/openai.spec.ts | 145 +++++++++++++++++++++ src/api/providers/openai.ts | 10 ++ 2 files changed, 155 insertions(+) diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index d95860d5739..11ce39e6fdd 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -374,6 +374,54 @@ describe("OpenAiHandler", () => { expect(callArgs.reasoning_effort).toBe("high") }) + it("should include thinking parameter when supportsReasoningBinary is true and reasoning effort is enabled", async () => { + const reasoningBinaryOptions: ApiHandlerOptions = { + ...mockOptions, + enableReasoningEffort: true, + openAiCustomModelInfo: { + contextWindow: 128_000, + supportsPromptCache: false, + supportsReasoningEffort: true, + supportsReasoningBinary: true, + reasoningEffort: "medium", + }, + } + const reasoningBinaryHandler = new OpenAiHandler(reasoningBinaryOptions) + const stream = reasoningBinaryHandler.createMessage(systemPrompt, messages) + // Consume the stream to trigger the API call + for await (const _chunk of stream) { + } + // Assert the mockCreate was called with both reasoning_effort and thinking + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.reasoning_effort).toBe("medium") + expect(callArgs.thinking).toEqual({ type: "enabled" }) + }) + + it("should not include thinking parameter when supportsReasoningBinary is false even with reasoning effort enabled", async () => { + const noReasoningBinaryOptions: ApiHandlerOptions = { + ...mockOptions, + enableReasoningEffort: true, + openAiCustomModelInfo: { + contextWindow: 128_000, + supportsPromptCache: false, + supportsReasoningEffort: true, + supportsReasoningBinary: false, + reasoningEffort: "high", + }, + } + const noReasoningBinaryHandler = new OpenAiHandler(noReasoningBinaryOptions) + const stream = noReasoningBinaryHandler.createMessage(systemPrompt, messages) + // Consume the stream to trigger the API call + for await (const _chunk of stream) { + } + // Assert the mockCreate was called with reasoning_effort but NOT thinking + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.reasoning_effort).toBe("high") + expect(callArgs.thinking).toBeUndefined() + }) + it("should not include reasoning_effort when reasoning effort is disabled", async () => { const noReasoningOptions: ApiHandlerOptions = { ...mockOptions, @@ -1138,6 +1186,103 @@ describe("OpenAiHandler", () => { { path: "/models/chat/completions" }, ) }) + + it("should include thinking parameter for O3 model when supportsReasoningBinary is true", async () => { + const o3ReasoningBinaryHandler = new OpenAiHandler({ + ...mockOptions, + openAiModelId: "o3-mini", + openAiCustomModelInfo: { + contextWindow: 128_000, + maxTokens: 65536, + supportsPromptCache: false, + reasoningEffort: "medium" as "low" | "medium" | "high", + supportsReasoningBinary: true, + }, + }) + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + const stream = o3ReasoningBinaryHandler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.reasoning_effort).toBe("medium") + expect(callArgs.thinking).toEqual({ type: "enabled" }) + }) + + it("should not include thinking parameter for O3 model when supportsReasoningBinary is false", async () => { + const o3NoReasoningBinaryHandler = new OpenAiHandler({ + ...mockOptions, + openAiModelId: "o3-mini", + openAiCustomModelInfo: { + contextWindow: 128_000, + maxTokens: 65536, + supportsPromptCache: false, + reasoningEffort: "high" as "low" | "medium" | "high", + supportsReasoningBinary: false, + }, + }) + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + const stream = o3NoReasoningBinaryHandler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.reasoning_effort).toBe("high") + expect(callArgs.thinking).toBeUndefined() + }) + + it("should include thinking parameter for O3 model in non-streaming mode when supportsReasoningBinary is true", async () => { + const o3NonStreamingHandler = new OpenAiHandler({ + ...mockOptions, + openAiModelId: "o3-mini", + openAiStreamingEnabled: false, + openAiCustomModelInfo: { + contextWindow: 128_000, + maxTokens: 65536, + supportsPromptCache: false, + reasoningEffort: "low" as "low" | "medium" | "high", + supportsReasoningBinary: true, + }, + }) + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [ + { + role: "user", + content: "Hello!", + }, + ] + + const stream = o3NonStreamingHandler.createMessage(systemPrompt, messages) + const chunks: any[] = [] + for await (const chunk of stream) { + chunks.push(chunk) + } + + expect(mockCreate).toHaveBeenCalled() + const callArgs = mockCreate.mock.calls[0][0] + expect(callArgs.reasoning_effort).toBe("low") + expect(callArgs.thinking).toEqual({ type: "enabled" }) + }) }) }) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 74cbb511138..aad7c8ac780 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -159,6 +159,8 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl stream: true as const, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), ...(reasoning && reasoning), + // Add thinking parameter for OpenAI-compatible APIs that require it when using reasoning effort + ...(reasoning && modelInfo.supportsReasoningBinary ? { thinking: { type: "enabled" } } : {}), tools: this.convertToolsForOpenAI(metadata?.tools), tool_choice: metadata?.tool_choice, parallel_tool_calls: metadata?.parallelToolCalls ?? false, @@ -344,6 +346,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl stream: true, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), reasoning_effort: modelInfo.reasoningEffort as "low" | "medium" | "high" | undefined, + // Add thinking parameter for OpenAI-compatible APIs that require it when using reasoning effort + ...(modelInfo.reasoningEffort && modelInfo.supportsReasoningBinary + ? { thinking: { type: "enabled" } } + : {}), temperature: undefined, // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) tools: this.convertToolsForOpenAI(metadata?.tools), @@ -378,6 +384,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ...convertToOpenAiMessages(messages), ], reasoning_effort: modelInfo.reasoningEffort as "low" | "medium" | "high" | undefined, + // Add thinking parameter for OpenAI-compatible APIs that require it when using reasoning effort + ...(modelInfo.reasoningEffort && modelInfo.supportsReasoningBinary + ? { thinking: { type: "enabled" } } + : {}), temperature: undefined, // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) tools: this.convertToolsForOpenAI(metadata?.tools),