P1.5.4 — OpenAI Adapter (GPT-4o family) — Execution Packet (Step 3)

This packet defines the concrete execution plan for Step 4 (Implement) and Step 5 (Verify) per the contract signed in Step 2.

1. Implementation sequence

Step 4 sub-sequence

  1. Create src/domains/router/adapters/ directory.
  2. Create src/__tests__/domains/router/adapters/ directory.
  3. Implement src/domains/router/adapters/openai.ts (~360 LOC).
  4. Implement src/__tests__/domains/router/adapters/openai.test.ts (~700 LOC, ~25 tests).
  5. Run npm run build — fails iff TypeScript types are wrong.
  6. Run npm run lint — fails iff stylistic rules violated.
  7. Run npm test -- --testPathPattern="openai" — green.
  8. Run npm test (full suite) — green; expected delta 3153 → 3153 + N (N ≈ 25).
  9. Commit feat(p1-5-4-openai-adapter): OpenAI GPT-4o adapter with surface parity (no stubs).

Step 5 sub-sequence

  1. Verify file enumeration:
    • src/domains/router/adapters/openai.ts — created
    • src/__tests__/domains/router/adapters/openai.test.ts — created
    • src/domains/router/index.tsbyte-identical to 89adef66 (CRITICAL)
  2. Run final test gate trifecta: npm run build && npm run lint && npm test.
  3. Inventory parity-test pass count against the AC table (§7 of audit).
  4. Write docs/verification/p1-5-4-openai-adapter-verification.md.
  5. Commit verify(p1-5-4-openai-adapter): parity tests + mapping evidence.

2. Module shape — src/domains/router/adapters/openai.ts

2.1 Imports

import type {
  CompletionResult,
} from '../../integrations/claude.js';

The adapter imports CompletionResult as a type-only re-export from the Phase 0 ν Claude adapter. This is the single load-bearing import — it guarantees CompletionResult from openai.ts and claude.ts are literally the same type, which is what makes both adapters assignable to the router’s CompletionFn shape.

No runtime dependencies from claude.ts are imported (no error classes shared, no helpers shared). The adapter is fully self-contained at the runtime level.

2.2 Constants

const OPENAI_API_BASE_DEFAULT = 'https://api.openai.com/v1';
const OPENAI_PATH_CHAT = '/chat/completions';
const DEFAULT_MODEL = 'gpt-4o';
const DEFAULT_MAX_TOKENS = 1024;
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 100;

The DEFAULT_MODEL is the only model id literal in the entire module — and it is the default value injected when options.model is undefined. The caller is responsible for supplying 'gpt-4o-mini' (or any other variant) via options.model. This is the singleton-module-handles-the-family AC.

2.3 Error classes

export class OpenAiConfigError extends Error {
  readonly code = 'OPENAI_CONFIG_ERROR' as const;
  constructor(message: string) { super(message); this.name = 'OpenAiConfigError'; }
}

export class OpenAiApiError extends Error {
  readonly code: 'OPENAI_API_ERROR' | 'OPENAI_RETRIES_EXHAUSTED';
  readonly status: number | undefined;
  constructor(message: string, status?: number, exhausted = false) {
    super(message);
    this.name = 'OpenAiApiError';
    this.status = status;
    this.code = exhausted ? 'OPENAI_RETRIES_EXHAUSTED' : 'OPENAI_API_ERROR';
  }
}

2.4 Interfaces

export interface OpenAiTool {
  readonly name: string;
  readonly description: string;
  readonly input_schema: Record<string, unknown>;
}

export interface OpenAiCompletionOptions {
  readonly model?: string;
  readonly maxTokens?: number;
  readonly systemPrompt?: string;
  readonly apiKey?: string;
  readonly baseUrl?: string;
  readonly fetchFn?: typeof fetch;
  readonly logger?: (...args: unknown[]) => void;
  readonly delayFn?: (ms: number) => Promise<void>;
}

CompletionResult is re-exported from claude.js:

export type { CompletionResult } from '../../integrations/claude.js';

2.5 Helpers (file-internal)

function isRetryable(status: number): boolean { /* same as claude.ts */ }
function sleep(ms: number): Promise<void> { /* same as claude.ts */ }

/** Build the OpenAI Chat Completions request body. */
function buildRequestBody(
  prompt: string,
  model: string,
  maxTokens: number,
  systemPrompt?: string,
  tools?: ReadonlyArray<OpenAiTool>,
): Record<string, unknown>;

/** Wrap AnthropicTool[] into OpenAI function-call envelopes. */
function mapToolsForRequest(tools: ReadonlyArray<OpenAiTool>): Array<Record<string, unknown>>;

/** Normalize legacy function_call into a tool_calls-shaped array. */
function normalizeToolCalls(message: Record<string, unknown>): Array<Record<string, unknown>>;

/** Map an OpenAI tool_calls response into an Anthropic-shape content array. */
function mapToolUseResponse(toolCalls: Array<Record<string, unknown>>): Array<Record<string, unknown>>;

/** Parse the OpenAI Chat Completions response body into CompletionResult. */
function parseResult(json: any, startMs: number): CompletionResult;

2.6 Retry loop

async function attemptWithRetry(
  apiKey: string,
  baseUrl: string,
  requestBody: Record<string, unknown>,
  options: OpenAiCompletionOptions,
): Promise<CompletionResult> {
  // structurally identical to claude.ts attemptWithRetry, with:
  //   - URL: `${baseUrl}${OPENAI_PATH_CHAT}`
  //   - Authorization: `Bearer ${apiKey}`
  //   - Content-Type: 'application/json'
  //   - log prefix: '[openai]'
  //   - error class: OpenAiApiError
}

2.7 Public API

export async function createOpenAiCompletion(
  prompt: string,
  options: OpenAiCompletionOptions = {},
): Promise<CompletionResult>;

export async function createOpenAiCompletionWithTools(
  prompt: string,
  tools: ReadonlyArray<OpenAiTool>,
  options: OpenAiCompletionOptions = {},
): Promise<CompletionResult>;

Both:

  1. Resolve apiKey via options.apiKey ?? process.env['COLIBRI_OPENAI_API_KEY'].
  2. Throw OpenAiConfigError if missing.
  3. Resolve baseUrl via options.baseUrl ?? process.env['COLIBRI_OPENAI_BASE_URL'] ?? OPENAI_API_BASE_DEFAULT.
  4. Resolve model via options.model ?? DEFAULT_MODEL.
  5. Resolve maxTokens via options.maxTokens ?? DEFAULT_MAX_TOKENS.
  6. Build body via buildRequestBody(...).
  7. Delegate to attemptWithRetry(...).

3. Test file shape — src/__tests__/domains/router/adapters/openai.test.ts

3.1 Fixtures

const FAKE_API_KEY = 'sk-test-openai-fake-key';
const FAKE_MODEL = 'gpt-4o-test';
const FAKE_PROMPT = 'Say hello.';

const SUCCESS_RESPONSE = {
  id: 'chatcmpl-test-001',
  object: 'chat.completion',
  created: 1234567890,
  model: FAKE_MODEL,
  choices: [
    { index: 0, message: { role: 'assistant', content: 'Hello!' }, finish_reason: 'stop' },
  ],
  usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
};

const TOOL_USE_RESPONSE = {
  id: 'chatcmpl-test-002',
  model: FAKE_MODEL,
  choices: [
    {
      index: 0,
      message: {
        role: 'assistant',
        content: null,
        tool_calls: [
          {
            id: 'call_abc',
            type: 'function',
            function: {
              name: 'get_weather',
              arguments: '{"location":"London"}',
            },
          },
        ],
      },
      finish_reason: 'tool_calls',
    },
  ],
  usage: { prompt_tokens: 20, completion_tokens: 12, total_tokens: 32 },
};

const LEGACY_FUNCTION_CALL_RESPONSE = {
  id: 'chatcmpl-test-003',
  model: FAKE_MODEL,
  choices: [
    {
      index: 0,
      message: {
        role: 'assistant',
        content: null,
        function_call: {
          name: 'get_weather',
          arguments: '{"location":"Paris"}',
        },
      },
      finish_reason: 'function_call',
    },
  ],
  usage: { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 },
};

const SAMPLE_TOOL: OpenAiTool = {
  name: 'get_weather',
  description: 'Get the current weather',
  input_schema: {
    type: 'object',
    properties: { location: { type: 'string' } },
    required: ['location'],
  },
};

3.2 Mock helpers (mirror Phase 0 claude.test.ts)

makeMockFetch, makeSilentLogger, makeInstantDelay, baseOptions — all structurally identical to src/__tests__/domains/integrations/claude.test.ts helpers, with the response body fixtures swapped.

3.3 Test grouping

describe('createOpenAiCompletion — success path', ...)            // AC1, AC2, AC3
describe('createOpenAiCompletionWithTools — request shape', ...)  // AC4, AC5
describe('model selection', ...)                                  // AC6
describe('tool-use response mapping', ...)                        // AC7, AC8, AC9
describe('token + finish-reason mapping', ...)                    // AC10
describe('API key validation', ...)                               // AC11
describe('retry logic — 429', ...)                                // AC12, AC14
describe('retry logic — 5xx', ...)                                // AC13
describe('terminal errors — no retry', ...)                       // AC15, AC20
describe('logging', ...)                                          // AC16
describe('base URL configuration', ...)                           // AC17, AC18
describe('CompletionFn shape parity', ...)                        // AC19

4. File-disjointness guarantee

P1.5.2 (Kimi), P1.5.3 (Codex), P1.5.4 (OpenAI — this slice) all run in parallel in Wave 3. Their file footprints:

Slice Files written
P1.5.2 Kimi src/domains/router/adapters/kimi.ts + tests + 5 chain docs
P1.5.3 Codex src/domains/router/adapters/codex.ts + tests + 5 chain docs
P1.5.4 OpenAI (this) src/domains/router/adapters/openai.ts + tests + 5 chain docs
Wave 4 fold-in src/domains/router/index.ts (re-exports for all three)

There are zero file overlaps between the three parallel slices. The index.ts re-export is intentionally deferred to a coordinator fold-in.

5. Test count expectation

Base: 3153 tests across NN suites at 89adef66. Delta: + ~25 from new openai.test.ts. Expected post-PR: 3178 (3153 + 25).

6. Failure modes + recovery

Failure Likely cause Recovery
npm run build red on adapter TypeScript misalignment with CompletionResult Re-import CompletionResult from claude.js instead of redefining; ensure OpenAiTool is readonly on all fields.
npm run lint red unused import / any lint rule Mirror claude.ts’s // eslint-disable-next-line @typescript-eslint/no-explicit-any annotations on the response-body parser.
npm test red on full suite accidental mutation of shared state The adapter is pure (no module-scope state). Check that test fixtures use Object.freeze if shared between tests.
Pre-existing server-startup-smoke flake unrelated to this slice Re-run; documented as a known flake at 09d462f8 per memory.
Test mistakenly hits real OpenAI injected fetchFn not threaded through helper Use baseOptions({ fetchFn }) pattern from claude.test.ts; never call createOpenAiCompletion without fetchFn in tests.

7. Compile-time CompletionFn assignment proof

The test file will include:

import type { CompletionFn } from '../../../../domains/router/fallback.js';
import {
  createOpenAiCompletion,
  createOpenAiCompletionWithTools,
  type OpenAiTool,
} from '../../../../domains/router/adapters/openai.js';

// Compile-time parity assertion: if these assignments fail to typecheck,
// the adapter has drifted from CompletionFn and the build will fail.
const _completionFnParity: CompletionFn = createOpenAiCompletion;
const _completionFnWithToolsParity: CompletionFn = (prompt, opts) =>
  createOpenAiCompletionWithTools(prompt, [] as ReadonlyArray<OpenAiTool>, opts);

// Reference both to silence "unused variable" lint:
void _completionFnParity;
void _completionFnWithToolsParity;

8. Step 5 verification checklist (preview)

  • src/domains/router/adapters/openai.ts exists.
  • src/__tests__/domains/router/adapters/openai.test.ts exists.
  • src/domains/router/index.ts byte-identical to 89adef66.
  • All exports listed in §1.1 + §1.2 + §1.3 present.
  • AC1–AC20 each have a matching test.
  • npm run build green.
  • npm run lint green.
  • npm test green; test count rises by ≥ 5.
  • No process.stdout.write call anywhere in openai.ts.
  • No reference to AMS_* env var.
  • No MCP tool registration.
  • No literal gpt-4o-mini outside test fixtures (the adapter uses the caller-supplied options.model).
  • No index.ts mutation.

9. Writeback preview

task_update:
  task_id: P1.5.4
  status: done
  progress: 100

thought_record:
  task_id: P1.5.4
  branch: feature/p1-5-4-openai-adapter
  commits: [<sha1>, <sha2>, <sha3>, <sha4>, <sha5>]
  worktree: .worktrees/claude/p1-5-4-openai-adapter
  tests_run: ['npm run build', 'npm run lint', 'npm test']
  summary: "OpenAI adapter ships with GPT-4o + GPT-4o mini under a single
            module. Env COLIBRI_OPENAI_API_KEY + COLIBRI_OPENAI_BASE_URL.
            tool_calls response mapped into AnthropicTool shape (with
            JSON-parsing of arguments). options.model selects the
            specific GPT-4o variant. Legacy function_call accepted on
            response side. 25 parity tests green. index.ts untouched per
            Wave 3 file-disjoint constraint  re-export deferred to
            Wave 4 fold-in."
  blockers: []

End of packet.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.