import type { Capabilities, ProviderFactory, Request, StreamEvent, WireFamily } from '@wrongstack/core/types'; import { ProviderError } from '@wrongstack/core/types'; import { type HeadersLike } from './error-parse.js'; import type { BuildBodyContext } from './model-output-limits.js'; import { type SSEMessage, parseSSE } from './sse.js'; import { WireAdapter, type WireAdapterStreamOptions } from './wire-adapter.js'; /** * Declarative wire-format definition. Sufficient to add a new HTTP+SSE * provider without subclassing `WireAdapter` — the boilerplate (HTTP errors, * abort wiring, SSE body parsing) is shared. * * The shape covers the variation that actually matters between providers: * - URL template (path, query) * - Auth headers (x-api-key, Authorization, etc.) * - Request body (field names, system-prompt placement, tool format) * - SSE event translation (one wire event → 0+ canonical events) * * Anything more exotic (non-SSE streams, multipart bodies, OAuth flows) still * needs a hand-written subclass — those cases are too varied to template. * * `S` is provider-internal state threaded across SSE events for one stream: * accumulating partial tool-call JSON, tracking block kinds, carrying the * model id forward from `message_start`, etc. Each `stream()` call gets a * fresh `S` via `createStreamState`. */ export interface WireFormatConfig> { /** Provider id (matches catalog id when the provider is in models.dev). */ id: string; /** Wire family — used by the registry's factory list. */ family: WireFamily; capabilities: Capabilities; /** Used when the user doesn't override via config.baseUrl. */ defaultBaseUrl: string; /** Build the HTTPS endpoint. Receives the (possibly user-overridden) base URL. */ buildUrl(baseUrl: string, req: Request): string; /** Per-request headers. Default `content-type`/`accept` are provided already. */ buildHeaders(apiKey: string, req: Request): Record; /** Map a canonical Request onto the provider's body shape. * Receives the provider's resolved `Capabilities` and its id; pass both to * `resolveMaxOutputTokens(req, ctx)` rather than reading * `ctx.capabilities.maxOutput` directly. Capabilities are provider-scoped * and resolved once at boot, so they describe the model the session * started on — the catalog lookup keyed on `req.model` is what stays * correct across `/model` switches, fallback hops and subagents. */ buildBody(req: Request, ctx: BuildBodyContext): Record; /** Construct fresh per-stream state. Called once per `stream()` call. */ createStreamState(fallbackModel: string): S; /** * Translate one SSE event into 0+ canonical events. Mutating `state` is * expected — providers carry per-stream accumulators (partial tool JSON, * current model id, usage) here. */ parseStreamEvent(msg: SSEMessage, state: S): StreamEvent[]; /** * Optional: yield any final events after the upstream stream closes * (e.g. emit a synthetic `message_stop` when the wire format ends with * `[DONE]` instead of an explicit terminator). */ finalizeStream?(state: S): StreamEvent[]; /** * Optional: report whether the stream closed WITHOUT a terminal marker * (e.g. OpenAI's `[DONE]` / a `finish_reason`). A clean mid-stream FIN from * a proxy/LB idle timeout otherwise reaches `finalizeStream`, which happily * synthesizes a `message_stop` with the default `end_turn` — committing a * truncated response to history as if it finished. When this returns true, * `runStream` throws a retryable error instead so the agent loop retries. */ isTruncated?(state: S): boolean; /** Optional override; defaults to the shared HTTP error parser. `headers` * (when the fetch impl provides them) carries Retry-After hints — impls * may ignore it; the wire format backfills `body.retryAfterMs` either way. */ normalizeError?(status: number, body: string, headers?: HeadersLike): ProviderError; } /** * Concrete Provider built from a declarative config. Extends WireAdapter to * inherit the canonical HTTP + abort + error machinery. */ export declare class WireFormatProvider> extends WireAdapter { readonly id: string; readonly capabilities: Capabilities; private readonly cfg; constructor(cfg: WireFormatConfig, opts: { apiKey: string; baseUrl?: string | undefined; fetchImpl?: typeof fetch | undefined; streamOpts?: WireAdapterStreamOptions | undefined; }); protected buildUrl(req: Request): string; protected buildHeaders(req: Request): Record; protected buildBody(req: Request, ctx: BuildBodyContext): Record; protected parseStream(body: Parameters[0], fallbackModel: string): AsyncIterable; protected translateError(status: number, body: string, headers?: HeadersLike): ProviderError; private runStream; } /** * Identity helper that gives authors type checking on the config literal. * Use at module level: * * export const myProvider = defineWireFormat({ * id: 'mistral', * family: 'openai-compatible', * capabilities: { ... }, * ... * }); */ export declare function defineWireFormat>(cfg: WireFormatConfig): WireFormatConfig; export interface WireFactoryOptions { /** * Optional config-time override of the API key. When omitted, the factory * reads `cfg.apiKey` (passed in at create time by the registry / config * loader). Setting this here is useful in tests. */ apiKey?: string | undefined; /** Override the base URL at factory build time. */ baseUrl?: string | undefined; } /** * Build a `ProviderFactory` from a declarative wire-format. Plug into * `ProviderRegistry.register(...)` or use in `buildProviderFactoriesFromRegistry` * for catalog-driven discovery. */ export declare function createWireFormatFactory(cfg: WireFormatConfig, opts?: WireFactoryOptions): ProviderFactory; //# sourceMappingURL=wire-format.d.ts.map