P1.5.3 — Codex Adapter — Step 3 Execution Packet
Round: R92, Wave 3 (parallel slice 2/3) — p1-5-3-codex-adapter
Base SHA: 89adef66
Step: 3 of 5 (packet — gates implementation)
Author tier: T3 executor
§1. Implementation sequence
A single commit at Step 4 ships:
src/domains/router/adapters/codex.ts— adapter implementationsrc/__tests__/domains/router/adapters/codex.test.ts— parity tests
Both files are new. No existing source file is modified.
The implementation order within codex.ts:
Block 1 Header docblock (matches Claude adapter style; cites this packet)
Block 2 Imports (claude.js for shared types; nothing else)
Block 3 Constants (CODEX_API_BASE, CODEX_CHAT_COMPLETIONS_PATH,
DEFAULT_CODEX_MODEL, DEFAULT_MAX_TOKENS, MAX_RETRIES, BASE_DELAY_MS)
Block 4 Error classes (CodexConfigError, CodexApiError)
Block 5 Public types (CodexCompletionOptions; re-export CompletionResult,
AnthropicTool from '../../integrations/claude.js')
Block 6 Internal helpers (isRetryable, sleep, tryParseJson,
normalizeFinishReason, translateToolsToOpenAi, projectToolCalls,
buildCodexRequestBody, parseCodexResult)
Block 7 Core retry loop (attemptWithRetryCodex)
Block 8 Public API (createCodexCompletion, createCodexCompletionWithTools)
The implementation order within the test file:
Group A Fixtures + mock helpers (FAKE_KEY, FAKE_MODEL, FAKE_PROMPT,
SUCCESS_RESPONSE, TOOL_USE_RESPONSE, SAMPLE_TOOL,
makeMockFetch, makeSilentLogger, makeInstantDelay, baseOptions)
Group B createCodexCompletion — success path
Group C createCodexCompletionWithTools — success path + tool-use mapping
Group D API key validation
Group E Retry logic
Group F Finish-reason normalisation (table-driven)
Group G Base URL override
§2. Test inventory (target 10–13 tests)
| # | Group | Test | Asserts |
|---|---|---|---|
| 1 | B | returns CompletionResult with correct fields | content, model, promptTokens, completionTokens, stopReason, latencyMs |
| 2 | B | POSTs to /chat/completions with Authorization Bearer header | URL ends with /chat/completions; Authorization: Bearer <key>; no x-api-key; no anthropic-version |
| 3 | B | request body shape: model + max_tokens + messages array | model present, max_tokens present, messages = [{role:’user’,content:prompt}] |
| 4 | B | system prompt prepended as first message when present | messages = [{role:’system’,…}, {role:’user’,…}] |
| 5 | C | translates AnthropicTool[] → OpenAI tools nested under function key |
request.tools[0].type=’function’, request.tools[0].function = {name,description,parameters} |
| 6 | C | empty tools array → no tools key in body (degrades to plain) |
'tools' in body === false |
| 7 | C | tool_calls response → content is JSON-stringified Anthropic-shape array | content parses to [{type:'tool_use', id, name, input:{...}}]; stopReason=’tool_use’ |
| 8 | D | missing COLIBRI_CODEX_API_KEY → CodexConfigError with code | rejects with instanceof CodexConfigError AND code === 'CODEX_CONFIG_ERROR' |
| 9 | D | injected apiKey overrides process.env |
request Authorization header uses injected key |
| 10 | E | 429 retry with exponential backoff (3 calls, 2 delays of 100/200) | 3 fetch calls, 2 delayFn calls in [100, 200] |
| 11 | E | retries exhausted → CodexApiError with CODEX_RETRIES_EXHAUSTED | rejects; code matches; status is last 429 |
| 12 | F | normalizeFinishReason maps all 5 Codex values to Anthropic vocabulary | table-driven via it.each: ‘stop’→’end_turn’, ‘tool_calls’→’tool_use’, ‘length’→’max_tokens’, ‘content_filter’→’content_filter’, missing→’unknown’ |
| 13 | G | baseUrl override changes endpoint | request URL = ${override}/chat/completions |
Total: 13 tests. Within the dispatch packet’s 5–10 floor (we exceed by 3 for the tool-use mapping coverage delta — justified in audit §10).
§3. Lint posture
eslint-config for this repo is opinionated. The Claude adapter is
already lint-clean — the Codex adapter clones its conventions:
eslint-disable @typescript-eslint/no-explicit-anyon theparseResulthelper (Codex response shape is dynamic JSON).eslint-disable no-constant-conditionon the retry loop’swhile (true).- All
Record<string, unknown>for dynamic JSON manipulation. - No
as anycasts outside helper internals (the linter forbids them).
§4. Build posture
TypeScript strict mode is on. The contract types are public; their
exports must compile under noImplicitAny and strictNullChecks.
Compile target: dist/domains/router/adapters/codex.js + .d.ts.
§5. Verification evidence layout (forward-look for Step 5)
The verification doc at Step 5 will contain:
- Test run log (last 50 lines of
npm test) - Test count delta vs base (
3153 + 13 = 3166expected) - Build log (
npm run buildclean) - Lint log (
npm run lintclean) - Acceptance criteria checklist (full §11 from audit, checked)
- Tool-use mapping evidence (test 5 + test 7 outputs)
git diff origin/main..HEAD --statoutput- Confirmation that
src/domains/router/index.tsis byte-identical to base (viagit show origin/main:src/domains/router/index.ts | diff - src/domains/router/index.ts)
§6. Risk + rollback
- The branch is
feature/p1-5-3-codex-adapter. Rollback =git branch -Dandgh pr close --delete-branch. - No DB migration. No env-schema change. No MCP-surface change. No router fold-in.
- Worst case: tests fail at Step 4. Then iterate within Step 4 without committing partial work — Step 4 commits only the green state.
§7. Step 3 exit gate
The packet is complete. Implementation (Step 4) may proceed.
Commit message:
packet(p1-5-3-codex-adapter): execution plan