From 1636a8ec4e8a7f606c2eb101571db6ab9a8b5291 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 19 Jul 2026 03:18:06 +0300 Subject: [PATCH] fix(executors): disable parallel tools for Codex Responses Lite (#7171) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168) * fix(executors): disable parallel tools for Codex Responses Lite * docs(changelog): add Responses Lite fix fragment --------- Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: alexey.nazarov@softmg.ru --- ...171-codex-responses-lite-parallel-tools.md | 1 + open-sse/executors/codex.ts | 62 ++++++++++++++++-- tests/unit/executor-codex-gpt56.test.ts | 64 +++++++++++++++++++ 3 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 changelog.d/fixes/7171-codex-responses-lite-parallel-tools.md diff --git a/changelog.d/fixes/7171-codex-responses-lite-parallel-tools.md b/changelog.d/fixes/7171-codex-responses-lite-parallel-tools.md new file mode 100644 index 0000000000..328f60441a --- /dev/null +++ b/changelog.d/fixes/7171-codex-responses-lite-parallel-tools.md @@ -0,0 +1 @@ +- **fix(executors):** Codex Responses Lite requests force serial tool calls required by the upstream API ([#7171](https://github.com/diegosouzapw/OmniRoute/pull/7171)) — thanks @fenix007 diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 50bc53c305..46cb7ae7d0 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -124,6 +124,52 @@ const GPT_5_6_MAX_ALIAS_MODELS = new Set(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5 const GPT_5_6_ULTRA_ALIAS_MODELS = new Set(["gpt-5.6-sol", "gpt-5.6-terra"]); const CODEX_FAST_WIRE_VALUE = "priority"; const CODEX_RESPONSES_WS_URL = "wss://chatgpt.com/backend-api/codex/responses"; +const CODEX_RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite"; +const CODEX_RESPONSES_LITE_WS_METADATA_KEY = + "ws_request_header_x_openai_internal_codex_responses_lite"; + +// The official Codex client marks Responses Lite over an HTTP header or, for WebSocket +// requests, mirrors the same signal into client_metadata. Lite rejects parallel tool calls. +function isEnabledResponsesLiteFlag(value: unknown): boolean { + return value === true || (typeof value === "string" && value.trim().toLowerCase() === "true"); +} + +function isCodexResponsesLiteRequest( + bodyInput: unknown, + clientHeaders?: Record | null +): boolean { + const hasLiteHeader = Object.entries(clientHeaders ?? {}).some( + ([key, value]) => + key.toLowerCase() === CODEX_RESPONSES_LITE_HEADER && isEnabledResponsesLiteFlag(value) + ); + if (hasLiteHeader) return true; + + if (!bodyInput || typeof bodyInput !== "object" || Array.isArray(bodyInput)) return false; + const metadata = (bodyInput as Record).client_metadata; + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return false; + + return isEnabledResponsesLiteFlag( + (metadata as Record)[CODEX_RESPONSES_LITE_WS_METADATA_KEY] + ); +} + +function enforceCodexResponsesLiteParallelToolCalls( + bodyInput: unknown, + clientHeaders?: Record | null +): unknown { + if ( + !isCodexResponsesLiteRequest(bodyInput, clientHeaders) || + !bodyInput || + typeof bodyInput !== "object" || + Array.isArray(bodyInput) + ) { + return bodyInput; + } + + const body = bodyInput as Record; + if (body.parallel_tool_calls === false) return bodyInput; + return { ...body, parallel_tool_calls: false }; +} function splitCodexReasoningSuffix(model: unknown): { baseModel: string; @@ -801,24 +847,26 @@ export class CodexExecutor extends BaseExecutor { } async execute(input: ExecuteInput) { + const requestBody = enforceCodexResponsesLiteParallelToolCalls(input.body, input.clientHeaders); + const requestInput = requestBody === input.body ? input : { ...input, body: requestBody }; const sessionId = this.getPromptCacheSessionId( - input.credentials, - input.body as Record | null + requestInput.credentials, + requestInput.body as Record | null ); const identity = createCodexClientIdentity( sessionId, - input.credentials?.providerSpecificData ?? null + requestInput.credentials?.providerSpecificData ?? null ); const credentials = identity ? { - ...input.credentials, + ...requestInput.credentials, providerSpecificData: { - ...(input.credentials?.providerSpecificData || {}), + ...(requestInput.credentials?.providerSpecificData || {}), codexClientIdentity: identity, }, } - : input.credentials; - const nextInput = { ...input, credentials }; + : requestInput.credentials; + const nextInput = { ...requestInput, credentials }; if (!isCodexResponsesWebSocketRequired(nextInput.model, nextInput.credentials)) { const httpResult = await super.execute(nextInput); diff --git a/tests/unit/executor-codex-gpt56.test.ts b/tests/unit/executor-codex-gpt56.test.ts index 4c2fd01a1e..2dd29809e0 100644 --- a/tests/unit/executor-codex-gpt56.test.ts +++ b/tests/unit/executor-codex-gpt56.test.ts @@ -81,3 +81,67 @@ test("CodexExecutor.transformRequest clamps Luna ultra requests to its max effor assert.equal(result.model, "gpt-5.6-luna"); assert.equal(result.reasoning.effort, "max"); }); + +test("CodexExecutor.execute disables parallel tool calls for Responses Lite markers", async () => { + 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" }, + }); + }; + + const headerBody = { + _nativeCodexPassthrough: true, + model: "gpt-5.6-sol", + input: [], + parallel_tool_calls: true, + }; + const metadataBody = { + _nativeCodexPassthrough: true, + model: "gpt-5.6-sol", + input: [], + client_metadata: { + ws_request_header_x_openai_internal_codex_responses_lite: "true", + }, + }; + const standardBody = { + _nativeCodexPassthrough: true, + model: "gpt-5.6-sol", + input: [], + parallel_tool_calls: true, + }; + + try { + for (const request of [ + { + body: headerBody, + clientHeaders: { "X-OpenAI-Internal-Codex-Responses-Lite": "true" }, + }, + { body: metadataBody }, + { body: standardBody }, + ]) { + const result = await executor.execute({ + model: "gpt-5.6-sol", + body: request.body, + stream: true, + credentials: { accessToken: "codex-token" }, + clientHeaders: request.clientHeaders, + }); + assert.equal(result.response.status, 200); + } + + assert.equal(capturedBodies[0].parallel_tool_calls, false); + assert.equal(headerBody.parallel_tool_calls, true); + assert.equal(capturedBodies[1].parallel_tool_calls, false); + assert.equal(metadataBody.parallel_tool_calls, undefined); + assert.equal(capturedBodies[2].parallel_tool_calls, true); + assert.equal(standardBody.parallel_tool_calls, true); + } finally { + globalThis.fetch = originalFetch; + } +});