/** * ChatCompletions transport — OpenAI-compatible workhorse for 16+ providers. * * Covers any provider that exposes an OpenAI-compatible * `chat.completions.create` endpoint: OpenRouter, DeepSeek, xAI/Grok, Groq, * Together, Fireworks, Kimi, Gemini-via-OpenAI-shim, Moonshot, and more. * * Port of Hermes `agent/transports/chat_completions.py` (~614 LOC). MVP slice: * - Base message + tool conversion (provider-neutral → OpenAI wire shape) * - Provider-quirk dispatch table (Gemini thinking config, Kimi reasoning_effort, * OpenRouter Pareto router, xAI x-grok-conv-id, Moonshot JSON schema sanitizer) * - NormalizedResponse mapping (content, toolCalls, stopReason, usage, raw) * * Deferred from MVP (documented as TODO): * - ProviderProfile hook callbacks (prepareMessages, buildExtraBody, * buildApiKwargsExtras) — the contract is now extended in W0c. Wire-up is * tracked as T-llm-p4-1d (Wave 1d — Moonshot + chat-completions quirks * consolidation moves inline quirks into per-provider profile hooks). * - Multi-turn tool replay (agent loop responsibility). * * W0c adds stub `stream()` + `apiMode` for compile parity with the extended * `LlmTransport` interface. Wave 1d migration (T-llm-p4-1d) replaces the stub * with a real streaming implementation. * * @module llm/transports/chat-completions * @task T9272 * @task T9282 (W0c — stub stream() + apiMode) * @epic T9261 (T-LLM-CRED-CENTRALIZATION Phase 3) */ 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 { ProviderProfile } from '@cleocode/contracts/llm/provider-profile.js'; import type { ModelTransport } from '@cleocode/contracts/operations/llm.js'; /** * Options accepted by {@link ChatCompletionsTransport}. * * `baseUrl` overrides the default endpoint — required for every non-OpenAI * provider that routes through an OpenAI-compatible shim (e.g. OpenRouter * at `https://openrouter.ai/api/v1`, Groq at `https://api.groq.com/openai/v1`). * * `defaultHeaders` carries extra HTTP headers merged into every SDK request. * Typically used for proxy auth (`X-Title`, `HTTP-Referer` for OpenRouter) * or for OAuth bearer tokens on providers that use `Authorization: Bearer`. * * `profile` is an optional {@link ProviderProfile} whose hooks * (`buildExtraBody`, `buildApiKwargsExtras`) are called during request * preparation. When absent, the transport falls back to the legacy inline * model-name quirk dispatch for backward compatibility. */ export interface ChatCompletionsTransportOptions { /** * `ModelTransport` identifier for the logical provider this instance serves. * * Returned verbatim from {@link ChatCompletionsTransport.provider} so that * role-resolver and factory code can route without inspecting the base URL. */ 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 SDK request. */ defaultHeaders?: Record; /** * Optional provider profile supplying `buildExtraBody` and * `buildApiKwargsExtras` hooks. When present, these hooks are dispatched * instead of the legacy inline model-name pattern quirks. */ profile?: ProviderProfile; } /** * Generic OpenAI-compatible transport. * * A single instance of this class can serve any provider that accepts the * OpenAI `chat.completions.create` wire shape. Provider-specific quirks are * applied inline by {@link ChatCompletionsTransport._applyProviderQuirks} via * a model-name pattern dispatch table, keeping the hot path minimal. * * Construction: `new ChatCompletionsTransport({ provider, apiKey, baseUrl?, defaultHeaders? })` * * @example * ```ts * // OpenRouter (16+ providers behind one base URL) * const transport = new ChatCompletionsTransport({ * provider: 'openai', * apiKey: process.env.OPENROUTER_API_KEY, * baseUrl: 'https://openrouter.ai/api/v1', * defaultHeaders: { 'HTTP-Referer': 'https://cleocode.dev' }, * }); * const response = await transport.complete({ * model: 'openrouter/anthropic/claude-sonnet-4', * messages: [{ role: 'user', content: 'Hello' }], * maxTokens: 512, * }); * ``` */ export declare class ChatCompletionsTransport implements LlmTransport { /** * Provider identifier — mirrors the `provider` option passed at construction. * Matches a {@link ModelTransport} value so role-resolver can select this * transport at runtime. */ readonly provider: ModelTransport; /** * Wire protocol spoken by this transport — always `'chat_completions'`. * * All providers served by this transport (OpenRouter, DeepSeek, xAI, Groq, * Moonshot, Gemini-via-shim, etc.) use the OpenAI chat completions wire format. * * @see ADR-072 §Type lock-in */ readonly apiMode: ApiMode; /** Underlying OpenAI-compatible SDK client. */ private readonly _client; /** * Optional provider profile — when present, its hooks replace the inline * model-name pattern quirks in `_applyProviderQuirks`. */ private readonly _profile; /** * Construct a ChatCompletionsTransport. * * @param opts - Construction options including provider, API key, and * optional base URL / default headers / provider profile. */ constructor(opts: ChatCompletionsTransportOptions); /** * Execute a single completion and return a normalized response. * * Maps the provider-neutral {@link TransportRequest} to the OpenAI * `chat.completions.create` wire shape, applies any provider-specific * quirks (Gemini, Kimi, Moonshot, OpenRouter, xAI), and normalizes the * raw SDK response into a {@link NormalizedResponse}. * * @param request - Provider-neutral request parameters. * @param _ctx - Transport context (request ID, abort signal). Currently unused; * `request.signal` takes precedence for abort support. * @returns Normalized response envelope. */ complete(request: TransportRequest, _ctx?: TransportContext): Promise; /** * Convert provider-neutral messages + optional system prompt to OpenAI * wire-format message array. * * When `request.system` is set it is prepended as a `{ role: 'system' }` * message, which is the canonical OpenAI representation understood by all * OpenAI-compatible providers. * * Multimodal content blocks (W0c — {@link TransportMessage.content} union): * when `content` is an array, text blocks are concatenated and image blocks * are dropped (this transport currently operates in `'text'`-equivalent mode). * Wave 1d wires `request.imageMode` and ProviderProfile hooks to control this. * * TODO(T9272+/W1d): wire ProviderProfile.prepareMessages + imageMode routing * once Wave 1d migration lands. * * @param request - Full transport request. * @returns Array of OpenAI-compatible message objects. */ private _convertMessages; /** * Convert provider-neutral tool definitions to OpenAI function-calling wire * format (`{ type: "function", function: { name, description, parameters } }`). * * @param tools - Provider-neutral tool definitions. * @returns OpenAI-format tool objects. */ private _convertTools; /** * Apply provider-specific request transformations in place. * * When a {@link ProviderProfile} was supplied at construction time, its * `buildExtraBody` and `buildApiKwargsExtras` hooks are called and the * results merged into `kwargs` — this is the canonical W1d path. * * When no profile is present the legacy inline model-name pattern dispatch * is used as a fallback for backward compatibility with callers that have * not yet been migrated to supply a profile. * * Profile hook dispatch (W1d): * - `buildExtraBody` → merged into `kwargs.extra_body` (shallow merge). * Special key `__sanitizedTools` replaces `kwargs.tools` (Moonshot * schema sanitization path). * - `buildApiKwargsExtras` → shallow-merged into `kwargs` at top level. * Nested `extra_headers` is merged (not replaced) with any existing headers. * * Legacy inline quirks (fallback): * - **Gemini** (`gemini`): `extra_body.thinking_config` with model-aware budget. * - **Kimi** (`kimi`): `reasoning_effort: 'high'` + `extra_body.thinking`. * - **Moonshot** (`moonshot`): shallow tool schema sanitization. * - **OpenRouter Pareto** (`openrouter/` + Sonnet/Opus/Grok/GPT-4): plugins. * - **xAI/Grok** (`grok`): `extra_headers['x-grok-conv-id']`. * * @param kwargs - Mutable request kwargs dict (mutated in place). * @param request - Original transport request for model name + tool list. */ private _applyProviderQuirks; /** * Normalize an OpenAI `ChatCompletion` response into a * {@link NormalizedResponse}. * * Mapping rules: * - `content` — first choice message text (null when only tool_calls present). * - `toolCalls` — mapped from `tool_calls` array (null when none). * - `stopReason` — `finish_reason` or `'stop'` as fallback. * - `usage` — `prompt_tokens` → `inputTokens`, `completion_tokens` → `outputTokens`. * - `cachedTokens` — populated from `usage.prompt_tokens_details.cached_tokens` * when present (Anthropic-via-OpenAI-shim, some OpenRouter models). * - `raw` — unmodified SDK response object. * * @param response - Raw OpenAI SDK ChatCompletion. * @param requestedModel - Model string from the originating request. * @returns Normalized response envelope. */ private _normalize; /** * Stream a completion against the OpenAI-compatible chat completions endpoint. * * Requests a server-sent-events stream via `stream: true` + `stream_options: * { include_usage: true }`. Each SSE chunk carrying `delta.content` is routed * through {@link StreamingThinkScrubber} so that `` blocks * emitted by reasoning models (e.g. o1-series, DeepSeek-R1) are separated from * visible text before being yielded as {@link NormalizedDelta}. The final delta * carries `stopReason` and `usage`. * * Tool-call streaming: when a chunk carries `delta.tool_calls[i]`, the * transport emits `toolCallDelta` entries so consumers can accumulate partial * argument JSON in real time. Sequence per tool call index: * 1. First chunk for index `i` (`tool_calls[i].function.name` present) → yields * `toolCallDelta` with `{ index, name, argumentsChunk }`. * 2. Subsequent chunks (name absent) → yields `toolCallDelta` with * `{ index, argumentsChunk }` containing the partial JSON fragment. * * Provider quirks (`_applyProviderQuirks`) are applied to the request kwargs * before the stream is opened, matching the behaviour of {@link complete}. * * @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; } //# sourceMappingURL=chat-completions.d.ts.map