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.
This commit is contained in:
Roo Code 2026-01-28 01:03:42 +00:00
parent d748de6fae
commit 19bd598a78
2 changed files with 155 additions and 0 deletions

View file

@ -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" })
})
})
})

View file

@ -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),