diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 0442ea1cc..7c338e86b 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -405,7 +405,7 @@ Positional arguments after flags are joined into the optional initial task deliv - OpenAI-compatible chat completions, streamed via `@intx/inference` - JSON-schema tool definitions for director-layer tools (`ask_operator`, `present`, `submit_output`) and agent tools (`manage_tasks`, `tool_search`, `use_skill`, `search_agents`, …) -- 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). +- 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). ### State Persistence diff --git a/src/provider/codex-responses.test.ts b/src/provider/codex-responses.test.ts index cb8b8facd..62b540ccb 100644 --- a/src/provider/codex-responses.test.ts +++ b/src/provider/codex-responses.test.ts @@ -61,6 +61,19 @@ describe("createCodexResponsesAdapter", () => { expect(body.prompt_cache_key).toBe("sess-1"); }); + test("sends parallel_tool_calls:true on the wire (CL-7420)", () => { + const adapter = createCodexResponsesAdapter(source); + const request = adapter.buildRequest( + [userTurn("hello")], + "gpt-5.1-codex", + {}, + ); + const body = JSON.parse(request.body) as { + parallel_tool_calls?: unknown; + }; + expect(body.parallel_tool_calls).toBe(true); + }); + test("wraps the system prompt with host product identity as a developer item", () => { const adapter = createCodexResponsesAdapter(source); const request = adapter.buildRequest([userTurn("x")], "gpt-5.1-codex", { diff --git a/src/provider/codex-responses.ts b/src/provider/codex-responses.ts index 67520a344..5523ca703 100644 --- a/src/provider/codex-responses.ts +++ b/src/provider/codex-responses.ts @@ -51,9 +51,27 @@ function withHostReasoningEffort(adapter: ProviderAdapter): ProviderAdapter { }; } +// CL-7420 probe: the ChatGPT Codex backend accepts parallel_tool_calls:true +// (HTTP 200 on POST /codex/responses, echoed true in response.created). The +// packaged adapter still pins false, so the host rewrites the field on the +// wire; the reactor already fans out a multi-call batch concurrently. +function withParallelToolCalls(adapter: ProviderAdapter): ProviderAdapter { + return { + ...adapter, + buildRequest: (messages, model, options) => { + const request = adapter.buildRequest(messages, model, options); + const body = JSON.parse(request.body) as Record; + body["parallel_tool_calls"] = true; + return { ...request, body: JSON.stringify(body) }; + }, + }; +} + // CodexQuirks are required by the package factory; Responses option keys are // not part of that bag, so host reasoning_effort is aliased at buildRequest. export const createCodexResponsesAdapter: AdapterFactory = (source) => withHostReasoningEffort( - createPackageCodexResponsesAdapter(source, CODEX_QUIRKS), + withParallelToolCalls( + createPackageCodexResponsesAdapter(source, CODEX_QUIRKS), + ), );