fix(executors): disable parallel tools for Codex Responses Lite (#7171)

* 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 <alexey.nazarov@softmg.ru>
This commit is contained in:
Alex
2026-07-19 03:18:06 +03:00
committed by GitHub
parent 02d4a9a8fe
commit 1636a8ec4e
3 changed files with 120 additions and 7 deletions

View File

@@ -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

View File

@@ -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<string, string> | 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<string, unknown>).client_metadata;
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return false;
return isEnabledResponsesLiteFlag(
(metadata as Record<string, unknown>)[CODEX_RESPONSES_LITE_WS_METADATA_KEY]
);
}
function enforceCodexResponsesLiteParallelToolCalls(
bodyInput: unknown,
clientHeaders?: Record<string, string> | null
): unknown {
if (
!isCodexResponsesLiteRequest(bodyInput, clientHeaders) ||
!bodyInput ||
typeof bodyInput !== "object" ||
Array.isArray(bodyInput)
) {
return bodyInput;
}
const body = bodyInput as Record<string, unknown>;
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<string, unknown> | null
requestInput.credentials,
requestInput.body as Record<string, unknown> | 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);

View File

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