mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
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:
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user