From d7e56bc25e882b56c19cf7989239e2fc8e811488 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sat, 24 Oct 2026 23:07:07 -0500 Subject: [PATCH] fix(ai): send OpenAI Chat options that were silently dropped (#58857) --- packages/ai/src/protocols/openai-chat.ts | 17 ++++++- packages/ai/test/provider/openai-chat.test.ts | 51 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/ai/src/protocols/openai-chat.ts b/packages/ai/src/protocols/openai-chat.ts index dba54c075323..5c907c054db2 100644 --- a/packages/ai/src/protocols/openai-chat.ts +++ b/packages/ai/src/protocols/openai-chat.ts @@ -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), } }) diff --git a/packages/ai/test/provider/openai-chat.test.ts b/packages/ai/test/provider/openai-chat.test.ts index 6535c71ba2f6..b7262134f71c 100644 --- a/packages/ai/test/provider/openai-chat.test.ts +++ b/packages/ai/test/provider/openai-chat.test.ts @@ -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(