import type { ApiKeyCredentials, ChatMessage, ModelInfo, ProviderContract, ProviderStreamOptions, ToolContract, } from "@dispatch/kernel"; import type { FetchLike } from "@dispatch/trace-replay"; import { listModels as fetchModels } from "./listModels.js"; import { streamChat } from "./stream.js"; /** * Generic factory for an OpenAI-compatible provider. A provider extension * supplies its own `id` (stamped on the ProviderContract + used in listModels * error labels) and an optional `transformBody` hook to add provider-specific * body fields (e.g. `reasoning_effort`) before the request is sent. The library * names no concrete feature — those knobs belong to the extension layer. */ export interface CreateOpenAICompatProviderOpts { readonly credentials: ApiKeyCredentials; readonly model: string; /** Provider id (was hardcoded "openai-compat"). Stamped on the ProviderContract.id * + used in listModels error labels. */ readonly id: string; /** * Internal injectable fetch — used by tests and replay mode. * When absent, falls back to globalThis.fetch (production default). */ readonly fetchFn?: FetchLike; /** * Optional hook a provider extension uses to add provider-specific body fields (e.g. * `reasoning_effort`) before the request is sent. Receives the body built so far + * the ProviderStreamOptions; returns ADDITIONAL fields to merge (or the full body). * Default (absent): no extra fields. Generic — the library names no feature. */ readonly transformBody?: ( body: Record, opts: ProviderStreamOptions, ) => Record; } export function createOpenAICompatProvider(opts: CreateOpenAICompatProviderOpts): ProviderContract { const baseURL = opts.credentials.baseURL ?? "https://opencode.ai/zen/go/v1"; const apiKey = opts.credentials.apiKey; const fetchFn = opts.fetchFn; const transformBody = opts.transformBody; const streamConfig = { baseURL, apiKey, model: opts.model, ...(fetchFn !== undefined ? { fetchFn } : {}), ...(transformBody !== undefined ? { transformBody } : {}), }; return { id: opts.id, stream: ( messages: readonly ChatMessage[], tools: readonly ToolContract[], streamOpts?: ProviderStreamOptions, ) => streamChat(streamConfig, messages, tools, streamOpts), listModels: (): Promise => fetchModels({ baseURL, apiKey, providerId: opts.id, ...(fetchFn !== undefined ? { fetchFn } : {}), }), }; }