From 5d9b53b2c74853c739a652b2ba866ae57c834564 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:29:43 -0500 Subject: [PATCH] fix(ai): honor Responses parallel tool control (#44744) --- packages/ai/src/protocols/open-responses.ts | 10 +++++- packages/ai/src/protocols/openai-responses.ts | 2 ++ .../openai-compatible-responses.test.ts | 34 +++++++++++++++++++ .../ai/test/provider/openai-responses.test.ts | 25 ++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/protocols/open-responses.ts b/packages/ai/src/protocols/open-responses.ts index 59d8323bb3d..5948e3f90bc 100644 --- a/packages/ai/src/protocols/open-responses.ts +++ b/packages/ai/src/protocols/open-responses.ts @@ -666,6 +666,7 @@ const lowerMessages = Effect.fn("OpenResponses.lowerMessages")(function* (reques const lowerOptions = (request: LLMRequest) => { const options = OpenResponsesOptions.resolve(request) const cacheKey = ProviderShared.clampPromptCacheKey(request.promptCacheKey) + const parallelToolCalls = resolveParallelToolCalls(request) return { ...(options.instructions ? { instructions: options.instructions } : {}), ...(options.store !== undefined ? { store: options.store } : {}), @@ -683,11 +684,18 @@ const lowerOptions = (request: LLMRequest) => { ...(options.textVerbosity ? { text: { verbosity: options.textVerbosity } } : {}), ...(options.serviceTier ? { service_tier: options.serviceTier } : {}), ...(options.maxToolCalls !== undefined ? { max_tool_calls: options.maxToolCalls } : {}), - ...(options.parallelToolCalls !== undefined ? { parallel_tool_calls: options.parallelToolCalls } : {}), + ...(parallelToolCalls !== undefined ? { parallel_tool_calls: parallelToolCalls } : {}), ...(options.truncation ? { truncation: options.truncation } : {}), } } +export const resolveParallelToolCalls = (request: LLMRequest) => { + const configured = OpenResponsesOptions.resolve(request).parallelToolCalls + if (configured !== undefined) return configured + const disabled = request.toolChoice?.disableParallelToolUse + return disabled === undefined ? undefined : !disabled +} + const allowedToolChoice = (request: LLMRequest) => { const allowed = OpenResponsesOptions.resolve(request).allowedTools if (!allowed) return undefined diff --git a/packages/ai/src/protocols/openai-responses.ts b/packages/ai/src/protocols/openai-responses.ts index 9b3074a06a4..b53c21667c7 100644 --- a/packages/ai/src/protocols/openai-responses.ts +++ b/packages/ai/src/protocols/openai-responses.ts @@ -111,8 +111,10 @@ const fromRequest = Effect.fn("OpenAIResponses.fromRequest")(function* (request: extension, ) const toolSchemaCompatibility = request.model.compatibility?.toolSchema + const parallelToolCalls = OpenResponses.resolveParallelToolCalls(request) return { ...body, + ...(parallelToolCalls === undefined ? {} : { parallel_tool_calls: parallelToolCalls }), tools: request.tools.length === 0 ? undefined diff --git a/packages/ai/test/provider/openai-compatible-responses.test.ts b/packages/ai/test/provider/openai-compatible-responses.test.ts index 22c5f8f5d31..35616519308 100644 --- a/packages/ai/test/provider/openai-compatible-responses.test.ts +++ b/packages/ai/test/provider/openai-compatible-responses.test.ts @@ -132,6 +132,40 @@ describe("Open Responses-compatible route", () => { }), ) + it.effect("lowers canonical parallel tool control", () => + Effect.gen(function* () { + const model = configure({ + apiKey: "test-key", + baseURL: "https://responses.example.test/v1", + }).model("example-model") + const prepared = yield* compileRequest( + LLM.request({ + model, + prompt: "Read the file.", + tools: [ + ToolDefinition.make({ + name: "read", + description: "Read a file.", + inputSchema: { type: "object" }, + }), + ], + toolChoice: { type: "auto", disableParallelToolUse: true }, + }), + ) + + expect(prepared.body.parallel_tool_calls).toBe(false) + expect(prepared.body.tools).toEqual([ + { + type: "function", + name: "read", + description: "Read a file.", + parameters: { type: "object" }, + strict: false, + }, + ]) + }), + ) + it.effect("keeps foreign item id grammars but drops malformed ids", () => Effect.gen(function* () { const model = configure({ diff --git a/packages/ai/test/provider/openai-responses.test.ts b/packages/ai/test/provider/openai-responses.test.ts index 9fd367a684f..fd10f609331 100644 --- a/packages/ai/test/provider/openai-responses.test.ts +++ b/packages/ai/test/provider/openai-responses.test.ts @@ -258,6 +258,31 @@ describe("OpenAI Responses route", () => { }), ) + it.effect("maps the canonical parallel tool setting with provider-option precedence", () => + Effect.gen(function* () { + const disabled = yield* compileRequest( + LLMRequest.update(request, { + toolChoice: { type: "auto", disableParallelToolUse: true }, + }), + ) + const enabled = yield* compileRequest( + LLMRequest.update(request, { + toolChoice: { type: "auto", disableParallelToolUse: false }, + }), + ) + const overridden = yield* compileRequest( + LLMRequest.update(request, { + toolChoice: { type: "auto", disableParallelToolUse: true }, + providerOptions: { parallelToolCalls: true }, + }), + ) + + expect(disabled.body.parallel_tool_calls).toBe(false) + expect(enabled.body.parallel_tool_calls).toBe(true) + expect(overridden.body.parallel_tool_calls).toBe(true) + }), + ) + it.effect("lowers chronological system updates to developer messages in order", () => Effect.gen(function* () { const prepared = yield* compileRequest(