Skip to content

Commit 9a07ad5

Browse files
Send parallel_tool_calls true on Codex Responses requests (#969)
A live probe against the ChatGPT Codex backend accepts parallel_tool_calls:true (HTTP 200 on POST /codex/responses, echoed true in response.created), so the host rewrites the field the packaged adapter still pins to false. Related to CL-7420.
1 parent d9ec82c commit 9a07ad5

3 files changed

Lines changed: 33 additions & 2 deletions

File tree

‎docs/IMPLEMENTATION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ Positional arguments after flags are joined into the optional initial task deliv
405405

406406
- OpenAI-compatible chat completions, streamed via `@intx/inference`
407407
- JSON-schema tool definitions for director-layer tools (`ask_operator`, `present`, `submit_output`) and agent tools (`manage_tasks`, `tool_search`, `use_skill`, `search_agents`, …)
408-
- Codex Responses (`@corbits/codex-provider` via `codex-responses.ts`): ChatGPT Codex is Responses-only. The host system prompt is wrapped with product identity as a leading `developer` input item; conversation turns follow in order. There is no official-prompt fetch, cache, or startup refresh. It requires `store: false` (`store: true` → 400) and rejects `previous_response_id`. Multi-turn continuity is full `input` replay; encrypted reasoning captured via `include: ["reasoning.encrypted_content"]` is resent as a `reasoning` item. `prompt_cache_key` (session id) is the cache-routing signal. `parallel_tool_calls` is sent `false` (serial at this surface); the reactor already fans out a multi-call batch concurrently. `max_output_tokens` is omitted (backend rejects it).
408+
- Codex Responses (`@corbits/codex-provider` via `codex-responses.ts`): ChatGPT Codex is Responses-only. The host system prompt is wrapped with product identity as a leading `developer` input item; conversation turns follow in order. There is no official-prompt fetch, cache, or startup refresh. It requires `store: false` (`store: true` → 400) and rejects `previous_response_id`. Multi-turn continuity is full `input` replay; encrypted reasoning captured via `include: ["reasoning.encrypted_content"]` is resent as a `reasoning` item. `prompt_cache_key` (session id) is the cache-routing signal. `parallel_tool_calls` is sent `true` (CL-7420 live probe: backend 200s and echoes it true; the packaged adapter still pins `false`, so the host rewrites it on the wire); the reactor already fans out a multi-call batch concurrently. `max_output_tokens` is omitted (backend rejects it).
409409

410410
### State Persistence
411411

‎src/provider/codex-responses.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ describe("createCodexResponsesAdapter", () => {
6161
expect(body.prompt_cache_key).toBe("sess-1");
6262
});
6363

64+
test("sends parallel_tool_calls:true on the wire (CL-7420)", () => {
65+
const adapter = createCodexResponsesAdapter(source);
66+
const request = adapter.buildRequest(
67+
[userTurn("hello")],
68+
"gpt-5.1-codex",
69+
{},
70+
);
71+
const body = JSON.parse(request.body) as {
72+
parallel_tool_calls?: unknown;
73+
};
74+
expect(body.parallel_tool_calls).toBe(true);
75+
});
76+
6477
test("wraps the system prompt with host product identity as a developer item", () => {
6578
const adapter = createCodexResponsesAdapter(source);
6679
const request = adapter.buildRequest([userTurn("x")], "gpt-5.1-codex", {

‎src/provider/codex-responses.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,27 @@ function withHostReasoningEffort(adapter: ProviderAdapter): ProviderAdapter {
5151
};
5252
}
5353

54+
// CL-7420 probe: the ChatGPT Codex backend accepts parallel_tool_calls:true
55+
// (HTTP 200 on POST /codex/responses, echoed true in response.created). The
56+
// packaged adapter still pins false, so the host rewrites the field on the
57+
// wire; the reactor already fans out a multi-call batch concurrently.
58+
function withParallelToolCalls(adapter: ProviderAdapter): ProviderAdapter {
59+
return {
60+
...adapter,
61+
buildRequest: (messages, model, options) => {
62+
const request = adapter.buildRequest(messages, model, options);
63+
const body = JSON.parse(request.body) as Record<string, unknown>;
64+
body["parallel_tool_calls"] = true;
65+
return { ...request, body: JSON.stringify(body) };
66+
},
67+
};
68+
}
69+
5470
// CodexQuirks are required by the package factory; Responses option keys are
5571
// not part of that bag, so host reasoning_effort is aliased at buildRequest.
5672
export const createCodexResponsesAdapter: AdapterFactory = (source) =>
5773
withHostReasoningEffort(
58-
createPackageCodexResponsesAdapter(source, CODEX_QUIRKS),
74+
withParallelToolCalls(
75+
createPackageCodexResponsesAdapter(source, CODEX_QUIRKS),
76+
),
5977
);

0 commit comments

Comments
 (0)