Compare commits

...

1 Commits

Author SHA1 Message Date
Markus Hartung
dec9bd37dd fix(codex): keep parallel_tool_calls:false on translated Responses Lite path (#11707)
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.
2026-08-29 05:00:17 -03:00
4 changed files with 80 additions and 2 deletions

View File

@@ -0,0 +1 @@
- fix(codex): keep `parallel_tool_calls:false` on the translated Codex Responses Lite path (#11707)

View File

@@ -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",
]);

View File

@@ -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<string, unknown>): Promise<Record<string, unknown>[]> {
const executor = new CodexExecutor();
const originalFetch = globalThis.fetch;
const capturedBodies: Record<string, unknown>[] = [];
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)"
);
});

View File

@@ -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");