mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(ai): constrain GPT-6 Chat function calls to no reasoning
This commit is contained in:
@@ -766,8 +766,12 @@ const detectZaiToolStream = (provider: string, baseURL: string | undefined, mode
|
||||
return true
|
||||
}
|
||||
|
||||
const lowerOptions = (request: LLMRequest, supportsStore: boolean) => {
|
||||
const options = OpenAIOptions.resolve(request)
|
||||
const lowerOptions = (
|
||||
request: LLMRequest,
|
||||
options: ReturnType<typeof OpenAIOptions.resolve>,
|
||||
supportsStore: boolean,
|
||||
requiresNoneEffort: boolean,
|
||||
) => {
|
||||
// Default off: strict providers 400 on unknown body fields, so only send
|
||||
// the key where compatibility explicitly allows it. Header-based affinity
|
||||
// (x-session-affinity, x-grok-conv-id, ...) is unaffected.
|
||||
@@ -780,7 +784,11 @@ const lowerOptions = (request: LLMRequest, supportsStore: boolean) => {
|
||||
// native OpenAI Chat default. Non-standard providers omit `store` entirely.
|
||||
...(supportsStore && options.store === undefined ? { store: false } : {}),
|
||||
...(cacheKey ? { prompt_cache_key: cacheKey } : {}),
|
||||
...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
|
||||
...(requiresNoneEffort
|
||||
? { reasoning_effort: "none" }
|
||||
: options.reasoningEffort
|
||||
? { reasoning_effort: options.reasoningEffort }
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,6 +818,17 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (
|
||||
request.model.compatibility?.zaiToolStream ?? detectZaiToolStream(provider, baseURL, request.model.id)
|
||||
const hasHistory = hasToolHistory(request.messages)
|
||||
const hasActiveTools = flattened.tools.length > 0
|
||||
// GPT-6 Sol/Luna only support Chat function calling without reasoning.
|
||||
// Responses supports tools at every effort, so this applies to native OpenAI Chat only.
|
||||
const requiresNoneEffort =
|
||||
provider === "openai" &&
|
||||
(request.model.id === "gpt-6-sol" || request.model.id === "gpt-6-luna") &&
|
||||
(hasActiveTools || hasHistory)
|
||||
const nativeOptions = OpenAIOptions.resolve(request)
|
||||
if (requiresNoneEffort && nativeOptions.reasoningEffort !== undefined && nativeOptions.reasoningEffort !== "none")
|
||||
return yield* ProviderShared.invalidRequest(
|
||||
`${request.model.id} Chat function calling requires reasoningEffort "none"; use the Responses API for reasoning with tools`,
|
||||
)
|
||||
return {
|
||||
model: request.model.id,
|
||||
messages: yield* lowerMessages(flattened.request, options),
|
||||
@@ -832,7 +851,7 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (
|
||||
presence_penalty: generation?.presencePenalty,
|
||||
seed: generation?.seed,
|
||||
stop: generation?.stop,
|
||||
...lowerOptions(request, supportsStore),
|
||||
...lowerOptions(request, nativeOptions, supportsStore, requiresNoneEffort),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -249,6 +249,88 @@ describe("OpenAI Chat route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
for (const id of ["gpt-6-sol", "gpt-6-luna"]) {
|
||||
it.effect(`enables function calling for ${id} on OpenAI Chat`, () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ apiKey: "test" }).chat(id),
|
||||
prompt: "Look up the weather.",
|
||||
tools: [ToolDefinition.make({ name: "weather", description: "Find weather", inputSchema: {} })],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.tools).toHaveLength(1)
|
||||
expect(prepared.body.reasoning_effort).toBe("none")
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
it.effect("rejects GPT-6 Chat tools with explicit reasoning instead of silently downgrading it", () =>
|
||||
Effect.gen(function* () {
|
||||
const error = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ apiKey: "test", providerOptions: { reasoningEffort: "high" } }).chat("gpt-6-sol"),
|
||||
prompt: "Look up the weather.",
|
||||
tools: [ToolDefinition.make({ name: "weather", description: "Find weather", inputSchema: {} })],
|
||||
}),
|
||||
).pipe(Effect.flip)
|
||||
|
||||
expect(error.message).toContain('requires reasoningEffort "none"')
|
||||
expect(error.message).toContain("Responses API")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps GPT-6 Chat reasoning when no function calling is involved", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ apiKey: "test" }).chat("gpt-6-luna"),
|
||||
prompt: "Think carefully.",
|
||||
providerOptions: { reasoningEffort: "high" },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.reasoning_effort).toBe("high")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("does not impose OpenAI's GPT-6 Chat restriction on compatible providers", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: OpenAICompatible.configure({ baseURL: "https://api.compatible.test/v1", apiKey: "test" }).model(
|
||||
"gpt-6-sol",
|
||||
),
|
||||
prompt: "Look up the weather.",
|
||||
tools: [ToolDefinition.make({ name: "weather", description: "Find weather", inputSchema: {} })],
|
||||
providerOptions: { reasoningEffort: "high" },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.tools).toHaveLength(1)
|
||||
expect(prepared.body.reasoning_effort).toBe("high")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps GPT-6 Chat tool history valid without active tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ apiKey: "test" }).chat("gpt-6-sol"),
|
||||
messages: [
|
||||
Message.user("Look up the weather."),
|
||||
Message.assistant([ToolCallPart.make({ id: "call_1", name: "weather", input: {} })]),
|
||||
Message.tool({ id: "call_1", name: "weather", result: { forecast: "sunny" } }),
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.tools).toEqual([])
|
||||
expect(prepared.body.reasoning_effort).toBe("none")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps valid Chat options when a sibling option is malformed", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
|
||||
@@ -153,6 +153,22 @@ describe("OpenAI Responses route", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps GPT-6 reasoning with function tools on the Responses API", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
LLM.request({
|
||||
model: OpenAI.configure({ apiKey: "test" }).responses("gpt-6-sol"),
|
||||
prompt: "Look up the weather.",
|
||||
tools: [ToolDefinition.make({ name: "weather", description: "Find weather", inputSchema: {} })],
|
||||
providerOptions: { reasoningEffort: "high" },
|
||||
}),
|
||||
)
|
||||
|
||||
expect(prepared.body.tools).toHaveLength(1)
|
||||
expect(prepared.body.reasoning).toMatchObject({ effort: "high" })
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("lowers the hosted OpenAI image generation tool", () =>
|
||||
Effect.gen(function* () {
|
||||
const prepared = yield* compileRequest(
|
||||
|
||||
Reference in New Issue
Block a user