Skip to content

Commit e3a2fb7

Browse files
committed
Document Codex Responses continuity and serial tool calls
The ChatGPT Codex backend rejects store chaining, so every turn resends full input plus encrypted reasoning. Parallel tool calls stay off at this request layer because backend acceptance of true is unverified, not because the runner cannot fan out.
1 parent df0dbe4 commit e3a2fb7

3 files changed

Lines changed: 16 additions & 2 deletions

File tree

docs/IMPLEMENTATION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ Positional arguments after flags are joined into the optional initial task deliv
361361

362362
- OpenAI-compatible chat completions, streamed via `@intx/inference`
363363
- JSON-schema tool definitions for director-layer tools (`ask_operator`, `present`, `submit_output`) and agent tools (`manage_tasks`, `tool_search`, `use_skill`, `search_agents`, …)
364+
- Codex Responses (`codex-responses-adapter.ts` `buildRequest`): ChatGPT Codex is Responses-only. 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).
364365

365366
### State Persistence
366367

src/provider/codex-responses-adapter.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,15 @@ import { PRODUCT_NAME, ENVIRONMENT_TAG_NAME } from "../branding.js";
3030
// Credentials and the chatgpt-account-id ride through differently: the access
3131
// token is injected by the harness via the bearer sentinel, while the account
3232
// id and session id travel in `source.defaults.providerOptions` (merged into
33-
// InferenceOptions.providerOptions by the harness) and are lifted into headers
34-
// here. Neither is placed in the request body.
33+
// InferenceOptions.providerOptions by the harness). Account id is headers-only
34+
// (`chatgpt-account-id`). Session id is the `session_id` header and
35+
// `prompt_cache_key` in the body — the only cache-routing signal under
36+
// `store: false`.
37+
//
38+
// Continuity is not Responses store chaining. The ChatGPT Codex backend
39+
// requires `store: false` (`store: true` → 400) and rejects
40+
// `previous_response_id`. Every turn resends the full `input`; encrypted
41+
// reasoning captured from the prior stream is resent as a `reasoning` item.
3542

3643
export const CODEX_RESPONSES_PROVIDER = "codex-responses";
3744

@@ -342,6 +349,10 @@ function buildRequest(
342349
store: false,
343350
stream: true,
344351
include: ["reasoning.encrypted_content"],
352+
// Serial at the request layer. The reactor already executes a multi-call
353+
// batch concurrently; this flag is what the ChatGPT Codex backend is sent.
354+
// Do not flip without verifying the backend accepts true — unlike store /
355+
// previous_response_id there is no recorded 400.
345356
parallel_tool_calls: false,
346357
};
347358
// The Codex backend rejects `max_output_tokens`; it is intentionally omitted.

tests/unit/codex-responses-adapter.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ describe("codex-responses buildRequest", () => {
4747
expect(body["model"]).toBe("gpt-5-codex");
4848
expect(body["stream"]).toBe(true);
4949
expect(body["store"]).toBe(false);
50+
expect(body).not.toHaveProperty("previous_response_id");
51+
expect(body["parallel_tool_calls"]).toBe(false);
5052
expect(body["include"]).toEqual(["reasoning.encrypted_content"]);
5153
expect(body["prompt_cache_key"]).toBe("sess-1");
5254
expect(body["input"]).toEqual([

0 commit comments

Comments
 (0)