Skip to content

Commit 4628119

Browse files
committed
Serve Codex through the shared Responses adapter
1 parent 3b120a2 commit 4628119

9 files changed

Lines changed: 168 additions & 20 deletions

File tree

‎docs/IMPLEMENTATION.md‎

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

394394
- OpenAI-compatible chat completions, streamed via `@intx/inference`
395395
- JSON-schema tool definitions for director-layer tools (`ask_operator`, `present`, `submit_output`) and agent tools (`manage_tasks`, `tool_search`, `use_skill`, `search_agents`, …)
396-
- Codex Responses (`codex-responses-adapter.ts` `buildRequest`): ChatGPT Codex is Responses-only. `instructions` is exactly the supplied Corbits system prompt (including an empty string), omitted when unset. `input` contains only the converted conversation turns, preserving their roles and order without a synthetic developer bridge. 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).
396+
- 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).
397397

398398
### State Persistence
399399

‎src/config/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
CODEX_RESPONSES_PROVIDER,
4242
CODEX_ACCOUNT_ID_OPTION,
4343
CODEX_SESSION_ID_OPTION,
44-
} from "../provider/codex-responses-adapter.js";
44+
} from "../provider/codex-responses.js";
4545
import {
4646
GROK_RESPONSES_PROVIDER,
4747
GROK_SESSION_ID_OPTION,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { BEARER_CREDENTIAL_SENTINEL } from "@intx/inference";
3+
import type { ConversationTurn, LastCycleSource } from "@intx/types/runtime";
4+
import { ENVIRONMENT_TAG_NAME, PRODUCT_NAME } from "../branding.js";
5+
import {
6+
CODEX_ACCOUNT_ID_OPTION,
7+
CODEX_RESPONSES_PROVIDER,
8+
CODEX_SESSION_ID_OPTION,
9+
createCodexResponsesAdapter,
10+
} from "./codex-responses.js";
11+
12+
const source: LastCycleSource = {
13+
sourceId: "codex/test",
14+
provider: CODEX_RESPONSES_PROVIDER,
15+
model: "gpt-5.1-codex",
16+
};
17+
18+
function userTurn(text: string): ConversationTurn {
19+
return { role: "user", timestamp: 0, content: [{ type: "text", text }] };
20+
}
21+
22+
describe("createCodexResponsesAdapter", () => {
23+
test("forwards providerOptions.reasoning_effort onto reasoning.effort", () => {
24+
const adapter = createCodexResponsesAdapter(source);
25+
const request = adapter.buildRequest([userTurn("hello")], "gpt-5.1-codex", {
26+
providerOptions: { reasoning_effort: "high" },
27+
});
28+
const body = JSON.parse(request.body) as {
29+
reasoning?: { effort?: string };
30+
};
31+
expect(body.reasoning).toEqual({ effort: "high" });
32+
});
33+
34+
test("omits reasoning when effort is none or unset", () => {
35+
const adapter = createCodexResponsesAdapter(source);
36+
const unset = JSON.parse(
37+
adapter.buildRequest([userTurn("hello")], "gpt-5.1-codex", {}).body,
38+
) as { reasoning?: unknown };
39+
const none = JSON.parse(
40+
adapter.buildRequest([userTurn("hello")], "gpt-5.1-codex", {
41+
providerOptions: { reasoning_effort: "none" },
42+
}).body,
43+
) as { reasoning?: unknown };
44+
expect(unset.reasoning).toBeUndefined();
45+
expect(none.reasoning).toBeUndefined();
46+
});
47+
48+
test("lifts host account and session option keys into Codex headers", () => {
49+
const adapter = createCodexResponsesAdapter(source);
50+
const request = adapter.buildRequest([userTurn("hi")], "gpt-5.1-codex", {
51+
providerOptions: {
52+
[CODEX_ACCOUNT_ID_OPTION]: "acct-1",
53+
[CODEX_SESSION_ID_OPTION]: "sess-1",
54+
},
55+
});
56+
expect(request.url).toBe("/codex/responses");
57+
expect(request.headers["authorization"]).toBe(BEARER_CREDENTIAL_SENTINEL);
58+
expect(request.headers["chatgpt-account-id"]).toBe("acct-1");
59+
expect(request.headers["session_id"]).toBe("sess-1");
60+
const body = JSON.parse(request.body) as { prompt_cache_key?: string };
61+
expect(body.prompt_cache_key).toBe("sess-1");
62+
});
63+
64+
test("wraps the system prompt with host product identity as a developer item", () => {
65+
const adapter = createCodexResponsesAdapter(source);
66+
const request = adapter.buildRequest([userTurn("x")], "gpt-5.1-codex", {
67+
systemPrompt: "be terse",
68+
});
69+
const body = JSON.parse(request.body) as {
70+
instructions?: unknown;
71+
input: { role?: string; content?: { text?: string }[] }[];
72+
};
73+
expect(body.instructions).toBeUndefined();
74+
expect(body.input[0]?.role).toBe("developer");
75+
const wrapped = body.input[0]?.content?.[0]?.text ?? "";
76+
expect(wrapped).toContain(`<${ENVIRONMENT_TAG_NAME} priority="0">`);
77+
expect(wrapped).toContain(`${PRODUCT_NAME} is the harness`);
78+
expect(wrapped).toContain("be terse");
79+
expect(body.input[1]?.role).toBe("user");
80+
});
81+
});

‎src/provider/codex-responses.ts‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
createCodexResponsesAdapter as createPackageCodexResponsesAdapter,
3+
CODEX_ACCOUNT_ID_OPTION,
4+
CODEX_REASONING_EFFORT_OPTION,
5+
CODEX_SESSION_ID_OPTION,
6+
withCodexContentTypeRepair,
7+
} from "@corbits/codex-provider";
8+
import type { AdapterFactory, ProviderAdapter } from "@intx/inference";
9+
import type { InferenceOptions } from "@intx/types/runtime";
10+
import { ENVIRONMENT_TAG_NAME, PRODUCT_NAME } from "../branding.js";
11+
12+
export const CODEX_RESPONSES_PROVIDER = "codex-responses";
13+
14+
export {
15+
CODEX_ACCOUNT_ID_OPTION,
16+
CODEX_SESSION_ID_OPTION,
17+
withCodexContentTypeRepair,
18+
};
19+
20+
const CODEX_QUIRKS = {
21+
productName: PRODUCT_NAME,
22+
environmentTagName: ENVIRONMENT_TAG_NAME,
23+
};
24+
25+
const HOST_REASONING_EFFORT_OPTION = "reasoning_effort";
26+
27+
function remapReasoningEffort(options: InferenceOptions): InferenceOptions {
28+
const providerOptions = options.providerOptions;
29+
if (providerOptions === undefined) return options;
30+
const hostEffort = providerOptions[HOST_REASONING_EFFORT_OPTION];
31+
if (
32+
hostEffort === undefined ||
33+
providerOptions[CODEX_REASONING_EFFORT_OPTION] !== undefined
34+
) {
35+
return options;
36+
}
37+
return {
38+
...options,
39+
providerOptions: {
40+
...providerOptions,
41+
[CODEX_REASONING_EFFORT_OPTION]: hostEffort,
42+
},
43+
};
44+
}
45+
46+
function withHostReasoningEffort(adapter: ProviderAdapter): ProviderAdapter {
47+
return {
48+
...adapter,
49+
buildRequest: (messages, model, options) =>
50+
adapter.buildRequest(messages, model, remapReasoningEffort(options)),
51+
};
52+
}
53+
54+
// CodexQuirks are required by the package factory; Responses option keys are
55+
// not part of that bag, so host reasoning_effort is aliased at buildRequest.
56+
export const createCodexResponsesAdapter: AdapterFactory = (source) =>
57+
withHostReasoningEffort(
58+
createPackageCodexResponsesAdapter(source, CODEX_QUIRKS),
59+
);

‎src/provider/inference-dependencies.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import {
66
import { loadAdapterRegistry } from "@intx/inference/providers";
77
import * as openaiCompatible from "./openai-compatible-adapter.js";
88
import * as opencodeGo from "./opencode-go-adapter.js";
9-
import * as codexResponses from "./codex-responses-adapter.js";
9+
import * as codexResponses from "./codex-responses.js";
1010
import * as grokResponses from "./grok-responses.js";
1111
import * as bifrostAdapter from "./bifrost-adapter.js";
1212
import * as openaiResponses from "./openai-responses.js";
1313
import * as opencodeGoAnthropic from "./opencode-go-anthropic-adapter.js";
1414
import {
1515
CODEX_RESPONSES_PROVIDER,
1616
withCodexContentTypeRepair,
17-
} from "./codex-responses-adapter.js";
17+
} from "./codex-responses.js";
1818
import { GROK_RESPONSES_PROVIDER } from "./grok-responses.js";
1919
import { withReplaySanitizer } from "./replay-sanitizer.js";
2020
import { OPENCODE_GO_PROVIDER_ID } from "../../packages/opencode-go/src/index.js";
@@ -39,7 +39,7 @@ const manifest: AdapterManifest = [
3939
},
4040
{
4141
provider: CODEX_RESPONSES_PROVIDER,
42-
specifier: "codex-responses-adapter",
42+
specifier: "codex-responses",
4343
export: "createCodexResponsesAdapter",
4444
},
4545
{
@@ -67,7 +67,7 @@ const manifest: AdapterManifest = [
6767
const localModules: Record<string, unknown> = {
6868
"openai-compatible-adapter": openaiCompatible,
6969
"opencode-go-adapter": opencodeGo,
70-
"codex-responses-adapter": codexResponses,
70+
"codex-responses": codexResponses,
7171
"grok-responses": grokResponses,
7272
"bifrost-adapter": bifrostAdapter,
7373
"openai-responses": openaiResponses,

‎src/provider/replay-sanitizer.test.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import type { ConversationTurn, LastCycleSource } from "@intx/types/runtime";
66
import {
77
CODEX_RESPONSES_PROVIDER,
88
createCodexResponsesAdapter,
9-
tagSignature,
10-
} from "./codex-responses-adapter.js";
9+
} from "./codex-responses.js";
1110
import { createGrokResponsesAdapter } from "./grok-responses.js";
1211
import { createOpenAICompatibleAdapter } from "./openai-compatible-adapter.js";
1312
import {
@@ -417,7 +416,7 @@ describe("withReplaySanitizer", () => {
417416
provider: CODEX_RESPONSES_PROVIDER,
418417
model: "gpt-5.1-codex",
419418
});
420-
const signature = tagSignature(CODEX_RESPONSES_PROVIDER, "cipher");
419+
const signature = `${CODEX_RESPONSES_PROVIDER}:cipher`;
421420
const turns: ConversationTurn[] = [
422421
{ role: "user", content: [{ type: "text", text: "hi" }], timestamp: 1 },
423422
{

‎tests/fixtures/codex-sse/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Codex / Responses SSE fixtures
22

33
Sanitized multi-event streams for golden tests of `parseResponse` in
4-
`src/provider/codex-responses-adapter.ts`.
4+
the Codex Responses adapter (`src/provider/codex-responses.ts`, served by
5+
`@corbits/codex-provider`).
56

67
Each `*.json` file is a JSON array of Responses SSE **data payloads** (the
78
object after `data: ` on each SSE line). No real tokens, prompts, account IDs,

‎tests/unit/codex-sse-fixtures.test.ts‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77
import { describe, expect, test } from "bun:test";
88
import { readFileSync } from "node:fs";
99
import { join } from "node:path";
10-
import {
11-
createCodexResponsesAdapter,
12-
isResponsesStreamTerminal,
13-
tagSignature,
14-
} from "../../src/provider/codex-responses-adapter.js";
10+
import { createCodexResponsesAdapter } from "../../src/provider/codex-responses.js";
1511
import type { InferenceEvent, LastCycleSource } from "@intx/types/runtime";
1612
import { defined } from "../helpers/defined.js";
1713
import { ProtocolMismatchError } from "@intx/inference";
@@ -101,7 +97,7 @@ describe("codex-sse fixtures (golden parse)", () => {
10197
expect(out[3]).toMatchObject({
10298
type: "inference.block.signature",
10399
data: {
104-
signature: tagSignature(SOURCE.provider, "ENC_FIXTURE_BLOB_NOT_REAL"),
100+
signature: `${SOURCE.provider}:ENC_FIXTURE_BLOB_NOT_REAL`,
105101
index: 0,
106102
},
107103
});
@@ -146,7 +142,11 @@ describe("codex-sse fixtures (golden parse)", () => {
146142
loadFixture("interleaved-reasoning-text-tools.json").at(-1),
147143
"last payload",
148144
);
149-
expect(isResponsesStreamTerminal(JSON.stringify(lastPayload))).toBe(true);
145+
expect(
146+
createCodexResponsesAdapter(SOURCE).isStreamTerminal?.(
147+
JSON.stringify(lastPayload),
148+
),
149+
).toBe(true);
150150
});
151151

152152
test("incomplete stream emits partial text and is terminal without usage", () => {
@@ -171,7 +171,11 @@ describe("codex-sse fixtures (golden parse)", () => {
171171
loadFixture("incomplete.json").at(-1),
172172
"last payload",
173173
);
174-
expect(isResponsesStreamTerminal(JSON.stringify(lastPayload))).toBe(true);
174+
expect(
175+
createCodexResponsesAdapter(SOURCE).isStreamTerminal?.(
176+
JSON.stringify(lastPayload),
177+
),
178+
).toBe(true);
175179
});
176180

177181
test("failed response throws ProtocolMismatchError after prior deltas", () => {
@@ -204,6 +208,10 @@ describe("codex-sse fixtures (golden parse)", () => {
204208
"last payload",
205209
);
206210
expect((lastPayload as { type: string }).type).toBe("response.done");
207-
expect(isResponsesStreamTerminal(JSON.stringify(lastPayload))).toBe(true);
211+
expect(
212+
createCodexResponsesAdapter(SOURCE).isStreamTerminal?.(
213+
JSON.stringify(lastPayload),
214+
),
215+
).toBe(true);
208216
});
209217
});

‎tests/unit/inference-response-kind.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { createInferenceDependencies } from "../../src/provider/inference-depend
2222
import {
2323
CODEX_RESPONSES_PROVIDER,
2424
withCodexContentTypeRepair,
25-
} from "../../src/provider/codex-responses-adapter.js";
25+
} from "../../src/provider/codex-responses.js";
2626
import { CODEX_RESPONSES_PATH } from "../../src/auth/codex/constants.js";
2727

2828
const CODEX_URL = `https://chatgpt.com/backend-api${CODEX_RESPONSES_PATH}`;

0 commit comments

Comments
 (0)