/** * OpenRouter transport contracts. * * The client is deliberately thin: it POSTs a body the wire layer rendered, * parses SSE into `UpstreamChunk`s, and classifies failures. It holds no * routing policy — that lives in `router/`. */ import type { UpstreamChunk, WireError } from "../wire/types.ts"; export type UpstreamErrorKind = | "auth" | "rate_limit" /** Plan credits or a usage allowance exhausted (Ollama Cloud 402). Account-level, retryable elsewhere. */ | "quota" | "context_length" | "model_unavailable" | "invalid_request" | "moderation" | "upstream_error" | "timeout" | "network" | "aborted"; export class UpstreamError extends Error { constructor( readonly kind: UpstreamErrorKind, readonly status: number, message: string, readonly retryable: boolean, readonly body?: unknown, ) { super(message); this.name = "UpstreamError"; } toWireError(): WireError { return { status: this.status, code: this.kind, message: this.message }; } } export interface DispatchOptions { body: Record; /** Forwarded as the `x-session-id` header, mirroring body `session_id`. */ sessionId: string; /** * Per-turn credentials by upstream id (`NormRequest.upstreamKeys`). The * client dispatching this body prefers its own entry over the configured * `apiKey`, without ever writing to the shared config: concurrent turns * carry different tenants' keys over the same `UpstreamEntry`. */ upstreamKeys?: Readonly>; signal: AbortSignal; } /** * A live upstream generation. * * `chunks` is single-pass. The escalation guard may abandon it before * completion; callers MUST abort the signal in that case so the upstream * connection is torn down and no further tokens are billed. */ export interface Dispatch { chunks: AsyncIterable; /** Resolves once the generation id is known, i.e. on the first chunk. */ generationId(): Promise; } /** One tool call a model asked for, provider-shape normalised. */ export interface ToolCall { id: string; name: string; /** Parsed arguments; `{}` when the model sent malformed JSON (which is itself a finding). */ args: Record; /** True when `arguments` did not parse — graded as a schema failure, not a refusal. */ malformed: boolean; } export interface CompletionResult { text: string; costUsd: number | null; toolCalls: ToolCall[]; } export interface UpstreamClient { /** Streaming chat completion. Always requests `stream: true` upstream. */ dispatch(opts: DispatchOptions): Promise; /** * Non-streaming single-shot, used by the classifier adjudicator and the eval * harness. Returns assistant text, the reported cost, and any tool calls the * model asked for — the eval suite drives a real tool loop, which needs the * calls themselves and not a text description of them. */ complete(body: Record, signal: AbortSignal): Promise; /** Raw catalog fetch. Returns the parsed `data` array untouched. */ fetchModels(signal?: AbortSignal): Promise; /** * Key-scoped catalog fetch (`GET /models/user`). Returns the models * available to the configured key under active guardrails and preferences. */ fetchModelsForUser(signal?: AbortSignal): Promise; }