mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: inject thinking param for OpenAI-compatible APIs with reasoning_effort
When reasoning_effort is set, OpenAI-compatible APIs like VolcEngine
default "thinking" to "disabled", causing a 400 error. This adds a
fetch wrapper that injects thinking: { type: "enabled" } into
request bodies that contain reasoning_effort but lack a thinking field.
Standard OpenAI endpoints silently ignore unknown body fields,
so this is safe for all OpenAI-compatible providers.
Closes #11395
This commit is contained in:
parent
dcb33c47ad
commit
5ead2da5a0
2 changed files with 118 additions and 1 deletions
|
|
@ -68,7 +68,7 @@ vi.mock("axios", () => ({
|
|||
},
|
||||
}))
|
||||
|
||||
import { OpenAiHandler, getOpenAiModels } from "../openai"
|
||||
import { OpenAiHandler, getOpenAiModels, createThinkingAwareFetch } from "../openai"
|
||||
import { ApiHandlerOptions } from "../../../shared/api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { openAiModelInfoSaneDefaults } from "@roo-code/types"
|
||||
|
|
@ -143,6 +143,7 @@ describe("OpenAiHandler", () => {
|
|||
expect.objectContaining({
|
||||
baseURL: "https://api.openai.com/v1",
|
||||
apiKey: "test-api-key",
|
||||
fetch: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
@ -978,3 +979,90 @@ describe("getOpenAiModels", () => {
|
|||
expect(result).toEqual(["gpt-4", "gpt-3.5-turbo"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("createThinkingAwareFetch", () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
it("should inject thinking: { type: 'enabled' } when reasoning_effort is present", async () => {
|
||||
let capturedBody: string | undefined
|
||||
|
||||
globalThis.fetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
capturedBody = init?.body as string
|
||||
return new Response(JSON.stringify({}), { status: 200 })
|
||||
}) as any
|
||||
|
||||
const wrappedFetch = createThinkingAwareFetch()
|
||||
const body = JSON.stringify({ model: "some-model", reasoning_effort: "high" })
|
||||
await wrappedFetch("https://example.com/v1/chat/completions", { method: "POST", body })
|
||||
|
||||
const parsed = JSON.parse(capturedBody!)
|
||||
expect(parsed.thinking).toEqual({ type: "enabled" })
|
||||
expect(parsed.reasoning_effort).toBe("high")
|
||||
})
|
||||
|
||||
it("should not inject thinking when reasoning_effort is absent", async () => {
|
||||
let capturedBody: string | undefined
|
||||
|
||||
globalThis.fetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
capturedBody = init?.body as string
|
||||
return new Response(JSON.stringify({}), { status: 200 })
|
||||
}) as any
|
||||
|
||||
const wrappedFetch = createThinkingAwareFetch()
|
||||
const body = JSON.stringify({ model: "some-model", messages: [] })
|
||||
await wrappedFetch("https://example.com/v1/chat/completions", { method: "POST", body })
|
||||
|
||||
const parsed = JSON.parse(capturedBody!)
|
||||
expect(parsed.thinking).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should not overwrite existing thinking parameter", async () => {
|
||||
let capturedBody: string | undefined
|
||||
|
||||
globalThis.fetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
capturedBody = init?.body as string
|
||||
return new Response(JSON.stringify({}), { status: 200 })
|
||||
}) as any
|
||||
|
||||
const wrappedFetch = createThinkingAwareFetch()
|
||||
const body = JSON.stringify({
|
||||
model: "some-model",
|
||||
reasoning_effort: "high",
|
||||
thinking: { type: "disabled", budget_tokens: 0 },
|
||||
})
|
||||
await wrappedFetch("https://example.com/v1/chat/completions", { method: "POST", body })
|
||||
|
||||
const parsed = JSON.parse(capturedBody!)
|
||||
expect(parsed.thinking).toEqual({ type: "disabled", budget_tokens: 0 })
|
||||
})
|
||||
|
||||
it("should pass through non-JSON bodies unchanged", async () => {
|
||||
let capturedBody: string | undefined
|
||||
|
||||
globalThis.fetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
capturedBody = init?.body as string
|
||||
return new Response("ok", { status: 200 })
|
||||
}) as any
|
||||
|
||||
const wrappedFetch = createThinkingAwareFetch()
|
||||
const body = "not-json-body"
|
||||
await wrappedFetch("https://example.com/v1/chat/completions", { method: "POST", body })
|
||||
|
||||
expect(capturedBody).toBe("not-json-body")
|
||||
})
|
||||
|
||||
it("should pass through requests with no body", async () => {
|
||||
globalThis.fetch = vi.fn(async () => {
|
||||
return new Response("ok", { status: 200 })
|
||||
}) as any
|
||||
|
||||
const wrappedFetch = createThinkingAwareFetch()
|
||||
await wrappedFetch("https://example.com/v1/models")
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith("https://example.com/v1/models", undefined)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
baseURL,
|
||||
apiKey,
|
||||
headers,
|
||||
fetch: createThinkingAwareFetch(),
|
||||
})
|
||||
this.languageModelFactory = (modelId: string) => provider.chat(modelId)
|
||||
}
|
||||
|
|
@ -373,6 +374,34 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fetch wrapper that injects `thinking: { type: "enabled" }` into
|
||||
* request bodies that already contain `reasoning_effort`. This fixes
|
||||
* OpenAI-compatible APIs (e.g. VolcEngine) that default `thinking` to
|
||||
* `disabled` when it's not explicitly provided, causing a 400 error when
|
||||
* combined with `reasoning_effort`.
|
||||
*
|
||||
* Standard OpenAI endpoints silently ignore unrecognised body fields,
|
||||
* so this is safe for all OpenAI-compatible providers.
|
||||
*/
|
||||
/** @internal Exported for testing. */
|
||||
export function createThinkingAwareFetch(): typeof globalThis.fetch {
|
||||
return async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
if (init?.body && typeof init.body === "string") {
|
||||
try {
|
||||
const json = JSON.parse(init.body)
|
||||
if (json.reasoning_effort && !json.thinking) {
|
||||
json.thinking = { type: "enabled" }
|
||||
init = { ...init, body: JSON.stringify(json) }
|
||||
}
|
||||
} catch {
|
||||
// Not JSON – pass through untouched.
|
||||
}
|
||||
}
|
||||
return globalThis.fetch(input, init)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOpenAiModels(baseUrl?: string, apiKey?: string, openAiHeaders?: Record<string, string>) {
|
||||
try {
|
||||
if (!baseUrl) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue