Files
olp/lib/providers/index.mjs
T
taodengandClaude Opus 4.7 (noreply@anthropic.com) ea9184d2e8 feat(phase-1): land OpenAI Codex provider plugin (D6)
Phase 1 Day 4. OpenAI Codex provider plugin code lands as Candidate.
Anthropic and Codex now both in STATIC_REGISTRY length 2. Codex CLI is
NOT installed on the orchestrator machine, so D6 ships with a docs-only
authority pin; D7 will install the binary, probe real behaviour, and
fix any docs-vs-reality divergences (A3 access-token field, A4 NDJSON
event schema, possibly keyring storage support).

Files:
  NEW:  lib/providers/codex.mjs (586 lines initially, expanded ~10 lines
        via reviewer fold-ins) — Codex provider implementing the v1.0
        contract. spawns `codex exec --json --model <id> [PROMPT|-]` per
        canonical Codex docs.
  MOD:  lib/providers/index.mjs — STATIC_REGISTRY now [anthropic, codex],
        listAllProviderNames() returns 2 entries.
  MOD:  models-registry.json — providers.openai populated with five
        documented model IDs (gpt-5.5, gpt-5.4, gpt-5.4-mini,
        gpt-5.3-codex, gpt-5.3-codex-spark) and four aliases.
  MOD:  test-features.mjs — Suite 11 added with 46 tests covering contract
        conformance, IR translation, mock-spawn behaviour, healthCheck,
        estimateCost, registry length.

Authority citations (all WebFetched and verified during reviewer pass):
  CLI reference: https://developers.openai.com/codex/cli/reference
    Source for `codex exec` subcommand syntax, --json flag, --model -m
    flag, and PROMPT positional including the `-` form for stdin piping.
  Features:      https://developers.openai.com/codex/cli/features
    Reference for the supported-models list.
  Auth:          https://developers.openai.com/codex/auth/
    Canonical pin for `~/.codex/auth.json` plaintext credential file
    and `cli_auth_credentials_store = keyring` OS credential store option.
  Models:        https://developers.openai.com/codex/models
    Canonical pin for the five documented model IDs (each shown as a
    `codex -m <id>` example on the page).
  ChatGPT plan:  https://help.openai.com/en/articles/11369540 — Codex
    runs against ChatGPT subscription budget when OAuth-authenticated;
    OPENAI_API_KEY env path is for `codex login --with-api-key` only,
    not `codex exec` runtime.

Architectural decisions:
  1. Mirror D4 anthropic.mjs structure: file header, lossy translation
     docs, default export = provider object, named exports include
     __setSpawnImpl / __resetSpawnImpl for test injection.
  2. Stdin path uses `args.push('-')` per documented CLI behaviour.
     (Original D6 sonnet draft omitted the positional entirely and wrote
     stdin directly — D6 reviewer pass 2 caught this; corrected before
     commit. D7 E2E confirms.)
  3. Auth artifact path `~/.codex/auth.json` is now documented in the
     header as CONFIRMED per canonical auth doc, not assumed.
  4. Access-token field name remains a defensive 3-name try-order
     (access_token / token / accessToken) because the auth doc does
     not enumerate field names. D7 captures real auth.json post-login.
  5. OPENAI_API_KEY env injection during spawn is intentionally NOT
     done. The auth doc clarifies OPENAI_API_KEY is a login-time input,
     not a runtime override. codex exec reads its own auth artifact.
  6. Codex stays Candidate. loadProviders({}) returns empty Map; only
     loadProviders({ enabled: { openai: true } }) loads it. POST /v1/
     chat/completions gpt-5.5 etc still returns 503 until config flag
     is set + E2E audit passes.

Reviewer chain (Iron Rule 10):
  Implementer: sonnet (general-purpose).
  Fresh-context reviewer: opus (ecc:code-reviewer). Verdict
    APPROVE_WITH_MINOR.
  Reviewer ran npm test (174/174 pass with Suite 10 skipped), WebFetched
    all four canonical Codex docs URLs, and discovered two additional
    docs pages (auth + models) that sonnet had missed. Reviewer
    independently verified the documentation citations rather than
    trusting sonnet quotes — Rule 2 (No Invention) is the load-bearing
    check for D6 because no local binary exists to ground-truth the
    plugin.

Reviewer non-blocking findings folded in this commit:
  1. Stdin path corrected: docs explicitly state `Use - to pipe the
     prompt from stdin`. The original draft assumed "no positional →
     stdin"; docs require literal `-`. Fixed in irToCodex; test
     "irToCodex: multiline prompt uses stdin path (useStdin=true) with
     - positional" updated to assert args.includes('-').
  2. Model registry expanded from 3 to 5 entries per canonical models
     doc. Added gpt-5.4-mini and gpt-5.3-codex-spark. Removed the
     misread Rule 2 comment that justified omitting -spark suffix —
     the docs literally show `codex -m gpt-5.3-codex-spark`, so the
     -spark variant is a separate model not a -codex normalization.
     New aliases: codex-spark, gpt5-mini.
  3. File header A2 upgraded from "assumed" to "CONFIRMED" with the
     canonical auth doc URL cited.
  4. File header now cites both auth and models canonical URLs at the
     top, alongside reference and features.

Reviewer findings deferred to D7:
  - OS credential store / keyring support. Codex docs mention
    cli_auth_credentials_store = keyring as an alternative to file
    storage. The Anthropic plugin supports macOS keychain via security
    find-generic-password; Codex equivalent unknown without inspecting
    a real install. D7 will install codex, run codex login, see what
    keyring entry codex creates (if any), and mirror the Anthropic
    keychain support pattern.
  - Real NDJSON event schema (field names). Defensive 4-shape parser
    handles the most common conventions; D7 captures real stdout and
    pins the schema.
  - access_token field name in auth.json. D7 captures the real auth
    artifact and removes unused fallback names.

Test count: 128 (after D5) → 174 (after D6).

Verification:
  node --check on all touched files: clean.
  npm test on Node 25.8.0: 174/174 pass in 210ms with Suite 10 skipped.
  Test "codex.models contains all 5 docs-listed model IDs" passes.
  Test "irToCodex: multiline prompt uses stdin path with - positional"
    passes (asserts args.includes('-')).
  Hygiene grep: zero personal-name/path/token hits.

Co-Authored-By: Claude Opus 4.7 (noreply@anthropic.com)
2026-05-23 21:20:13 +10:00

106 lines
3.7 KiB
JavaScript

/**
* lib/providers/index.mjs — Static provider registry
*
* Authority: ADR 0002 § "Loading model"
*
* This file is a hand-maintained static enumeration. There is no filesystem
* scan, no dynamic discovery, no npm-installed plugin loading. Adding a
* provider requires: (1) write lib/providers/<name>.mjs, (2) add one import +
* one entry to STATIC_REGISTRY below, (3) README § "Supported Providers",
* (4) inclusion ADR per ADR 0006.
*
* At D4 (Phase 1 Day 2), the anthropic plugin is added to STATIC_REGISTRY
* as a Candidate. It is NOT Enabled by default — a config with
* { enabled: { anthropic: true } } is required, which only happens after D5
* E2E audit passes per ALIGNMENT.md § Provider Inventory.
*
* At D6 (Phase 1 Day 4), the openai (Codex) plugin is added to STATIC_REGISTRY
* as a Candidate. Enabled by { enabled: { openai: true } } after D7 E2E audit.
*/
import { validateProvider } from './base.mjs';
import anthropicDefault from './anthropic.mjs';
import codexDefault from './codex.mjs';
// Normalize default export pattern
const anthropic = anthropicDefault;
const codex = codexDefault;
// ── Static registry ───────────────────────────────────────────────────────
const STATIC_REGISTRY = [
anthropic,
codex,
];
// ── Registry functions ────────────────────────────────────────────────────
/**
* Loads and validates providers, filtering by the enabled set in config.
* At D4, the anthropic provider is Candidate only — it will not appear in
* the loaded Map unless config.enabled.anthropic === true (set by D5 after E2E).
*
* @param {{ enabled?: Record<string,boolean> }} [config={}]
* @returns {Map<string, import('./base.mjs').ProviderContractV1>}
*/
export function loadProviders(config = {}) {
const loaded = new Map();
for (const p of STATIC_REGISTRY) {
const { valid, errors } = validateProvider(p);
if (!valid) {
throw new Error(`Provider ${p?.name ?? 'unknown'} fails contract validation: ${errors.join('; ')}`);
}
const enabled = config.enabled?.[p.name] === true;
if (enabled) {
loaded.set(p.name, p);
}
}
return loaded;
}
/**
* Returns the first provider in `loadedProviders` whose `models[]` includes
* the exact `modelString`. Returns null if no provider is found.
*
* This is the naive lookup strategy for D4. Phase 2+ may add prefix/alias
* matching once models-registry.json is consulted per AGENTS.md SPOT policy.
*
* @param {Map<string, import('./base.mjs').ProviderContractV1>} loadedProviders
* @param {string} modelString
* @returns {{ provider: import('./base.mjs').ProviderContractV1, name: string }|null}
*/
export function getProviderForModel(loadedProviders, modelString) {
for (const [name, p] of loadedProviders) {
if (p.models.includes(modelString)) {
return { provider: p, name };
}
}
return null;
}
/**
* Looks up a provider by name in the loaded Map. Returns null if not found.
* Useful for tests that need to access a specific provider directly.
*
* @param {Map<string, import('./base.mjs').ProviderContractV1>} loadedProviders
* @param {string} name
* @returns {import('./base.mjs').ProviderContractV1|null}
*/
export function getProviderByName(loadedProviders, name) {
return loadedProviders.get(name) ?? null;
}
/**
* Returns all provider names in the static registry (whether enabled or not).
* Used by /health and diagnostics.
* At D4: returns ['anthropic']; at D6: returns ['anthropic', 'openai'].
*
* @returns {string[]}
*/
export function listAllProviderNames() {
return STATIC_REGISTRY.map(p => p.name);
}