/** * CodexResponsesTransport — OpenAI Responses API transport (raw-fetch variant). * * Implements {@link LlmTransport} for the ChatGPT Codex backend * (`https://chatgpt.com/backend-api/codex/responses`) and any provider that * speaks a compatible Responses-API SSE protocol (xAI grok-via-responses). * * ## Wire shape (mirrored from @earendil-works/pi-ai 0.78.x — imports banned) * * Unlike the previous OpenAI-SDK-based implementation, this transport uses * **raw `fetch`** to control every request header. The Codex backend rejects * requests that carry the SDK's default `Accept: application/json` header or * that omit `store: false` in the body. * * Mandatory request headers: * - `Authorization: Bearer ` * - `chatgpt-account-id: ` * - `originator: codex_cli_rs` (set by buildCodexOAuthHeaders) * - `OpenAI-Beta: responses=experimental` * - `accept: text/event-stream` (always — backend mandates stream:true on every request) * - `content-type: application/json` * * Mandatory body fields: * - `model`, `input`, `instructions` * - `store: false` (codex backend rejects store:true or absent) * - `stream: true` (always — the Codex backend rejects stream:false) * * @module llm/transports/codex-responses * @task T11985 * @task T9311 * @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 5) * @see ADR-072 §LlmTransport — pure wire level */ import type { NormalizedDelta, TransportContext } from '@cleocode/contracts/llm/interfaces.js'; import type { LlmTransport, NormalizedResponse, TransportRequest } from '@cleocode/contracts/llm/normalized-response.js'; import type { ApiMode } from '@cleocode/contracts/llm/provider-id.js'; import type { ModelTransport } from '@cleocode/contracts/operations/llm.js'; /** * Options accepted by {@link CodexResponsesTransport}. * * `baseUrl` overrides the default endpoint. Required when serving xAI (use * `'https://api.x.ai/v1'`) or any other Responses-compatible shim. * * `defaultHeaders` carries extra HTTP headers merged into every request. * For Codex OAuth callers this should include at minimum * `Authorization`, `chatgpt-account-id`, and `originator` (built by * {@link buildCodexOAuthHeaders}). */ export interface CodexResponsesTransportOptions { /** * {@link ModelTransport} identifier for the logical provider this transport * serves. Returned verbatim from {@link CodexResponsesTransport.provider}. */ provider: ModelTransport; /** API key or bearer token. */ apiKey: string; /** Override base URL for non-OpenAI providers (no trailing slash). */ baseUrl?: string; /** Extra headers merged into every request. */ defaultHeaders?: Record; } /** * Transport for providers that expose the OpenAI Responses API via raw fetch. * * Uses native `fetch` (not the OpenAI SDK) so that the `Accept` header, * `store: false` body field, and `OpenAI-Beta` header can all be set * explicitly — the OpenAI SDK hardcodes `Accept: application/json` and omits * `store`, both of which cause the Codex ChatGPT backend to return a 400. * * SSE stream parsing mirrors the pi-ai 0.78.x reference implementation * (`@earendil-works/pi-ai/dist/providers/openai-codex-responses.js`) — * imports from that package are banned; the wire shape is transcribed here. * * @example * ```ts * const transport = new CodexResponsesTransport({ * provider: 'openai', * apiKey: process.env.CODEX_OAUTH_TOKEN!, * defaultHeaders: buildCodexOAuthHeaders(process.env.CODEX_OAUTH_TOKEN!), * }); * for await (const delta of transport.stream({ * model: 'gpt-5.5', * messages: [{ role: 'user', content: 'Reply with exactly: pong' }], * maxTokens: 64, * }, {})) { * process.stdout.write(delta.text); * } * ``` */ export declare class CodexResponsesTransport implements LlmTransport { /** * Provider identifier — mirrors the `provider` option passed at construction. * Matches a {@link ModelTransport} value for role-resolver routing. */ readonly provider: ModelTransport; /** * Wire protocol spoken by this transport — always `'codex_responses'`. * * @see ADR-072 §Type lock-in */ readonly apiMode: ApiMode; /** API key / OAuth bearer token. */ private readonly _apiKey; /** * Resolved endpoint URL (the full `/codex/responses` path). * Derived once at construction so the URL normalisation runs once. */ private readonly _endpointUrl; /** Extra headers merged into every request (auth, originator, etc.). */ private readonly _defaultHeaders; /** * Construct a CodexResponsesTransport. * * @param opts - Construction options: provider, API key, optional base URL * and default headers (auth + Cloudflare-bypass headers for OAuth callers). */ constructor(opts: CodexResponsesTransportOptions); /** * Execute a single (non-streaming) completion using the Responses API. * * The Codex ChatGPT backend mandates `stream: true` on every request and * returns `400 {"detail":"Stream must be set to true"}` for `stream: false`. * This method therefore always sends `stream: true` with * `Accept: text/event-stream`, internally consumes the SSE stream, and * aggregates text deltas + tool calls + usage into a {@link NormalizedResponse} * — identical to calling `stream()` and collecting all chunks. * * @param request - Provider-neutral request parameters. * @param _ctx - Transport context (unused; `request.signal` handles abort). * @returns Normalized response envelope. */ complete(request: TransportRequest, _ctx?: TransportContext): Promise; /** * Stream a completion using the Responses API SSE stream. * * Yields {@link NormalizedDelta} chunks from `response.output_text.delta` * events parsed from the raw SSE stream. The final delta carries * `stopReason` and `usage` from `response.completed`. * * Wire shape: * - `store: false` (required — codex backend rejects absent or true) * - `stream: true` * - `Accept: text/event-stream` * - `OpenAI-Beta: responses=experimental` * * @param request - Provider-neutral request parameters. * @param _ctx - Transport context (unused; `request.signal` handles abort). * @returns Async iterable of normalized delta chunks. */ stream(request: TransportRequest, _ctx: TransportContext): AsyncIterable; /** * Build the HTTP headers for a Codex request. * * Merges the auth/cloudflare headers from `_defaultHeaders` with the * mandatory Codex-specific headers: * - `OpenAI-Beta: responses=experimental` * - `content-type: application/json` * - `accept: text/event-stream` (always — both complete() and stream() use SSE) * * The Authorization header is injected only if not already present in * `_defaultHeaders` (OAuth callers pre-set it via buildCodexOAuthHeaders). * * @param streaming - Whether this is a streaming (SSE) or completion call. * @returns Plain string record of all request headers. */ private _buildHeaders; /** * Build the JSON request body for a Codex Responses API call. * * Mandatory fields per the Codex backend: * - `store: false` — backend rejects absent or `true`. * - `instructions` — system prompt (defaulting to empty string when absent). * - `input` — array of Responses API input items. * * @param request - Provider-neutral request. * @param streaming - Whether to enable SSE streaming. * @returns Serialisable request body. */ private _buildBody; } /** * Resolve the full Responses API endpoint URL from an optional base URL. * * Mirrors the `resolveCodexUrl` logic from pi-ai 0.78.x so the same base URL * variants work: * - `https://chatgpt.com/backend-api` → `.../codex/responses` * - `https://chatgpt.com/backend-api/codex` → `.../codex/responses` * - `https://chatgpt.com/backend-api/codex/responses` → unchanged * - `undefined` (no override) → ChatGPT Codex default * * For non-ChatGPT providers (xAI, custom shims) the raw baseUrl is * appended with `/responses` when it doesn't already end with `/responses`. * * @param baseUrl - Optional override base URL (no trailing slash). * @returns The fully resolved endpoint URL. */ export declare function resolveCodexUrl(baseUrl: string | undefined): string; //# sourceMappingURL=codex-responses.d.ts.map