fix(ai): send OpenAI Chat options that were silently dropped (#58857)

This commit is contained in:
Aiden Cline 2026-10-24 23:07:07 -05:00 • committed by GitHub
parent 9cbdffcb06
commit d7e56bc25e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 66 additions and 2 deletions

View file

@ -25,6 +25,7 @@ import {
import { classifyProviderFailure } from "../provider-error.js"
import { isRecord, JsonObject, optionalArray, optionalNull, ProviderShared } from "./shared.js"
import { cacheControl } from "./utils/cache.js"
import { resolveParallelToolCalls } from "./open-responses.js"
import { OpenResponsesOptions } from "./utils/open-responses-options.js"
import { Lifecycle } from "./utils/lifecycle.js"
import { ToolStream } from "./utils/tool-stream.js"
@ -186,6 +187,11 @@ export const bodyFields = {
store: Schema.optional(Schema.Boolean),
prompt_cache_key: Schema.optional(Schema.String),
reasoning_effort: Schema.optional(OpenResponsesOptions.ReasoningEffort),
verbosity: Schema.optional(OpenResponsesOptions.TextVerbositySchema),
service_tier: Schema.optional(OpenResponsesOptions.ServiceTierSchema),
parallel_tool_calls: Schema.optional(Schema.Boolean),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
safety_identifier: Schema.optional(Schema.String),
tool_stream: Schema.optional(Schema.Boolean),
max_completion_tokens: Schema.optional(Schema.Number),
max_tokens: Schema.optional(Schema.Number),
@ -761,8 +767,10 @@ const detectZaiToolStream = (provider: string, baseURL: string | undefined, mode
return true
}
const lowerOptions = (request: LLMRequest, supportsStore: boolean) => {
const lowerOptions = (request: LLMRequest, supportsStore: boolean, hasActiveTools: boolean) => {
const options = OpenResponsesOptions.resolve(request)
// OpenAI rejects `parallel_tool_calls` unless tools are present.
const parallelToolCalls = hasActiveTools ? resolveParallelToolCalls(request, options) : undefined
// 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.
@ -772,6 +780,11 @@ const lowerOptions = (request: LLMRequest, supportsStore: boolean) => {
...(supportsStore ? { store: options.store ?? false } : {}),
...(cacheKey ? { prompt_cache_key: cacheKey } : {}),
...(options.reasoningEffort ? { reasoning_effort: options.reasoningEffort } : {}),
...(options.textVerbosity ? { verbosity: options.textVerbosity } : {}),
...(options.serviceTier ? { service_tier: options.serviceTier } : {}),
...(parallelToolCalls !== undefined ? { parallel_tool_calls: parallelToolCalls } : {}),
...(options.metadata ? { metadata: options.metadata } : {}),
...(options.safetyIdentifier ? { safety_identifier: options.safetyIdentifier } : {}),
}
}
@ -822,7 +835,7 @@ export const fromRequest = Effect.fn("OpenAIChat.fromRequest")(function* (
presence_penalty: generation?.presencePenalty,
seed: generation?.seed,
stop: generation?.stop,
...lowerOptions(request, supportsStore),
...lowerOptions(request, supportsStore, hasActiveTools),
}
})

View file

@ -312,6 +312,57 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("maps shared OpenAI options to Chat fields", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model: OpenAI.configure({ baseURL: "https://api.openai.test/v1/", apiKey: "test" }).chat("gpt-5-nano"),
prompt: "Hello",
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
providerOptions: {
store: true,
serviceTier: "flex",
textVerbosity: "low",
parallelToolCalls: false,
metadata: { source: "test" },
safetyIdentifier: "user-hash",
},
}),
)
expect(prepared.body).toMatchObject({
store: true,
service_tier: "flex",
verbosity: "low",
parallel_tool_calls: false,
metadata: { source: "test" },
safety_identifier: "user-hash",
})
expect(prepared.body).not.toHaveProperty("text")
}),
)
it.effect("maps disableParallelToolUse to parallel_tool_calls only when tools are present", () =>
Effect.gen(function* () {
const tool = ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })
const withTools = yield* compileRequest(
LLMRequest.update(request, { tools: [tool], toolChoice: { type: "auto", disableParallelToolUse: true } }),
)
const withoutTools = yield* compileRequest(
LLMRequest.update(request, { toolChoice: { type: "auto", disableParallelToolUse: true } }),
)
const unset = yield* compileRequest(LLMRequest.update(request, { tools: [tool] }))
expect(withTools.body.parallel_tool_calls).toBe(false)
expect(withoutTools.body).not.toHaveProperty("parallel_tool_calls")
expect(unset.body).not.toHaveProperty("parallel_tool_calls")
expect(unset.body).not.toHaveProperty("service_tier")
expect(unset.body).not.toHaveProperty("verbosity")
expect(unset.body).not.toHaveProperty("metadata")
expect(unset.body).not.toHaveProperty("safety_identifier")
}),
)
it.effect("maps the request prompt cache key when the compatibility flag is set", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(