From 71093eda770d3cde3dfdd6a2db79a07b91181de0 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sat, 29 Aug 2026 08:10:26 -0300 Subject: [PATCH] fix(codex): keep parallel_tool_calls:false on translated Responses Lite path (#11707) (#11984) enforceCodexResponsesLiteParallelToolCalls() forces parallel_tool_calls:false at the top of CodexExecutor.execute(), but transformRequest() early-returns the body before its RESPONSES_API_ALLOWLIST field filter only when _nativeCodexPassthrough is set. Any request that reaches the codex executor via the translated (non-native-passthrough) path never gets that flag, so the allowlist filter silently deleted parallel_tool_calls right before the fetch body was sent, reproducing the reported upstream rejection ('X-OpenAI-Internal-Codex-Responses-Lite requires parallel_tool_calls to be false') for every model. Add parallel_tool_calls to RESPONSES_API_ALLOWLIST so the value survives the translated path too. Update the sibling #2608 allowlist test that previously asserted parallel_tool_calls gets stripped like other Chat Completions-only fields -- it is a legitimate Responses API field that must now survive. Co-authored-by: Markus Hartung --- ...odex-responses-lite-parallel-tool-calls.md | 1 + open-sse/executors/codex.ts | 5 ++ ...dex-responses-lite-translated-path.test.ts | 74 +++++++++++++++++++ tests/unit/executor-codex.test.ts | 2 - 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/11707-codex-responses-lite-parallel-tool-calls.md create mode 100644 tests/unit/executor-codex-responses-lite-translated-path.test.ts diff --git a/changelog.d/fixes/11707-codex-responses-lite-parallel-tool-calls.md b/changelog.d/fixes/11707-codex-responses-lite-parallel-tool-calls.md new file mode 100644 index 0000000000..a8cfd0cc1e --- /dev/null +++ b/changelog.d/fixes/11707-codex-responses-lite-parallel-tool-calls.md @@ -0,0 +1 @@ +- fix(codex): keep `parallel_tool_calls:false` on the translated Codex Responses Lite path (#11707) diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 9fc9a925cf..62e3a1da13 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -1478,6 +1478,11 @@ export class CodexExecutor extends BaseExecutor { "client_metadata", // GPT-5 output verbosity ({ verbosity } — normalized above by normalizeCodexVerbosity). "text", + // Responses Lite (#7171/#7821/#11707): enforceCodexResponsesLiteParallelToolCalls() + // forces this field on the translated (non-_nativeCodexPassthrough) path too — it + // must survive this allowlist filter or upstream rejects with "X-OpenAI-Internal- + // Codex-Responses-Lite requires `parallel_tool_calls` to be false." + "parallel_tool_calls", // Internal markers used by OmniRoute pipeline "_omnirouteResponsesStore", ]); diff --git a/tests/unit/executor-codex-responses-lite-translated-path.test.ts b/tests/unit/executor-codex-responses-lite-translated-path.test.ts new file mode 100644 index 0000000000..13283e3a88 --- /dev/null +++ b/tests/unit/executor-codex-responses-lite-translated-path.test.ts @@ -0,0 +1,74 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { CodexExecutor } from "../../open-sse/executors/codex.ts"; + +// Issue #11707: "X-OpenAI-Internal-Codex-Responses-Lite requires parallel_tool_calls +// to be false" persists even though #7171/#7821 already force +// parallel_tool_calls:false in enforceCodexResponsesLiteParallelToolCalls() at the +// top of CodexExecutor.execute(). +// +// Root cause: that enforcement result flows into transformRequest(), which early- +// returns the body BEFORE field filtering only when `_nativeCodexPassthrough` is +// true (client sent native Responses-API-shaped input straight through). For any +// request that reaches the codex executor via the TRANSLATED path (client format +// is not the native Responses API — e.g. Chat Completions shaped input translated +// by chatCore for the codex provider), `_nativeCodexPassthrough` is never set, so +// transformRequest() falls through to the RESPONSES_API_ALLOWLIST loop — and that +// allowlist did not include "parallel_tool_calls", so the very value +// enforceCodexResponsesLiteParallelToolCalls() just forced to `false` gets +// silently deleted again right before the fetch body is serialized. +// +// This reproduces with any model — matching the reporter's "every model I select +// fails" — because the deletion is unconditional on the translated path. + +async function runLiteRequest(body: Record): Promise[]> { + const executor = new CodexExecutor(); + const originalFetch = globalThis.fetch; + const capturedBodies: Record[] = []; + + globalThis.fetch = async (_url, init) => { + capturedBodies.push(JSON.parse(String(init?.body || "{}"))); + return new Response(JSON.stringify({ id: "resp_lite", object: "response" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + + try { + await executor.execute({ + model: String(body.model), + body, + stream: true, + credentials: { accessToken: "codex-token" }, + clientHeaders: { "X-OpenAI-Internal-Codex-Responses-Lite": "true" }, + }); + } finally { + globalThis.fetch = originalFetch; + } + + return capturedBodies; +} + +test("#11707: Responses Lite parallel_tool_calls:false survives the TRANSLATED (non-native-passthrough) codex path", async () => { + // No `_nativeCodexPassthrough` marker — this is how a request arrives when the + // client's own format isn't detected as native OpenAI Responses (e.g. a + // manually configured Codex client sending Chat-Completions-shaped input, or + // any other translated path chatCore routes into the codex provider). + const body = { + model: "gpt-5", + messages: [{ role: "user", content: "hi" }], + }; + + const capturedBodies = await runLiteRequest(body); + + assert.equal( + capturedBodies[0].parallel_tool_calls, + false, + "Responses Lite must force parallel_tool_calls:false on the outbound Codex " + + "request even when the request took the translated (non-passthrough) path " + + "through transformRequest()'s RESPONSES_API_ALLOWLIST filter — otherwise " + + "upstream rejects with 'X-OpenAI-Internal-Codex-Responses-Lite requires " + + "`parallel_tool_calls` to be false.' (#11707)" + ); +}); diff --git a/tests/unit/executor-codex.test.ts b/tests/unit/executor-codex.test.ts index 62ae465558..725d8d4e10 100644 --- a/tests/unit/executor-codex.test.ts +++ b/tests/unit/executor-codex.test.ts @@ -273,7 +273,6 @@ test("CodexExecutor.transformRequest non-passthrough allowlist strips all residu function_call: "auto", functions: [{ name: "test", parameters: {} }], max_completion_tokens: 1000, - parallel_tool_calls: true, user: "cursor-user", metadata: { key: "value" }, stream_options: { include_usage: true }, @@ -310,7 +309,6 @@ test("CodexExecutor.transformRequest non-passthrough allowlist strips all residu assert.equal(result.function_call, undefined, "function_call should be stripped"); assert.equal(result.functions, undefined, "functions should be stripped"); assert.equal(result.max_completion_tokens, undefined, "max_completion_tokens should be stripped"); - assert.equal(result.parallel_tool_calls, undefined, "parallel_tool_calls should be stripped"); assert.equal(result.user, undefined, "user should be stripped"); assert.equal(result.metadata, undefined, "metadata should be stripped"); assert.equal(result.stream_options, undefined, "stream_options should be stripped");