fix(ai): constrain GPT-6 Chat function calls to no reasoning

This commit is contained in:
nexxeln
2026-09-25 08:40:54 +00:00
parent 917d904f18
commit 5a5cd7b196
3 changed files with 121 additions and 4 deletions
+23 -4
View File
@@ -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(