import { z, ZodIssue, ZodError } from 'zod'; import { ObservabilityContext } from '@llm-ports/observability-contract'; /** * Multimodal content block types. * * Replaces the legacy `content: string` shape used by older LLM SDKs. * String content is syntactic sugar for `[{ type: "text", text: "..." }]`; * adapters accept either form and normalize internally. * * See docs/concepts/content-blocks for the full design rationale. */ /** Either a plain string (sugar) or an array of typed content blocks. */ type MessageContent = string | ContentBlock[]; /** Discriminated union of all supported content block kinds. */ type ContentBlock = TextBlock | ImageBlock | AudioBlock | ToolUseBlock | ToolResultBlock; /** Plain text content. */ interface TextBlock { type: "text"; text: string; } /** Image input (vision-capable models). */ interface ImageBlock { type: "image"; source: ImageSource; } type ImageSource = { kind: "base64"; mediaType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"; data: string; /** * Cost-vs-fidelity hint for the OpenAI vision pipeline. * - `"auto"` (default): provider decides based on image size. * - `"low"`: ~85 tokens regardless of image size; suitable for triage * and broad classification. * - `"high"`: full per-tile analysis (~170 tokens per 512×512 tile); * needed for OCR, fine-grained reasoning, small text. * * Honored by `@llm-ports/adapter-openai` (forwarded to OpenAI's * `image_url.detail`). Other adapters ignore the field — Anthropic and * Ollama don't have an equivalent knob. */ detail?: "auto" | "low" | "high"; } | { kind: "url"; url: string; /** See base64 variant. Same semantics. */ detail?: "auto" | "low" | "high"; }; /** Audio input (audio-capable models). */ interface AudioBlock { type: "audio"; source: AudioSource; } type AudioSource = { kind: "base64"; mediaType: "audio/wav" | "audio/mp3" | "audio/ogg"; data: string; } | { kind: "url"; url: string; }; /** Tool/function call request emitted by the model. */ interface ToolUseBlock { type: "tool_use"; /** Unique id for this tool call within the conversation. */ id: string; /** Name of the tool the model wants to invoke. */ name: string; /** Arguments the model is passing to the tool. Shape depends on the tool's input schema. */ input: unknown; } /** Tool/function call result, supplied back to the model. */ interface ToolResultBlock { type: "tool_result"; /** Must match the `id` of the corresponding ToolUseBlock. */ toolUseId: string; /** Result of executing the tool. May be a string, JSON, or further content blocks. */ content: string | ContentBlock[]; /** Set true if the tool execution failed; lets the model know to recover. */ isError?: boolean; } /** * Budget and cost gating types. * * Two pluggable backends, both optional. Most production users will use * the in-memory implementation in dev and a Redis-backed one (in a separate * package) in production. * * See implementation plan v3 §6.6. */ /** * Five-tier scope hierarchy. The caller passes one of these in * `budgetScope?: { scope, scopeId }` on any per-call options interface * to make the gating storage key per-tenant / per-customer / per-user / * per-agent / per-session instead of just per-alias. * * Tier semantics (from outermost to innermost): * - "tenant" — the billing org. * - "customer" — the end-customer of a multi-tenant SaaS. * - "user" — an individual identity. * - "agent" — a logical agent (workflow, automation, persona). * - "session" — a bounded run. * * Shipped in 0.1.0-alpha.20. The Registry composes storage keys as * `${alias}|${scope}:${scopeId}` when budgetScope is set; backwards * compatible: existing callers who omit budgetScope see identical * behavior to alpha.19.1. */ type BudgetScope = "tenant" | "customer" | "user" | "agent" | "session"; /** * Per-call hint identifying which scope this call belongs to. Set on any * of the five request option types to make gating storage per-scope. */ interface BudgetScopeRef { scope: BudgetScope; scopeId: string; } /** * Public type describing a single budget gate the caller can declare * programmatically (the documented shape for the next-level budget * surface shipping in beta.2 alongside the persistent BudgetBackend). * In alpha.20 the env-driven gates are still authoritative. */ interface BudgetGate { scope: BudgetScope; scopeId: string; limitUsd: number; window: "minute" | "hour" | "day" | "month" | "session"; onExceed: "throw" | "downgrade" | "queue"; } /** * Request-count gating. `kind: "requests"` carries optional per-window caps; * any combination is allowed, and the first cap to trip blocks the call. * * `requestsPerHour` exists for backwards compatibility with alpha.19 — the * `parseGating` token `req:N/hour` still writes that field. Backends * consume `perHour` if set, else fall back to `requestsPerHour`. */ type BudgetLimit = { kind: "requests"; /** * @deprecated alpha.20 — use `perHour` instead. Still populated by * `parseGating` when it sees `req:N/hour` so existing env configs * keep working. Backends fall back to it when `perHour` is unset. */ requestsPerHour?: number; /** Request ceiling per rolling minute window. (alpha.20+) */ perMinute?: number; /** Request ceiling per rolling hour window. (alpha.20+) */ perHour?: number; /** * Request ceiling per CostSession. (alpha.20+) Only enforced when a * CostSession is open; ignored on direct port calls. */ perSession?: number; } | { kind: "unlimited"; }; interface BudgetCheckResult { allowed: boolean; /** Current request count in the active window. */ current: number; /** Limit value (Infinity if unlimited). */ limit: number; /** Reason populated when allowed=false. */ reason?: string; } interface BudgetBackend { /** Increment the request counter for this provider alias. */ recordRequest(alias: string): Promise; /** Check whether another request would exceed the configured limit. */ check(alias: string, limit: BudgetLimit): Promise; } type CostLimit = { kind: "usd"; /** USD ceiling per rolling minute. (alpha.20+) */ perMinute?: number; /** USD ceiling per rolling hour. */ perHour?: number; /** USD ceiling per rolling day (24h). */ perDay?: number; /** USD ceiling per rolling 30-day window. */ perMonth?: number; /** * USD ceiling per CostSession. (alpha.20+) Only enforced when a * CostSession is open; ignored on direct port calls. */ perSession?: number; } | { kind: "unlimited"; }; interface CostCheckResult { allowed: boolean; /** Current spend in the most-restrictive active window, in USD. */ current: number; /** Limit value in USD (Infinity if unlimited). */ limit: number; reason?: string; } interface CostBackend { /** Add USD cost to this provider alias's running totals. */ recordCost(alias: string, usd: number): Promise; /** Check whether another request would exceed any configured cost ceiling. */ check(alias: string, limit: CostLimit): Promise; } /** * Token / tool-call ceilings that apply within a single CostSession. The * env-driven `parseGating` tokens `total_tokens:N/session` and * `tool_calls:N/session` write to this shape. Only enforced when a * CostSession is open; ignored on direct port calls. */ interface SessionGrainLimits { /** Total tokens (input + output, including reasoning + cache) per session. */ totalTokensPerSession?: number; /** Tool / function calls per session (runAgent only). */ toolCallsPerSession?: number; } /** * Pricing for one model, in USD per 1M tokens. * Adapters ship pricing tables in `pricing.ts`; users override via registry config. */ interface ModelPricing { inputPer1M: number; outputPer1M: number; /** Anthropic-style prompt caching read price (typically much lower). */ cacheReadPer1M?: number; /** Anthropic-style prompt caching write price. */ cacheWritePer1M?: number; /** Embedding-only models: cost per 1M input tokens (no output). */ embeddingPer1M?: number; /** * Model-specific capability flags. Adapters consult these to adapt request * shape — the right pipeline for an older chat model isn't the same as the * right one for a reasoning model. Defaults assume "standard chat": * full temperature range, separate system message, JSON mode, streaming, * tool use, optional vision. Setting a flag to a non-default tells the * adapter to take a divergent path. * * Unknown future models without flags use the default assumptions plus a * runtime error fallback in the adapter (e.g. catch a temperature rejection * and retry without it). */ capabilities?: ModelCapabilities; } /** * Model-specific behavior flags. Each flag describes what a model can do * differently from the standard chat-completion baseline. Adapters use these * to choose the right pipeline (standard chat, reasoning, etc.) per model. */ interface ModelCapabilities { /** * Reasoning models (OpenAI o1/o3 family, gpt-5-nano) reject any non-default * temperature value. When true, the adapter omits `temperature` from the * request entirely, regardless of what the user set or what defaults the * capability factory chose. */ temperatureLocked?: boolean; /** * Some reasoning models reject custom system messages. When true, the adapter * folds instructions into the user message instead of sending a separate * `system` field. (OpenAI o1-preview historically; later versions accept * system messages.) */ systemMessageInUserOnly?: boolean; /** * Native JSON mode supported (e.g. OpenAI's `response_format: json_object`, * Ollama's `format: "json"`). When false or undefined, structured-output * capabilities use prompted JSON instead of the native mode. Defaults vary * by adapter — typically true for major chat models, false for reasoning * models that don't pair JSON mode with their reasoning pipeline. */ jsonMode?: boolean; /** Streaming supported. Default: true. */ streaming?: boolean; /** Tool/function calling supported. Default: true. */ toolUse?: boolean; /** Vision input supported. Default: false (text-only is the historical baseline). */ vision?: boolean; /** * Reasoning model: the provider's "max output tokens" parameter caps * BOTH the model's internal reasoning chain-of-thought AND the visible * output. With a small budget, the model can spend all of it on reasoning * and emit zero visible text. Adapters that detect this expand the budget * automatically — see {@link reasoningHeadroomMultiplier} below for the * factor applied. Adapters typically learn this flag at runtime from * the first response that reports reasoning_tokens > 0. */ reasoningModel?: boolean; /** * For reasoning models, the multiplier applied to the user's * `maxOutputTokens` to produce the value sent to the provider. Defaults * to 10 (chosen so that a request for 20 visible output tokens gets a * 200-token total budget, leaving 180 for reasoning). Override per * model if you have specific knowledge of its reasoning intensity. */ reasoningHeadroomMultiplier?: number; } /** * LLMPort interface — the SDK-independent surface that business logic * depends on. All adapters implement this interface. * * The five methods correspond to the five primitive operations every major * LLM provider exposes: text generation, structured output, agent tool-use * loops, streaming text, and streaming structured output. * * Embeddings are intentionally split into a sibling EmbeddingsPort (see * embeddings-port.ts) because most chat adapters do not implement them * and most embedding-only adapters do not implement chat. * * See implementation plan v3 §6.2. */ /** * Task type. Free-form string; users define their own vocabulary. * For type-safe usage with autocomplete, see `declareTasks()`. */ type TaskType = string; /** Priority tier. 0 = critical (bypasses budget gating); 3 = low. */ type LLMPriority = 0 | 1 | 2 | 3; /** * A caller-owned identifier for a versioned or identifiable artifact that * should be attributed to this call: a prompt, a scaffold, a policy, a tool * schema, a cost-attribution tag, an experiment variant, a session id, a * tenant, whatever the consumer versions or tracks. * * Every field is optional. Callers set the ones that make sense for their * artifact identity. Common conventions the ecosystem is converging on: * - `key` — human-readable canonical name (`"team-dev.materialize"`) * - `version` — integer, semver, git SHA, timestamp — whatever the * consumer's versioning scheme uses * - `hash` — content hash for tamper-evidence (`sha256` recommended) * - `meta` — free-form bag for consumer-specific attribution * * llm-ports does NOT enforce shape, vocabulary, or content. Refs are * consumer-owned trace metadata that flow through to observability events * unchanged. * * Added in `0.1.0-alpha.25`. */ interface ArtifactRef { /** Human-readable identifier — the artifact's canonical name. Optional. */ key?: string; /** Version — integer, semver, git SHA, timestamp, whatever the consumer uses. */ version?: string | number; /** Content hash for tamper-evidence and correlation. sha256 recommended but not enforced. */ hash?: string; /** Free-form metadata for consumer-specific attribution. */ meta?: Record; } type MessageRole = "system" | "user" | "assistant" | "tool"; interface LLMMessage { role: MessageRole; content: MessageContent; } /** A tool the model may invoke during runAgent. */ interface ToolDefinition { name: string; description: string; inputSchema: TParams; execute: (input: z.infer) => Promise; /** Signals "this writes/deletes state". Used by createAgent to gate execution. */ destructive?: boolean; /** When true, agent must obtain user approval before execution. */ requiresConfirmation?: boolean; /** Truncate tool output to prevent context flooding. */ maxOutputBytes?: number; } interface TokenUsage { inputTokens: number; /** * Tokens emitted by the model. For "standard" chat models, this is the * visible response length. For reasoning models (OpenAI o-series, gpt-5-nano, * etc.), this INCLUDES reasoning_tokens — the model's internal chain-of- * thought tokens that don't appear in `text` but are billed at the output * rate. Use {@link reasoningTokens} to break out the reasoning portion. */ outputTokens: number; totalTokens: number; /** Tokens read from prompt cache (Anthropic + OpenAI feature). */ cacheReadTokens?: number; /** Tokens written to prompt cache (Anthropic explicit caching). */ cacheWriteTokens?: number; /** * Reasoning tokens (subset of outputTokens). Only populated for reasoning * models that report it via the provider API. When set, visible output * tokens = outputTokens - reasoningTokens. Useful for cost attribution * and for diagnosing "model used all the budget on thinking" cases. */ reasoningTokens?: number; /** * Rerank-specific billing unit. Set by `RerankPort` adapters whose * provider bills per "search unit" rather than per token (e.g. Cohere * Rerank: 1 search unit = 1 query of ≤100 documents). Adapters whose * rerank API bills in tokens populate `inputTokens` instead. * (alpha.17+) */ searchUnits?: number; /** * Count of documents reranked in this call. Telemetry only; not used * for billing. Populated by `RerankPort` adapters; undefined for * LLMPort and EmbeddingsPort calls. (alpha.17+) */ rerankedDocuments?: number; } interface CostUsage { inputUSD: number; outputUSD: number; totalUSD: number; /** * USD saved on this call by hitting prompt cache, vs. paying the full * input rate for the cached tokens. Populated whenever the provider * returns cache telemetry (cacheReadTokens > 0). Aligns with * OpenInference `llm.cost.cache_savings` and Helicone's "savings" * vocabulary used in their dashboards. * * Renamed from `cacheDiscountUSD` in alpha.19 (BREAKING). The previous * name implied a vendor-applied discount; "savings" better reflects * that this is the caller-visible reduction in their bill regardless * of how the provider books it on its side. See CHANGELOG alpha.19. */ cacheSavingsUSD?: number; } /** * Provider-neutral cache configuration for a single call. Locks the * shape across the three caching patterns the major providers expose: * * - Anthropic's explicit `cache_control` markers on message blocks. * - OpenAI's implicit, automatic prompt cache (no opt-in / opt-out). * - Google Gemini's pre-created `CachedContent` handle pattern. * * `mode` selects which pattern to engage for this call: * * - `"auto"`: let the adapter decide. Anthropic places a cache_control * marker at the last static block when one is identifiable. OpenAI * is a no-op (implicit caching is always on). Google is a no-op * unless `cachedContentHandle` is supplied. * * - `"manual"`: caller supplies explicit `breakpoints`. Anthropic * places cache_control at the named positions. OpenAI is a no-op * (cannot influence the implicit cache). Google is a no-op. * * - `"preCreated"`: caller supplies a `cachedContentHandle` returned * from a previous `createCachedContent` call. Google uses the * handle to serve the cached content. Anthropic + OpenAI are * no-ops. * * - `"off"`: strip cache_control from Anthropic message blocks for * this call only. OpenAI + Google are no-ops (no API to disable * their caching). * * `namespace` partitions cache lookups by tenant or customer when the * proxy in front of the provider supports it (e.g. Helicone's * `Cache-Seed` header). The adapter forwards it where supported and * ignores it elsewhere; it never changes the provider request body. * * Stability: SHAPE LOCKED in alpha.19. Per-mode adapter behaviors will * mature across beta minors (full breakpoint placement, Gemini handle * lifecycle, OpenAI proxy namespace forwarding) without breaking the * shape. * * See `docs/concepts/cache.md` for the per-provider behavior table. */ interface CacheControl { /** * Which caching pattern to engage for this call. * * - `"auto"` — adapter decides per provider; sane default for most callers. * - `"manual"` — caller supplies explicit `breakpoints` (Anthropic). * - `"preCreated"` — caller supplies a `cachedContentHandle` (Google). * - `"off"` — caller opts out where the provider allows (Anthropic). */ mode: "auto" | "manual" | "preCreated" | "off"; /** * TTL in seconds for cache entries created by this call. Anthropic * accepts the discrete values `300` (5 minutes) and `3600` (1 hour); * other values fall back to `300`. Google Gemini accepts an arbitrary * positive value subject to the provider's minimum. Ignored by OpenAI. */ ttlSeconds?: number; /** * Explicit cache_control placement for `mode: "manual"`. Each entry * names a position in the message stack where the adapter should * insert a cache_control marker. `at` selects the section; `index` * picks a specific block within that section (0-based) when the * section has more than one block. * * Only honored by adapters that support explicit breakpoints * (currently Anthropic). Silently ignored elsewhere. */ breakpoints?: Array<{ at: "tools" | "system" | "message-index"; index?: number; }>; /** * Pre-created cache handle for `mode: "preCreated"`. Returned by a * previous `createCachedContent` call (Google Gemini). The adapter * sends the handle as the source of the cached content for this * call. Required for `mode: "preCreated"` on Google; silently * ignored elsewhere. */ cachedContentHandle?: string; /** * Per-tenant cache partition key. When the request flows through a * caching proxy that supports partition keys (e.g. Helicone's * `Cache-Seed` header), the adapter forwards this value verbatim. * Adapters without proxy-aware caching ignore the field. Never * changes the provider request body. */ namespace?: string; } /** * `signal?: AbortSignal` is supported on every options interface in * `0.1.0-alpha.6` and later. When supplied: * * 1. The adapter checks `signal.aborted` at entry; if already aborted, * it throws `signal.reason` (or a generic `AbortError`) without * invoking the provider SDK. * 2. The adapter threads the signal through to the underlying SDK call * (OpenAI, Anthropic, Ollama, Vercel, Google), so an in-flight * provider HTTP request is cancelled on `controller.abort()` instead * of leaking the cost. * * Cancellation semantics are best-effort and per-adapter; some SDKs return * a typed `AbortError`, others reject with the original `signal.reason`. * Callers should `catch` and inspect the error rather than assume one shape. * * `forceProviderAlias?: string` (alpha.7+) overrides the task-routing chain * for this call only. The registry routes directly to the named provider * alias, skipping the `LLM_TASK_ROUTE_*` lookup. Per-provider budget gates * still apply (so `forceProviderAlias` can't be used to bypass a hard cap); * runtime fallback also does NOT engage — if the forced provider fails, the * error propagates. Useful for UIs where the operator picks a specific * provider, or for one-off "use the expensive model for this single call" * patterns. * * `reasoningEffort?: "low" | "medium" | "high"` (alpha.12+) controls how many * tokens reasoning models spend on hidden chain-of-thought before producing * visible output. Forwarded as the `reasoning_effort` field on OpenAI-shape * requests — applies to OpenAI's `o3`/`o4-mini`/`gpt-5-nano`/`gpt-5` family * and to OpenAI-compat providers that honor the parameter (notably Groq's * `openai/gpt-oss-120b`). Silently ignored by adapters whose providers don't * have an equivalent (anthropic, ollama, google, vercel) — the call still * succeeds, just with the provider's default effort level. * * `providerExtras?: Record` (alpha.16+) is a per-call escape * hatch for provider-specific request fields not (yet) modeled on the port. * Adapters that implement it shallow-merge the field into the SDK request * body AFTER the typed port fields are set, so callers can override * port-modeled defaults if they need to. Field semantics are provider- * specific and not validated by the port. Common patterns documented in * each adapter's docs (e.g. `adapter-openai` covers vLLM `chat_template_ * kwargs`, SGLang `regex`, vLLM `guided_json` for non-strict structured * decoding). Generic on purpose — keeps the public type signature vendor- * neutral while letting users reach any per-server / per-model knob. */ interface GenerateTextOptions { taskType: TaskType; priority?: LLMPriority; /** * The canonical input shape (alpha.26+). A sequence of chat messages with * explicit roles that flow through to the provider's chat-completions * endpoint natively. Supports multi-turn conversations, multiple system * messages, and mid-conversation context — anything the underlying protocol * models. Every adapter passes the array through with per-provider * translation (Anthropic splits system into a top-level field, Google uses * `systemInstruction`, OpenAI keeps system inline). * * Construct via `[sys(text), usr(content)]`, or migrate the legacy shape * via `toMessages(instructions, prompt)`. See * `docs/migration/alpha-25-to-alpha-26.md` for the full story. * * When both `messages` and the legacy `{ instructions, prompt }` are set, * the Registry throws `MessagesConflictError` — ambiguity is a caller bug * worth surfacing. */ messages: LLMMessage[]; maxOutputTokens?: number; temperature?: number; /** Cancellation signal threaded through to the provider's HTTP fetch. */ signal?: AbortSignal; /** Override task routing for this call only; route directly to the named provider alias. Per-provider budget gates still apply. (alpha.7+) */ forceProviderAlias?: string; /** Reasoning effort hint for o-series / gpt-5-nano / Groq gpt-oss-120b. Silently ignored by adapters whose providers don't honor it. (alpha.12+) */ reasoningEffort?: "low" | "medium" | "high"; /** Per-call escape hatch for provider-specific request fields not modeled on the port. Shallow-merged into the SDK request body after typed fields. (alpha.16+) */ providerExtras?: Record; /** Provider-neutral cache configuration. Shape locked in alpha.19. */ cacheControl?: CacheControl; /** * Per-call scope hint. When set, the Registry hashes (scope, scopeId) * into the gating storage key so configured caps apply per-scope rather * than per-alias. Omitting it preserves alpha.19.1 per-alias behavior. * (alpha.20+) */ budgetScope?: BudgetScopeRef; /** * Reference tags flowing through to observability events. (alpha.25+) * * Consumer-owned, keyed map of ArtifactRefs — each ref describes an * artifact whose identity should be attributed to this call: a prompt, * a scaffold, a policy, a tool schema, a cost-attribution tag, an * experiment variant, a session id — anything the consumer versions, * tags, or wants stamped onto trace. * * Refs are observability-only: * - They flow through to the `refs` field on onCost / onTokenUsage / * onFallback / onCacheHit / onValidationRetry events. * - NOT sent to the model. * - NOT persisted anywhere by the library. * - NOT validated (empty object is legal; missing keys are legal). * * llm-ports enforces no vocabulary — consumer picks the keys. Common * conventions the ecosystem is converging on: `prompt`, `scaffold`, * `policy`, `tool_schema`, `model_config`, `experiment`, `session`, * `tenant`, `env`, `deploy`. */ refs?: Record; } interface GenerateStructuredOptions { taskType: TaskType; priority?: LLMPriority; /** Canonical alpha.26+ input. See `GenerateTextOptions.messages`. */ messages: LLMMessage[]; schema: z.ZodType; /** Hint for the model about what the schema represents. */ schemaName?: string; maxOutputTokens?: number; temperature?: number; /** Cancellation signal threaded through to the provider's HTTP fetch. */ signal?: AbortSignal; /** Override task routing for this call only; route directly to the named provider alias. Per-provider budget gates still apply. (alpha.7+) */ forceProviderAlias?: string; /** Reasoning effort hint for o-series / gpt-5-nano / Groq gpt-oss-120b. Silently ignored by adapters whose providers don't honor it. (alpha.12+) */ reasoningEffort?: "low" | "medium" | "high"; /** Per-call escape hatch for provider-specific request fields not modeled on the port. Shallow-merged into the SDK request body after typed fields. (alpha.16+) */ providerExtras?: Record; /** Provider-neutral cache configuration. Shape locked in alpha.19. */ cacheControl?: CacheControl; /** * Per-call scope hint. When set, the Registry hashes (scope, scopeId) * into the gating storage key so configured caps apply per-scope rather * than per-alias. Omitting it preserves alpha.19.1 per-alias behavior. * (alpha.20+) */ budgetScope?: BudgetScopeRef; /** * Per-call override for strict-schema response_format mode. (alpha.21+) * * - `true` → force strict `response_format: { type: "json_schema", strict: true }` * - `false` → force classic `response_format: { type: "json_object" }` * - undefined → use the adapter's existing default (auto-detected per baseURL * allowlist, or whatever `useStrictResponseFormat` was set to at construction) * * Adapters that do not implement strict mode (or whose backing provider * doesn't support it) MUST silently ignore this hint rather than throw. * * Use case: a registry has one adapter alias per provider, but a single * caller knows the schema for THIS call carries `z.record(...)` (open * dictionary, strict-incompatible) and wants to drop to `json_object` for * this call; or the caller knows the schema is closed-shape and wants to * force strict to eliminate retry tails on cheap-tier providers where the * adapter's auto-detect defaulted to `json_object`. See llm-ports#46. */ strict?: boolean; /** Consumer-owned artifact reference tags flowing through to observability events. See `GenerateTextOptions.refs`. (alpha.25+) */ refs?: Record; } interface StreamTextOptions { taskType: TaskType; priority?: LLMPriority; /** Canonical alpha.26+ input. See `GenerateTextOptions.messages`. */ messages: LLMMessage[]; maxOutputTokens?: number; temperature?: number; /** Cancellation signal threaded through to the provider's HTTP fetch. */ signal?: AbortSignal; /** Override task routing for this call only; route directly to the named provider alias. Per-provider budget gates still apply. (alpha.7+) */ forceProviderAlias?: string; /** Reasoning effort hint for o-series / gpt-5-nano / Groq gpt-oss-120b. Silently ignored by adapters whose providers don't honor it. (alpha.12+) */ reasoningEffort?: "low" | "medium" | "high"; /** Per-call escape hatch for provider-specific request fields not modeled on the port. Shallow-merged into the SDK request body after typed fields. (alpha.16+) */ providerExtras?: Record; /** Provider-neutral cache configuration. Shape locked in alpha.19. */ cacheControl?: CacheControl; /** * Per-call scope hint. When set, the Registry hashes (scope, scopeId) * into the gating storage key so configured caps apply per-scope rather * than per-alias. Omitting it preserves alpha.19.1 per-alias behavior. * (alpha.20+) */ budgetScope?: BudgetScopeRef; /** Consumer-owned artifact reference tags flowing through to observability events. See `GenerateTextOptions.refs`. (alpha.25+) */ refs?: Record; } interface StreamStructuredOptions { taskType: TaskType; priority?: LLMPriority; /** Canonical alpha.26+ input. See `GenerateTextOptions.messages`. */ messages: LLMMessage[]; schema: z.ZodType; schemaName?: string; maxOutputTokens?: number; temperature?: number; /** Cancellation signal threaded through to the provider's HTTP fetch. */ signal?: AbortSignal; /** Override task routing for this call only; route directly to the named provider alias. Per-provider budget gates still apply. (alpha.7+) */ forceProviderAlias?: string; /** Reasoning effort hint for o-series / gpt-5-nano / Groq gpt-oss-120b. Silently ignored by adapters whose providers don't honor it. (alpha.12+) */ reasoningEffort?: "low" | "medium" | "high"; /** Per-call escape hatch for provider-specific request fields not modeled on the port. Shallow-merged into the SDK request body after typed fields. (alpha.16+) */ providerExtras?: Record; /** Provider-neutral cache configuration. Shape locked in alpha.19. */ cacheControl?: CacheControl; /** * Per-call override for strict-schema response_format mode. (alpha.21+) * Same semantics as on `GenerateStructuredOptions`. See that field's docstring. */ strict?: boolean; /** * Per-call scope hint. When set, the Registry hashes (scope, scopeId) * into the gating storage key so configured caps apply per-scope rather * than per-alias. Omitting it preserves alpha.19.1 per-alias behavior. * (alpha.20+) */ budgetScope?: BudgetScopeRef; /** Consumer-owned artifact reference tags flowing through to observability events. See `GenerateTextOptions.refs`. (alpha.25+) */ refs?: Record; } interface RunAgentOptions { taskType: TaskType; priority?: LLMPriority; instructions: string; messages: LLMMessage[]; tools: Record; maxSteps?: number; maxOutputTokens?: number; temperature?: number; /** Cancellation signal threaded through to the provider's HTTP fetch. */ signal?: AbortSignal; /** Override task routing for this call only; route directly to the named provider alias. Per-provider budget gates still apply. (alpha.7+) */ forceProviderAlias?: string; /** Reasoning effort hint for o-series / gpt-5-nano / Groq gpt-oss-120b. Silently ignored by adapters whose providers don't honor it. (alpha.12+) */ reasoningEffort?: "low" | "medium" | "high"; /** Per-call escape hatch for provider-specific request fields not modeled on the port. Shallow-merged into the SDK request body after typed fields. (alpha.16+) */ providerExtras?: Record; /** Provider-neutral cache configuration. Shape locked in alpha.19. */ cacheControl?: CacheControl; /** * Per-call scope hint. When set, the Registry hashes (scope, scopeId) * into the gating storage key so configured caps apply per-scope rather * than per-alias. Omitting it preserves alpha.19.1 per-alias behavior. * (alpha.20+) */ budgetScope?: BudgetScopeRef; /** Consumer-owned artifact reference tags flowing through to observability events. See `GenerateTextOptions.refs`. (alpha.25+) */ refs?: Record; } interface GenerateTextResult { text: string; usage: TokenUsage; cost: CostUsage; modelId: string; providerAlias: string; latencyMs: number; } interface GenerateStructuredResult { data: T; usage: TokenUsage; cost: CostUsage; modelId: string; providerAlias: string; latencyMs: number; /** 1 = first try; 2+ = retried via retry-with-feedback. */ validationAttempts: number; } interface AgentResult { text: string; messages: LLMMessage[]; toolCalls: Array<{ name: string; input: Record; output: unknown; }>; usage: TokenUsage; cost: CostUsage; modelId: string; providerAlias: string; latencyMs: number; stepsTaken: number; terminationReason: "completed" | "max_steps" | "stopped_by_user"; } /** * Information about a single model the provider exposes. Returned by * {@link LLMPort.listModels} when supported. Each adapter populates the * fields the provider's API exposes; absent fields signal "the provider * doesn't tell us this", not "the model lacks the property". * * Used by {@link Registry.checkPricingFreshness} to compare bundled * pricing tables against the provider's current model catalog and warn * about drift. */ interface ProviderModelInfo { /** Provider-side model id, e.g. `gpt-5`, `claude-opus-4-7`, `gemini-2.5-flash`. */ id: string; /** Friendly name when the API exposes it. */ displayName?: string; /** USD per 1M input tokens, when the API exposes pricing. */ inputPer1M?: number; /** USD per 1M output tokens, when the API exposes pricing. */ outputPer1M?: number; /** Context-window size in tokens, when the API exposes it. */ contextWindow?: number; /** Free-form metadata bag for provider-specific fields (e.g. modality, family). */ metadata?: Record; } /** * Adapters implement this. Business logic depends on this. * Zero imports from any LLM SDK. */ interface LLMPort { /** Free-form text generation. Use for: drafts, summaries, recommendations. */ generateText(options: GenerateTextOptions): Promise; /** Schema-validated structured output. Use for: triage, scoring, extraction. */ generateStructured(options: GenerateStructuredOptions): Promise>; /** Token-by-token text streaming. Use for: chat UIs, long briefings. */ streamText(options: StreamTextOptions): AsyncIterable; /** * Progressively-parseable partial JSON streaming. Use for: * forms, cards, charts that render as the model emits them. * Yields successively more complete partial objects. */ streamStructured(options: StreamStructuredOptions): AsyncIterable>; /** Multi-turn tool-use loop. The agent primitive. */ runAgent(options: RunAgentOptions): Promise; /** * Runtime model discovery (alpha.9+). Returns the models the provider * currently exposes, with bundled metadata when the provider's API * exposes it (pricing, context window, family). Optional: adapters * implement it where the provider has a `/models` endpoint; bridges * (adapter-vercel) skip it. * * Used by `Registry.checkPricingFreshness()` to flag bundled-pricing * drift. Users can also call it directly for "show me the available * models" UIs. */ listModels?(): Promise; } /** * EmbeddingsPort — sibling interface to LLMPort for embedding generation. * * Split from LLMPort because most chat adapters (e.g. Anthropic) do not * expose embeddings, and some adapters (voyage-ai, cohere-embed-only) do * only embeddings. Forcing one port to span both kinds would produce * many stub implementations. * * See implementation plan v3 §6.2. */ interface EmbeddingOptions { taskType: TaskType; /** The text to embed. */ input: string; /** Optional model hint; the registry typically picks via taskType. */ modelHint?: string; /** Per-call scope hint for gating. Same semantics as LLMPort. (alpha.20+) */ budgetScope?: BudgetScopeRef; } interface BatchEmbeddingOptions { taskType: TaskType; /** Multiple inputs to embed in a single API call (provider-dependent batching). */ inputs: string[]; modelHint?: string; /** Per-call scope hint for gating. Same semantics as LLMPort. (alpha.20+) */ budgetScope?: BudgetScopeRef; } interface EmbeddingResult { vector: number[]; dimensions: number; modelId: string; providerAlias: string; usage: { inputTokens: number; }; cost: CostUsage; latencyMs: number; } interface BatchEmbeddingResult { vectors: number[][]; dimensions: number; modelId: string; providerAlias: string; usage: { inputTokens: number; }; cost: CostUsage; latencyMs: number; } /** * Adapters that support embeddings implement this interface. * The registry exposes it via `getEmbeddingsPort()`, separate from `getPort()`. */ interface EmbeddingsPort { generateEmbedding(options: EmbeddingOptions): Promise; generateEmbeddings(options: BatchEmbeddingOptions): Promise; } /** * RerankPort — sibling interface to LLMPort and EmbeddingsPort for document * reranking. * * Split from LLMPort because rerank is a separate computational primitive * from chat completion: the model takes (query, [docs]) and emits the same * documents re-scored and re-ordered by relevance. Cohere Rerank-3, Voyage * AI rerank-2, Jina Reranker, Mixedbread mxbai-rerank all ship dedicated * rerank APIs that are not chat-completion-shaped. * * Adapters implementing rerank wrap the provider's native rerank endpoint * (e.g. `cohere.rerank()`) directly. They MUST NOT route through the LLM * to fake rerank-via-LLM; that loses the cost and latency advantage of * dedicated reranker models (typically 50ms / $0.001 per 1000 docs vs * seconds / $0.01-$0.10 for an LLM-as-reranker). * * Added in alpha.17 (interface only; first implementing adapter is * adapter-cohere in beta.0). See roadmap §4.6 in the BEPA-internal release * plan for the locked design decisions. */ /** * Input to a rerank call. * * Single query per call by design (no batching). Cohere and Voyage both * support batching but the field rarely uses it; keeping the surface * narrow lets the same `cacheKey`/budget semantics apply uniformly. */ interface RerankInput { /** * The query string against which `documents` are ranked. */ query: string; /** * Candidate documents to rerank. Order does not matter; each document is * scored independently against `query`. The returned `RerankedDocument.index` * field maps back to the original position in this array. */ documents: string[]; /** * Return only the top N most-relevant documents. If omitted, all input * documents are returned (re-scored and re-ordered). */ topN?: number; /** * Cancellation signal. Mirrors the pattern in `LLMPort.GenerateTextOptions`; * adapters thread this into the underlying HTTP request. */ signal?: AbortSignal; /** * Override task routing for this call (mirrors `LLMPort.forceProviderAlias`). * Per-provider budget + cost gates still apply. */ forceProviderAlias?: string; /** * Per-call escape hatch for provider-specific request fields not modeled * on the port. Shallow-merged into the SDK request body after typed port * fields. Examples: * - Cohere: `{ max_chunks_per_doc: 10 }` for long-document chunking * - Voyage: `{ truncation: true }` for input truncation policy * - Jina: `{ return_documents: false }` to skip echoing doc text * * Same semantics as `providerExtras` on the LLMPort options. The port * does NOT validate; field semantics are provider-specific. */ providerExtras?: Record; } /** * A single rescored document in a rerank result. * * The `document` field echoes the input document text by design; consumers * typically need both the score AND the text together (e.g. for display * after a vector-search-then-rerank pipeline). */ interface RerankedDocument { /** * Original position of this document in `RerankInput.documents` (zero-indexed). * Use this to correlate results with consumer-side data structures. */ index: number; /** * Provider-normalized relevance score in the closed interval [0, 1]. * * Cohere returns 0-1 natively. Voyage and Jina also return 0-1. If a * future adapter wraps a provider that returns a different range, * the adapter MUST normalize before returning. */ relevanceScore: number; /** * The original document text, echoed for consumer convenience. * * Some providers can return only `index + relevanceScore` and skip the * text (Cohere `return_documents: false`); when callers opt into that * via `providerExtras`, this field will be an empty string. The default * shape includes the text. */ document: string; } /** * Result of a successful rerank call. * * The `usage` and `cost` shapes mirror LLMPort and EmbeddingsPort so that * observability hooks (`onCost`, `onTokenUsage` from alpha.21) work * uniformly across the three port families. */ interface RerankResult { /** * Reranked documents in descending order of `relevanceScore`. Length is * `min(topN, documents.length)`; defaults to `documents.length` when * `topN` is omitted. */ results: RerankedDocument[]; /** * Token / search-unit consumption telemetry. Rerank providers bill in * different units: * - Cohere: search units (1 unit = 1 search of ≤100 documents) * - Voyage / Jina / Mixedbread: input tokens * * The `usage` shape includes optional `searchUnits` and `rerankedDocuments` * fields specifically for rerank billing; standard `inputTokens` / * `outputTokens` from LLMPort.TokenUsage remain available for adapters * that report in tokens. */ usage: TokenUsage; /** * USD cost computed from `usage` + the adapter's pricing table. Aligned * with the LLMPort.CostUsage shape so the same `onCost` observability * hook works for rerank calls. */ cost: CostUsage; /** Provider-side model identifier that produced the ranking. */ modelId: string; /** Registry alias that resolved to this provider. */ providerAlias: string; } /** * Adapters that support reranking implement this interface. * * The factory returned by `createAdapter` exposes * `createRerankPort(modelId, alias)` (parallel to `createLLMPort` and * `createEmbeddingsPort`) on the adapter. Consumers obtain a RerankPort * via the registry or directly from the adapter. * * See `packages/adapter-cohere` (first implementation, beta.0) for the * canonical example. */ interface RerankPort { rerank(input: RerankInput): Promise; } /** * Helpers for normalizing between string and ContentBlock[] forms. * * Adapters use these to accept either input shape and emit the canonical * ContentBlock[] form that the rest of the system reasons about. */ /** True if the value is the string-sugar form of MessageContent. */ declare function isStringContent(content: MessageContent): content is string; /** Convert any MessageContent to its canonical ContentBlock[] form. */ declare function toBlocks(content: MessageContent): ContentBlock[]; /** * Collapse a ContentBlock[] to a single string if and only if every block is text. * Returns null if the content includes non-text blocks (caller must keep array form). * Useful for adapters whose underlying SDK wants `content: string` for text-only messages. */ declare function tryCollapseToText(blocks: ContentBlock[]): string | null; /** Concatenate all text content from a MessageContent, ignoring non-text blocks. */ declare function extractText(content: MessageContent): string; /** * Cross-adapter observability hook for transient retries. * * Adapters call `onRetry` when they decide to retry an in-flight request for * a known transient reason — burst-protection 401s, capability-rejection * fallback (drop temperature, drop json_object, drop system message), * reasoning-starved responses (model spent all tokens on hidden reasoning), * or schema-validation feedback retries. * * This is observability only. Adapters decide whether to retry; the hook just * gets told. Throwing from the hook does NOT cancel the retry — adapters call * the hook fire-and-forget. */ /** * Discriminator for why the adapter retried. New reasons may be added in * minor releases; consumers should default to logging the event verbatim * rather than switching exhaustively. */ type RetryReason = /** Project-key burst-protection 401 from OpenAI. The key is valid; retry. */ "transient-auth" /** Model rejected an unsupported parameter (temperature, json_object, system). Drop and retry. */ | "capability-fallback" /** Reasoning model spent its whole budget on hidden tokens. Retry with expanded budget. */ | "reasoning-starvation" /** Structured-output response failed schema validation; retry with corrective feedback. */ | "validation-feedback" /** * Tool call was emitted in the harmony reasoning channel (`message.reasoning_content`) * rather than the standard `message.tool_calls` array. The adapter extracted the * harmony tool call and hoisted it into the executable path. No retry was performed * — this is observability only, signaling that the response shape was non-standard * but recoverable. (alpha.23+) */ | "harmony-tool-call-extracted" /** * Model emitted prose without making any tool calls, despite the request providing * a tools array. Retry with a corrective system message asking the model to use * the standard `tool_calls` array. Single-shot retry. (alpha.23+) */ | "zero-tool-call-prose-retry"; /** What the adapter passes to `onRetry` each time it retries. */ interface RetryEvent { reason: RetryReason; /** 0-indexed retry number (0 = first retry, after the original attempt failed). */ attempt: number; modelId: string; providerAlias: string; /** Milliseconds slept before this retry. 0 when the retry fires immediately. */ delayMs: number; /** * The error that triggered the retry, when applicable. `undefined` for * reasoning-starvation (which inspects a successful response, not an error) * and may be undefined for validation-feedback if the adapter doesn't * forward the Zod issues here. */ cause?: unknown; /** * When `reason === "capability-fallback"`, names the specific capability * the adapter learned about (e.g. `temperatureLocked`, `jsonModeUnsupported`, * `systemMessageInUserOnly`). Lets observability stacks distinguish "we * stripped temperature" from "we stripped json_object" from each other. * Adapter authors should populate this whenever the retry was driven by a * specific capability rejection. Omitted for other retry reasons. */ capability?: string; } /** * Observability hook. Sync or async. Adapters call it fire-and-forget; they * do NOT await the returned promise and do NOT cancel the retry if the hook * throws. Use this to emit logs, metrics, or traces. */ type OnRetry = (event: RetryEvent) => void | Promise; /** * Jitter strategy for exponential backoff delays. * * Per the AWS Architecture Blog "Exponential Backoff And Jitter" (2015) and * subsequent industry consensus, decorrelated jitter is the recommended * default for high-concurrency clients because it preserves the average * backoff while breaking up retry storms most aggressively. "Full" matches * Genkit's default. "Equal" matches the classic Capacity-Random-Truncated * Binary Exponential Backoff. "None" disables jitter (use for tests). */ type JitterStrategy = "none" | "full" | "equal" | "decorrelated"; /** * Configurable jittered exponential backoff for adapter retry loops. * * Adapters consume this config when computing the delay before a retry. * The shape matches Genkit's middleware retry config so users migrating * from Genkit see a familiar API. * * Defaults (when fields are omitted): * - initialDelayMs: 200 * - maxDelayMs: 10000 * - multiplier: 2 * - jitter: "decorrelated" * * Pseudocode for delay computation: * * baseDelay = min(initialDelayMs * multiplier^attempt, maxDelayMs) * switch (jitter) { * case "none": return baseDelay * case "full": return random(0, baseDelay) * case "equal": return baseDelay/2 + random(0, baseDelay/2) * case "decorrelated": return min(maxDelayMs, random(initialDelayMs, prevDelay * 3)) * } * * Added in alpha.17. Adapters wire this in adapter-specific releases. */ interface BackoffConfig { /** * Delay before the first retry, in milliseconds. The base from which * subsequent attempts scale exponentially. Default: 200ms. */ initialDelayMs?: number; /** * Hard ceiling on any single retry delay. Prevents runaway exponential * growth. Default: 10000ms (10 seconds). */ maxDelayMs?: number; /** * Exponential growth factor. Default: 2 (each attempt waits ~2x the * previous one before jitter is applied). */ multiplier?: number; /** Jitter strategy. Default: "decorrelated". */ jitter?: JitterStrategy; } /** * Compute the delay (in ms) before the Nth retry attempt under a given * BackoffConfig. Pure function; useful for testing and for adapters that * want to apply uniform backoff semantics. * * @param attempt 0-indexed retry number (0 = before the first retry). * @param config Backoff configuration. Missing fields filled with defaults. * @param prevDelay The previous attempt's computed delay; required for * "decorrelated" jitter, ignored otherwise. Pass `initialDelayMs` for * the first call. * @param rng A 0-1 uniform random function. Defaults to Math.random for * production; tests should pass a deterministic function. */ declare function computeBackoffDelay(attempt: number, config?: BackoffConfig, prevDelay?: number, rng?: () => number): number; /** Cause of a Registry-level fallback advancement. */ type FallbackCause = /** The primary provider raised an error (budget exhausted, 401, 5xx, transient). */ "provider-error" /** A budget gate (req/min, cost/day, etc.) denied the call on the primary. */ | "budget-exhausted" /** Structured output validation exhausted retries; chain advances to a fallback model. */ | "validation-exhausted" /** The primary returned an empty response after starvation retries gave up. */ | "empty-response" /** The primary's circuit breaker is open. */ | "circuit-open"; /** Trigger for `onValidationRetry`. */ type ValidationRetryCause = /** The model returned valid JSON that failed Zod validation. Retry with feedback. */ "schema-mismatch" /** The model returned non-JSON text. Retry with stricter prompt. */ | "parse-error"; /** OnCost event: per-call cost breakdown. */ interface CostEvent { /** USD spent on input tokens for this call. */ promptUsd: number; /** USD spent on output tokens for this call. */ completionUsd: number; /** USD spent on cache-read tokens (when the provider has a discounted tier). */ cacheReadUsd?: number; /** USD spent on cache-write tokens (Anthropic-style explicit-cache providers). */ cacheWriteUsd?: number; /** USD spent on reasoning tokens (hidden chain-of-thought billed separately). */ reasoningUsd?: number; /** Total USD for this single call. */ totalUsd: number; /** Model that produced the result (may differ from requested when the model serves under an alias). */ modelId: string; /** Adapter alias used (the Registry-side name, e.g. `gptoss-cerebras`). */ providerAlias: string; /** Operation kind. */ operation: "generateText" | "generateStructured" | "streamText" | "streamStructured" | "runAgent" | "embed" | "rerank"; /** Optional task-type tag from the call site. */ taskType?: string; /** Optional scope hint passed by the caller for downstream attribution. */ budgetScope?: BudgetScopeRef; /** Consumer-owned artifact reference tags passed on the call options; verbatim on the event. (alpha.25+) */ refs?: Record; } /** OnTokenUsage event: per-call token counts (raw, before cost monetization). */ interface TokenUsageEvent { inputTokens: number; outputTokens: number; cachedInputTokens?: number; cacheCreationTokens?: number; reasoningTokens?: number; totalTokens: number; modelId: string; providerAlias: string; operation: CostEvent["operation"]; taskType?: string; budgetScope?: BudgetScopeRef; /** Consumer-owned artifact reference tags passed on the call options; verbatim on the event. (alpha.25+) */ refs?: Record; } /** OnFallback event: chain advanced to the next provider. */ interface FallbackEvent { /** The alias the call was originally routed to. */ fromAlias: string; /** The alias the call was reassigned to. */ toAlias: string; /** Why the chain advanced. */ cause: FallbackCause; /** Operation kind. */ operation: CostEvent["operation"]; /** Optional task type for grouping in observability stacks. */ taskType?: string; /** The error or signal that triggered the advancement, when applicable. */ reason?: unknown; /** Consumer-owned artifact reference tags passed on the call options; verbatim on the event. (alpha.25+) */ refs?: Record; } /** OnValidationRetry event: retry-with-feedback round-trip on structured output. */ interface ValidationRetryEvent { /** 0-indexed retry number (0 = first retry after the initial call). */ attempt: number; /** Maximum attempts the adapter is configured to make in total. */ maxAttempts: number; modelId: string; providerAlias: string; /** Why this retry fired. */ cause: ValidationRetryCause; /** Validation issues (Zod issues, parse error message, etc.). */ issues?: unknown; /** Operation kind. */ operation: "generateStructured" | "streamStructured"; /** Consumer-owned artifact reference tags passed on the call options; verbatim on the event. (alpha.25+) */ refs?: Record; } /** OnCacheHit event: provider reported cached prompt tokens. */ interface CacheHitEvent { /** Tokens served from cache (matches the provider's `cached_tokens` field). */ cachedTokens: number; /** Total input tokens this call would have billed without the cache hit. */ inputTokensTotal: number; /** Computed hit ratio = cachedTokens / inputTokensTotal. */ hitRatio: number; /** USD saved by the cache hit. Only populated when the provider has a discounted cache-read tier. */ savingsUsd?: number; modelId: string; providerAlias: string; operation: CostEvent["operation"]; taskType?: string; /** Consumer-owned artifact reference tags passed on the call options; verbatim on the event. (alpha.25+) */ refs?: Record; } /** Fired after every billable call with cost breakdown. Fire-and-forget. */ type OnCost = (event: CostEvent) => void | Promise; /** Fired after every billable call with raw token counts. Fire-and-forget. */ type OnTokenUsage = (event: TokenUsageEvent) => void | Promise; /** Fired when the Registry's provider chain advances. Fire-and-forget. */ type OnFallback = (event: FallbackEvent) => void | Promise; /** Fired when retry-with-feedback round-trips on structured output. Fire-and-forget. */ type OnValidationRetry = (event: ValidationRetryEvent) => void | Promise; /** Fired when the provider reports cache hits. Fire-and-forget. */ type OnCacheHit = (event: CacheHitEvent) => void | Promise; /** * Bundle of optional observability hooks passed at Registry construction * (alpha.21+). Each field is independently optional; pass only the ones the * downstream pipeline needs. */ interface ObservabilityHooks { onCost?: OnCost; onTokenUsage?: OnTokenUsage; onFallback?: OnFallback; onValidationRetry?: OnValidationRetry; onCacheHit?: OnCacheHit; } declare function emitCost(hook: OnCost | undefined, event: CostEvent): void; declare function emitTokenUsage(hook: OnTokenUsage | undefined, event: TokenUsageEvent): void; declare function emitFallback(hook: OnFallback | undefined, event: FallbackEvent): void; declare function emitValidationRetry(hook: OnValidationRetry | undefined, event: ValidationRetryEvent): void; declare function emitCacheHit(hook: OnCacheHit | undefined, event: CacheHitEvent): void; /** * Derive a validation-retry event from an adapter-level `OnRetry` event. * (alpha.24+) * * The Registry-level `onValidationRetry` hook fires only for the * `validation-feedback` retry reason (structured-output schema mismatch). * Pass the result of this helper as the adapter's `onRetry` so each * adapter's retry events flow through to the Registry's observability * hooks AND any user-supplied adapter-level callback. * * @example * import { createOpenAIAdapter } from "@llm-ports/adapter-openai"; * import { createRegistryFromEnv, deriveValidationRetryFromAdapterRetry } from "@llm-ports/core"; * * const registry = createRegistryFromEnv({ * // ... * observability: { * onValidationRetry: (e) => myMetrics.validationRetries.inc({ model: e.modelId }), * }, * }); * * const adapter = createOpenAIAdapter({ * apiKey: process.env.OPENAI_API_KEY!, * onRetry: deriveValidationRetryFromAdapterRetry(registry, { * userOnRetry: (e) => myLogger.warn("retry", e), // chain optional * }), * }); * * **Caveats.** The adapter-level `RetryEvent` doesn't carry `maxAttempts` * or `operation`, so this helper: * - Sets `maxAttempts` to `event.attempt + 1` (best-known lower bound) * - Defaults `operation` to `"generateStructured"` (validation-feedback * only fires from structured-output paths, so this is correct in * practice) * - Sets `cause` to `"schema-mismatch"` unconditionally (the adapter * doesn't distinguish parse errors from schema errors today) * * If the user supplies an `OperationOverride`, that wins. */ declare function deriveValidationRetryFromAdapterRetry(registry: { observability: ObservabilityHooks; }, opts?: { /** Chain a user-supplied adapter-level onRetry. Errors swallowed. */ userOnRetry?: (event: RetryEvent) => void | Promise; /** Override the operation discriminator. Default `"generateStructured"`. */ operation?: "generateStructured" | "streamStructured"; }): (event: RetryEvent) => void; /** * Internal contract between the Registry and an adapter for surfacing * stream-completion metadata (usage + cost + timing) back to the Registry's * observability hooks. (alpha.25+) * * The Registry attaches a `StreamCompleteCallback` to the options object * via `STREAM_COMPLETE_CALLBACK_KEY` before dispatching to the adapter. The * adapter reads it and calls it ONCE with the final metadata when the stream * completes naturally. The Registry then emits `onCost` + `onTokenUsage` * from the callback body, preserving `refs` and all other observability * plumbing. * * Semantics enforced by callers: * - The callback is called AT MOST ONCE per call. * - It is called only on natural stream completion. Mid-stream errors, * consumer-cancelled streams, and empty streams do NOT invoke it. * - Adapters that don't implement stream metadata (older adapter builds * mixed with new core) just don't call the callback; the Registry * falls back to zero-cost-recording behavior (matches alpha.24 for * stream methods). * * This is an internal contract, not a public consumer API. Consumers set * observability hooks at Registry construction; the Symbol-keyed callback * is Registry-internal plumbing. */ declare const STREAM_COMPLETE_CALLBACK_KEY: unique symbol; /** Payload the adapter passes when firing the stream-complete callback. */ interface StreamCompleteMetadata { usage: TokenUsage; cost: CostUsage; modelId: string; providerAlias: string; latencyMs: number; } /** Signature of the Registry-attached stream-complete callback. */ type StreamCompleteCallback = (meta: StreamCompleteMetadata) => void; /** * Read the Registry-attached stream-complete callback from a call options * object. Adapters call this to retrieve the callback before entering their * stream loop; if a callback is present, the adapter fires it on natural * completion. Type-safe wrapper around the Symbol-keyed access. */ declare function readStreamCompleteCallback(options: object | undefined): StreamCompleteCallback | undefined; /** * Attach a stream-complete callback to a call options object. The Registry * calls this before dispatching a stream call to an adapter that supports * completion metadata. Returns the mutated options (same reference). */ declare function attachStreamCompleteCallback(options: T, callback: StreamCompleteCallback): T; /** * Compute cache-hit metadata from a TokenUsage. Returns null when the usage * has no cache hit to emit (cachedInputTokens missing or 0). Adapters call * this after a successful call to determine whether to emit `onCacheHit`. */ declare function deriveCacheHit(usage: TokenUsage, cost: CostUsage | undefined): { cachedTokens: number; inputTokensTotal: number; hitRatio: number; savingsUsd?: number; } | null; /** * Validation strategies for generateStructured() output that fails schema. * * The default is `retry-with-feedback` with maxAttempts=2: when validation * fails, the strategy injects the Zod errors into the next prompt and asks * the model to regenerate. In the originating production workflow, this * strategy achieves ~70% fix rate on the second attempt. * * See docs/concepts/validation-strategies for the full design rationale * and when to swap strategies. */ /** * Strategy applied when generated structured output fails schema validation. * * - `throw`: fail immediately with ValidationError. * - `retry-with-feedback`: re-prompt the model with the Zod errors * injected into the next user message. Default. * - `fallback-to-next-provider`: skip to the next provider in the task chain * (the registry handles the actual fallback). * - `custom`: user-provided handler decides what to do. */ type ValidationStrategy = { kind: "throw"; } | { kind: "retry-with-feedback"; maxAttempts: number; includeOriginalError: boolean; } | { kind: "fallback-to-next-provider"; } | { kind: "custom"; handler: (ctx: ValidationFailureContext) => Promise; }; interface ValidationFailureContext { attempt: number; schema: z.ZodType; rawOutput: unknown; issues: ZodIssue[]; /** Re-invoke the model with a (possibly modified) prompt. */ retry: (correctionMessage?: string) => Promise; } /** Default strategy: retry twice with the validation errors fed back. */ declare const DEFAULT_VALIDATION_STRATEGY: ValidationStrategy; /** * Build a correction prompt from Zod issues. Adapters call this when * implementing retry-with-feedback to construct the re-prompt. */ declare function buildCorrectionPrompt(issues: ZodIssue[]): string; /** * Helper for adapters: throw the canonical ValidationError when the strategy * has exhausted attempts or kind is "throw". */ declare function failValidation(issues: ZodIssue[], attempts: number): never; /** * Deprecation-warning emitter with dedup + suppression + custom handler. * * A generic surface consumers and library authors reuse whenever a * public field, method, or configuration is deprecated on an active * release line. Every unique deprecation "where" produces at most one * warning per Registry instance; suppression is per-Registry; * structured logging routes through the optional `handler`. * * Renamed and generalized in `0.1.0-alpha.27` from the field-specific * `warnDeprecatedLegacyInput(state, method)` shipped in alpha.26. The * runtime behavior (method-only dedup, suppression, handler routing) * is identical; the new signature accepts a details object instead of * just a method name. */ /** Per-Registry deprecation-warning state. */ interface WarningState { /** When true, no warnings fire regardless of the dedup set. */ suppressed: boolean; /** Set of `where` keys already warned for. */ warned: Set; /** * Optional replacement for `console.warn`. Consumers wanting structured * logging can supply a function that receives the warning message. * Defaults to `console.warn`. */ handler?: (message: string) => void; } /** Fresh warning state for a Registry instance. */ declare function createWarningState(opts?: { suppressed?: boolean; handler?: (message: string) => void; }): WarningState; /** * Descriptor for a deprecation warning. `where` is the dedup key; every * unique `where` produces at most one warning per WarningState. Consumers * of the library and library authors both use this surface. */ interface DeprecationDetails { /** * Human-readable name of the deprecated surface. E.g. * `"'onMissing' as a function callback"` or * `"'perAttemptTimeoutMs' option"`. */ what: string; /** * Dedup key AND display location. E.g. `"createVersionedStore"` or * `"streamText"`. Every unique `where` value produces at most one * warning per WarningState. */ where: string; /** * Version the deprecated surface will be removed in. E.g. * `"alpha.35"`. Optional but recommended for consumer planning. */ removalVersion?: string; /** * URL to the migration guide for this specific deprecation. Optional * but recommended. */ migrationUrl?: string; } /** * Emit a deprecation warning through the shared WarningState. Cheap on * repeat calls: only the O(1) Set lookup runs after the first warning * per unique `where`. Respects `suppressed` and routes through the * optional `handler` (default `console.warn`). * * Consumers of `@llm-ports/core` who need to fire deprecation warnings * from custom code paths (adapter authors, downstream wrapper * libraries) can call this with a WarningState acquired from the * Registry: `warnDeprecated(registry.warningState, { what, where, ... })`. * * Generalized in `0.1.0-alpha.27` from the field-specific * `warnDeprecatedLegacyInput` that shipped in alpha.26. */ declare function warnDeprecated(state: WarningState, details: DeprecationDetails): void; /** * Env config parser for the registry. * * Reads `LLM_PROVIDER_*` and `LLM_TASK_ROUTE_*` entries from a record * (typically `process.env`) and returns a structured config the registry * uses for routing. * * Format: * LLM_PROVIDER_=||[,...] * LLM_TASK_ROUTE_=[,...] * * Gating tokens (alpha.20): * req:N/{minute|hour|session} -> request-count limit * cost:N/{minute|hour|day|month|session} -> USD limit * total_tokens:N/session -> tokens per CostSession * tool_calls:N/session -> tool calls per CostSession * unlimited -> no gating (useful for local Ollama) * * Backwards compatible: `req:N/hour` and `cost:N/{hour|day|month}` from * alpha.19 keep working unchanged. `req:N/hour` still writes the legacy * `requestsPerHour` field for backend backwards compat. * * See docs/concepts/task-routing for the full design rationale. */ interface ProviderEntry { /** User-chosen alias (lowercase, derived from env var name). */ alias: string; /** Which adapter implementation this alias uses ("anthropic", "openai", "ollama", "vercel"). */ adapter: string; /** Provider-specific model id. */ modelId: string; budgetLimit: BudgetLimit; costLimit: CostLimit; /** Session-grain limits (token + tool-call ceilings). Enforced by CostSession. (alpha.20+) */ sessionLimits?: SessionGrainLimits; } interface RegistryConfig { providers: Record; /** Maps task type -> ordered fallback chain of provider aliases. */ taskRoutes: Record; } interface ParseConfigOptions { /** Env var prefix. Default: "LLM_". */ envPrefix?: string; /** Source record. Default: process.env. */ env?: Record; } declare function parseRegistryConfig(opts?: ParseConfigOptions): RegistryConfig; /** * Session-scoped cost gating. * * Wraps an LLMPort with session-level ceilings independent of the per-provider * windowed gates. Designed for continuous-call workloads (screen capture, OCR * loops, multi-step agents) where a single stuck-open session can burn * arbitrary dollars / tokens / tool calls. * * Usage: * * const session = registry.openCostSession({ budgetUSD: 0.50 }); * const llm = session.getPort(); * try { * for (const frame of screenCaptureFrames) { * await llm.generateText({ taskType: "screen_analyze", prompt: [...] }); * } * } finally { * console.log("session spent:", session.totalSpentUSD()); * session.close(); * } * * Throws `SessionBudgetExceededError` mid-loop when a cap is reached. The * per-provider windowed gates still apply on top — session ceilings are a * hard backstop, not a replacement. * * alpha.20 additions: * - tokensUsed() / toolCallsMade() / requestsMade() helpers. * - Optional `maxTokens`, `maxToolCalls`, `maxRequests` ceilings that mirror * the env-driven `total_tokens:N/session`, `tool_calls:N/session`, and * `req:N/session` gating tokens. The error thrown is still * `SessionBudgetExceededError` so existing catch-blocks keep working; * the `reason` field distinguishes which cap tripped. */ interface OpenCostSessionOptions { /** Hard USD cap for this session. Required. */ budgetUSD: number; /** * Optional client-supplied session ID. If omitted, a timestamp-keyed id * is generated. The ID appears in `SessionBudgetExceededError.sessionId` * and is useful for log correlation across multi-step flows. */ sessionId?: string; /** * Optional ceiling on total tokens used in this session (input + output, * across every call). Maps to the `total_tokens:N/session` env gating * token. Tripping this throws `SessionBudgetExceededError`. (alpha.20+) */ maxTokens?: number; /** * Optional ceiling on tool / function calls made by `runAgent` in this * session. Maps to the `tool_calls:N/session` env gating token. Tripping * this throws `SessionBudgetExceededError`. (alpha.20+) */ maxToolCalls?: number; /** * Optional ceiling on requests made in this session (every call counts, * including streaming). Maps to the `req:N/session` env gating token. * Tripping this throws `SessionBudgetExceededError`. (alpha.20+) */ maxRequests?: number; } /** * Handle returned by `Registry.openCostSession`. */ declare class CostSession { private readonly underlying; readonly id: string; readonly budgetUSD: number; readonly maxTokens?: number; readonly maxToolCalls?: number; readonly maxRequests?: number; private spentUSD; private tokens; private toolCalls; private requests; private closed; constructor(underlying: LLMPort, opts: OpenCostSessionOptions); totalSpentUSD(): number; remainingUSD(): number; /** Total tokens billed (input + output) across all calls. (alpha.20+) */ tokensUsed(): number; /** Tool / function calls made by runAgent across the session. (alpha.20+) */ toolCallsMade(): number; /** Total port calls made in this session. (alpha.20+) */ requestsMade(): number; /** * Returns an LLMPort proxy that tracks every call's cost / tokens / tool * calls / request count against this session. Before invoking the * underlying port, the proxy checks every ceiling; the first one that * would be exceeded throws `SessionBudgetExceededError`. */ getPort(): LLMPort; /** * Close the session. After `close()`, calls through any port previously * returned by `getPort()` throw an error. * * Returns the total USD spent during the session — useful for logging * or charging back to a tenant. */ close(): number; } /** * Registry — selects an adapter for a given task type, applies budget/cost gating, * walks fallback chains on failure, and exposes the unified LLMPort surface. * * This implementation is intentionally minimal in v0.1: * - Adapters are registered by name (matching the env config's adapter token). * - selectModel(taskType) walks the configured fallback chain and returns the * first adapter whose budget allows the call. * - getPort() returns an LLMPort proxy whose every method invokes selectModel. * - The registry exposes both LLMPort and EmbeddingsPort (when supported by adapters). * * Note: this skeleton focuses on the routing/gating contract. Adapter implementations * land in their own packages (Week 2 onwards) and plug in here. */ /** * What an adapter passes to the registry on registration. The adapter * provides factories that, given a model id, return a configured port instance. * * Why factories instead of pre-built ports: the registry knows the model id * per-call (from env config), so the adapter must defer instantiation until * model selection time. */ interface AdapterRegistration { /** Adapter name, must match the env config's `` token. */ name: string; /** Build an LLMPort for a specific model. Required for chat-capable adapters. */ createLLMPort?: (modelId: string, alias: string) => LLMPort; /** Build an EmbeddingsPort for a specific model. Optional. */ createEmbeddingsPort?: (modelId: string, alias: string) => EmbeddingsPort; /** Pricing table this adapter ships, keyed by modelId. */ pricing: Record; } interface RegistryOptions { envPrefix?: string; env?: Record; /** Adapters keyed by their name (must match env config tokens). */ adapters: Record; budget?: BudgetBackend; cost?: CostBackend; validationStrategy?: ValidationStrategy; /** Override pricing for specific model ids (key = modelId). */ pricingOverrides?: Record; /** * Runtime-error fallback configuration. When a call to the first viable * provider in a task's fallback chain throws an error matching `catchClass`, * the registry walks to the next viable provider and retries — without the * caller having to catch and re-route themselves. * * Default: walks on `ProviderUnavailableError` only (the safest class — * covers 5xx, network errors, rate-limit-style 429s the SDK wraps). * * Set to `"none"` to disable runtime fallback entirely (v0.1 behavior; * caller catches `ProviderUnavailableError` and routes manually). * * Set to a custom predicate for finer control (e.g. walk on * `EmptyResponseError` too, or skip 429s and let the SDK's own backoff * handle them). * * Added in `0.1.0-alpha.7`. * * The `"aggressive"` preset was added in `0.1.0-alpha.25` (LP-REQ-01). * It bundles the opinionated classifier three consumers had rebuilt by * hand (BEPA Plan 29, HomeSignal, SalesCoach Plan 30). See * {@link aggressiveShouldFallback} for the full matrix; the summary is * "walk on RateLimitError, EmptyResponseError, ContextWindowExceededError, * BadRequestError with credit-exhaustion body patterns, and raw 5xx status * codes — in addition to the default ProviderUnavailableError". */ runtimeFallback?: "default" | "aggressive" | "none" | { shouldFallback: (err: unknown) => boolean; }; /** * OTel-aligned observability hooks. Optional; each hook is independent. * Hooks are fire-and-forget — errors thrown by hook callbacks are swallowed * so observability instrumentation can't break inference. * * Coverage in this release (alpha.21): * - onCost / onTokenUsage / onCacheHit : emitted by the Registry on every * successful call against generateText, generateStructured, runAgent * (and on each cache-hit response). Streaming methods do not emit cost * yet (streamed cost surfacing is a follow-up, mirroring the alpha.7 * `walkChain` "no cost to record at stream-creation" behavior). * - onFallback : emitted by the Registry's `walkChain` whenever it * advances from one provider alias to the next due to runtime error, * budget rejection, or empty response. Per-call only; not emitted for * the initial selection or for `forceProviderAlias` calls (which by * contract don't fall back). * - onValidationRetry : hook type defined but not Registry-emitted in * alpha.21. Consumers wanting validation-retry observability should * use the adapter's existing `onRetry` hook and filter on * `reason === "validation-feedback"`. Registry-level emission is the * alpha.22 follow-up. * * Added in 0.1.0-alpha.21. Aligned with OpenTelemetry's `gen_ai.*` semantic * conventions where applicable; events are designed to map cleanly onto * spans and metrics in a downstream OTel pipeline. */ observability?: ObservabilityHooks; /** * Per-attempt timeout, in milliseconds. When set, every provider attempt * within `walkChain` is wrapped in an `AbortController` that fires after * this many milliseconds. The abort propagates to the adapter's HTTP * client; the adapter throws `ProviderUnavailableError`; the Registry's * `shouldFallback` predicate catches it and walks to the next provider * with a fresh timer. * * Use case: a reasoning model that grinds on hidden chain-of-thought can * otherwise hang for minutes before the AbortSignal is the only escape. * Set a tight per-attempt cap (e.g. 30000) and let the chain fall back to * a fast non-reasoning provider after the cap fires. Per-attempt, not * chain-wide — each provider gets its own budget. * * Composes with a caller-supplied `signal` on the call options: BOTH the * timeout and the caller's abort fire the same wrapped controller. The * shorter trigger wins. * * Default: undefined (no timeout). When set, applies to `generateText`, * `generateStructured`, and `runAgent` walkChain attempts. Stream methods * use the same timeout for stream-creation only (not mid-stream — once a * stream opens, mid-stream timeout is a per-chunk policy that lives in * the consumer's `for await`). * * Added in 0.1.0-alpha.23. */ perAttemptTimeoutMs?: number; /** * When true, suppress the alpha.26+ deprecation warnings that fire when a * call uses the legacy `{instructions, prompt}` shape instead of the * canonical `messages: LLMMessage[]`. The legacy path still works during * the alpha.26 window; suppression is a per-Registry opt-out for * consumers who have read the migration guide and are working through * the migration incrementally. * * Removed in alpha.27, when the legacy fields go with it. * * Added in 0.1.0-alpha.26. */ suppressDeprecationWarnings?: boolean; /** * Optional replacement for the deprecation warning's default emitter * (`console.warn`). Consumers wanting structured logging can supply a * function that receives the fully-formatted warning message. Removed * in alpha.27. * * Added in 0.1.0-alpha.26. */ deprecationWarningHandler?: (message: string) => void; } interface ModelSelection { alias: string; adapter: AdapterRegistration; modelId: string; pricing: ModelPricing; port?: LLMPort; embeddingsPort?: EmbeddingsPort; } declare class Registry { readonly config: RegistryConfig; readonly budget: BudgetBackend; readonly cost: CostBackend; readonly validationStrategy: ValidationStrategy; /** * Returns true if an error should cause the registry to walk to the next * viable provider in the fallback chain. See `RegistryOptions.runtimeFallback`. */ readonly shouldFallback: (err: unknown) => boolean; /** OTel-aligned observability hooks, set at construction. Read by `walkChain` + `RegistryPort`. */ readonly observability: ObservabilityHooks; /** Per-attempt timeout in ms, applied by `walkChain` to each provider attempt. (alpha.23+) */ readonly perAttemptTimeoutMs: number | undefined; /** Deprecation-warning dedup state for the alpha.26+ legacy `{instructions, prompt}` path. */ readonly warningState: WarningState; private readonly adapters; private readonly pricingOverrides; constructor(opts: RegistryOptions); /** Sanity-check that every provider's adapter exists and every task chain references real providers. */ private validateConfig; /** * Compose the gating storage key. When `budgetScope` is set, the backend * sees `${alias}|${scope}:${scopeId}` so configured caps apply per-scope. * Otherwise the key is just `${alias}` — backwards-compatible with every * release up to alpha.19.1. (alpha.20+) */ scopedKey(alias: string, budgetScope?: BudgetScopeRef): string; /** Resolve the first usable provider in the task's fallback chain. */ selectModel(taskType: string, priority?: 0 | 1 | 2 | 3, budgetScope?: BudgetScopeRef): Promise; /** * Resolve a single provider by alias, bypassing the task-routing chain. * Used by `forceProviderAlias` (alpha.7+). Per-provider budget gates still * apply (P0 priority bypasses them, matching `selectModel`). Throws * `NoProvidersAvailableError` if the alias is unconfigured or fails gating. */ selectByAlias(alias: string, priority?: 0 | 1 | 2 | 3, budgetScope?: BudgetScopeRef): Promise; /** * Resolve EVERY usable provider in the task's fallback chain, in order. * Used by the registry's port proxy to walk the chain on runtime errors * (alpha.7+). The eligibility checks (provider configured, adapter * registered, budget allows, cost cap allows, pricing exists) are the * same as `selectModel`; the difference is this method returns the full * viable list instead of just the first. * * Throws `NoProvidersAvailableError` if NO providers in the chain are * viable. Returns at least one `ModelSelection` otherwise. */ selectViableChain(taskType: string, priority?: 0 | 1 | 2 | 3, budgetScope?: BudgetScopeRef): Promise; /** Returns an LLMPort whose methods route to the selected adapter per call. */ getPort(): LLMPort; /** Returns an EmbeddingsPort whose methods route to the selected adapter per call. */ getEmbeddingsPort(): EmbeddingsPort; /** * Open a session-scoped cost gate. Returns a {@link CostSession} that * wraps an LLMPort with a hard USD cap, independent of the per-provider * hour/day/month gates. Throws `SessionBudgetExceededError` mid-loop * when the cap is reached. * * Designed for continuous-call workloads (screen capture loops, OCR * pipelines, multi-step agents) where a single stuck-open session can * otherwise burn arbitrary dollars. * * The returned session has its own LLMPort via `session.getPort()`; the * underlying registry's per-provider budget gates still apply on top. */ openCostSession(opts: OpenCostSessionOptions): CostSession; /** Introspection: list all provider aliases. */ listProviders(): ProviderEntry[]; /** Introspection: list all configured task routes. */ listTasks(): Array<{ task: string; chain: string[]; }>; /** * Compare bundled per-adapter pricing tables against each provider's live * model catalog (via `LLMPort.listModels()`). Reports drift: models bundled * but not exposed by the provider (deprecated), models exposed but not * bundled (newly launched), and per-model rate divergence when the provider * exposes pricing. * * Use as a CI / scheduled job to get a warning when a provider quietly * changes its catalog. The bundled pricing tables remain the source of * truth for cost computation; this method does NOT auto-update them. * * Adapters that don't implement `listModels()` (e.g. `adapter-vercel`) * are skipped and reported under `skipped`. * * Added in `0.1.0-alpha.9`. */ checkPricingFreshness(): Promise; } /** * Output of {@link Registry.checkPricingFreshness}. * * `checked` has one entry per adapter that successfully reported its live * model catalog; `skipped` lists adapters that don't implement listModels() * or whose call failed. */ interface PricingFreshnessReport { checked: PricingFreshnessAdapterReport[]; skipped: Array<{ adapter: string; reason: string; }>; } interface PricingFreshnessAdapterReport { adapter: string; liveModelCount: number; bundledModelCount: number; /** Models exposed by the provider but not in the bundled pricing table. */ addedModels: string[]; /** Models in the bundled pricing table but no longer exposed by the provider. */ removedModels: string[]; /** Models where bundled USD/1M differs from live USD/1M (when the API exposes pricing). */ priceDrift: Array<{ modelId: string; bundledInputPer1M: number; bundledOutputPer1M: number; liveInputPer1M: number; liveOutputPer1M: number; }>; } /** * Convenience factory matching the public API surface advertised in the README. * * const registry = createRegistryFromEnv({ adapters: { anthropic: ... } }); * const llm = registry.getPort(); */ declare function createRegistryFromEnv(opts: RegistryOptions): Registry; /** * declareTasks() — opt-in type safety for task definitions. * * TaskType is intentionally `string` at the LLMPort surface so the library * does not constrain users' task vocabularies. The cost is loose typing at * call sites. declareTasks() recovers most of the safety with autocomplete * and typo protection. * * Stated as "open with opt-in typing," not "better than enum." * * See implementation plan v3 §6.4 and decision 17. */ interface TaskConfig { priority?: LLMPriority; defaultTemperature?: number; defaultMaxOutputTokens?: number; description?: string; } /** * Returns a typed map of task-name keys to their literal-string task type. * * @example * const tasks = declareTasks({ * triage: { priority: 1, defaultTemperature: 0 }, * draft: { priority: 2, defaultTemperature: 0.4 }, * }); * * llm.generateText({ taskType: tasks.triage, prompt: "..." }); * // ^^^^^^^^^^^^^ autocomplete + typo-safe * * The runtime value of `tasks.triage` is the literal string "triage"; * the type is also the literal "triage", not the wider `string`. */ declare function declareTasks>(config: T): { [K in keyof T]: K & string; } & { __meta: T; }; /** Read the original TaskConfig back from a declareTasks() result. */ declare function getTaskConfig>(declared: { [K in keyof T]: K & string; } & { __meta: T; }, taskName: keyof T): TaskConfig | undefined; /** * In-memory implementations of BudgetBackend and CostBackend. * * Default for development and single-process deployments. For multi-process * deployments, swap in a Redis-backed implementation (separate package). * * Storage keys are arbitrary strings — the Registry composes them as * `${alias}` or `${alias}|${scope}:${scopeId}` (alpha.20+) to make gating * per-alias or per-scope. The backends don't need to know the schema. */ /** * In-memory implementation. Stores per-key arrays of request timestamps and * counts windowed views from them lazily. Supports per-minute, per-hour, and * legacy `requestsPerHour` (alpha.19 backwards compat). * * `perSession` from BudgetLimit is intentionally ignored here — session-scope * enforcement lives in CostSession (which is the only thing that knows * "this is the same session"). The backend exists for per-alias / per-scope * windowed enforcement. */ declare class InMemoryBudget implements BudgetBackend { private requests; recordRequest(key: string): Promise; check(key: string, limit: BudgetLimit): Promise; private prune; } declare class InMemoryCost implements CostBackend { private spends; recordCost(key: string, usd: number): Promise; check(key: string, limit: CostLimit): Promise; private prune; } /** * Cost computation: convert TokenUsage + ModelPricing → CostUsage. * * Adapters call this after every LLM request to compute the dollar cost * of the call from token counts and the model's pricing entry. The result * goes into both the result object (for caller observability) and the * CostBackend (for budget enforcement). */ /** * Compute USD cost for a chat/text completion call. * Cache reads (Anthropic feature) are billed at the discounted rate when present. */ declare function computeChatCost(usage: TokenUsage, pricing: ModelPricing): CostUsage; /** Compute USD cost for an embedding call (input tokens only). */ declare function computeEmbeddingCost(inputTokens: number, pricing: ModelPricing): CostUsage; /** * Typed error taxonomy for @llm-ports. * * Designed for typed-error-driven failover (alpha.18+). Consumers declare * which error classes route to which fallback chains rather than string- * matching error messages. The shape matches the field consensus that * emerged across LiteLLM, Pydantic AI, LangChain, Portkey, and Genkit; * LiteLLM's 11-class taxonomy is the closest analogue. * * Hierarchy (alpha.18): * * LLMPortError // common base for instanceof checks * ├── BadRequestError // 400-class root (client-fixable) * │ ├── ContextWindowExceededError // prompt too long for model * │ └── ContentPolicyViolationError // content filter rejected the request * ├── AuthenticationError // 401/403 (NOT retryable to same provider) * ├── RateLimitError // 429 with optional retryAfterMs * ├── BudgetExceededError // port-internal cap exhausted * ├── SessionBudgetExceededError // CostSession budget exhausted * ├── ServiceUnavailableError // 503 root (transient) * │ ├── ProviderUnavailableError // SDK error or unreachable; route to fallback * │ └── EmptyResponseError // model returned empty visible text * ├── NoProvidersAvailableError // entire chain exhausted * ├── ValidationError // structured-output Zod failure * ├── ContentBlockUnsupportedError // block kind unknown to adapter * ├── ConfigError // env/config malformed * ├── ImageTooLargeError // image over per-provider byte limit * └── InvalidImageUrlError // image URL form invalid * * Breaking changes vs alpha.17: * * - `ContextWindowExceededError` is NEW. Previously the underlying provider * SDK threw a generic 400, which got wrapped as `ProviderUnavailableError`. * With the new taxonomy it's parented under `BadRequestError`. Consumers * branching on `instanceof ProviderUnavailableError` after a context-window * overflow will need to add `instanceof BadRequestError` to their handlers. * * - `ContentPolicyViolationError` is NEW. Same shape change. * * - `ServiceUnavailableError` is NEW. `ProviderUnavailableError` and * `EmptyResponseError` are reparented under it. Existing `instanceof * ProviderUnavailableError` checks continue to work; new `instanceof * ServiceUnavailableError` checks catch both. * * - `AuthenticationError` is NEW. Previously 401/403 errors were wrapped as * `ProviderUnavailableError`, which is wrong because they're not transient * and shouldn't trigger same-provider retries. * * - `RateLimitError` is NEW. Previously 429 errors were wrapped as * `ProviderUnavailableError`, which lost the `retryAfterMs` information. * * - `LLMPortError` is NEW. All errors thrown by this library extend it. * Use `e instanceof LLMPortError` to catch any library error; * `errorMatchers.all` does this for fallback predicates. */ /** * Common base class for every error thrown by this library. Useful for * blanket catches: `try { ... } catch (e) { if (e instanceof LLMPortError) * { /* library error *\/ } }`. Direct subclass of `Error`, so existing * `instanceof Error` checks continue to work. */ declare class LLMPortError extends Error { readonly name: string; } /** * 400-class root. Indicates the request itself is malformed, too large, or * policy-violating. Adapters should NOT include this in default fallback * chains: another provider will reject the same request the same way. * * Use `LiteLLM`-style explicit `context_window_fallbacks` / * `content_policy_fallbacks` chains to handle 400s by routing to a model * with a larger window or more permissive policy. */ declare class BadRequestError extends LLMPortError { readonly alias: string; readonly cause?: Error | undefined; readonly name: string; constructor(alias: string, message: string, cause?: Error | undefined); } /** * Thrown when the prompt + tools + system content exceeds the model's * context window. Subclass of `BadRequestError`: fallback to another * provider with the same window will fail the same way. Route to a * larger-window model explicitly. */ declare class ContextWindowExceededError extends BadRequestError { readonly modelId: string; readonly contextLimit?: number | undefined; readonly observedTokens?: number | undefined; readonly name: string; constructor(alias: string, modelId: string, contextLimit?: number | undefined, observedTokens?: number | undefined, cause?: Error); } /** * Thrown when the provider's content filter / safety classifier rejected * the request. Not retryable on the same provider's same policy; may * succeed on a provider with different policy thresholds. */ declare class ContentPolicyViolationError extends BadRequestError { readonly modelId: string; readonly policyDetails?: string | undefined; readonly name: string; constructor(alias: string, modelId: string, policyDetails?: string | undefined, cause?: Error); } /** * Thrown when the provider rejected the request as unauthenticated or * forbidden. Not transient: retrying the same call against the same * provider with the same credential will fail again. Adapters should * NOT include this in default fallback chains; the credential needs to * be fixed externally. */ declare class AuthenticationError extends LLMPortError { readonly alias: string; readonly cause?: Error | undefined; readonly name: string; constructor(alias: string, message: string, cause?: Error | undefined); } /** * Thrown when the provider returned HTTP 429 (rate limited). When the * provider supplies a `Retry-After` or `retry-after-ms` header, the * adapter parses it and exposes it as `retryAfterMs`. Fallback chains * SHOULD honor this delay (planned for alpha.21+) and demote the alias * for exactly that duration rather than guessing with exponential * backoff. */ declare class RateLimitError extends LLMPortError { readonly alias: string; readonly retryAfterMs?: number | undefined; readonly cause?: Error | undefined; readonly name: string; constructor(alias: string, message: string, retryAfterMs?: number | undefined, cause?: Error | undefined); } /** * Thrown when a provider's request budget (count or USD) is exhausted. * Distinct from a provider-side 429: this is the local registry's * `cost:N/day` or `req:N/hour` gating, evaluated BEFORE the provider call. */ declare class BudgetExceededError extends LLMPortError { readonly alias: string; readonly limit: number; readonly current: number; readonly gatingKind: "requests" | "cost"; readonly name: string; constructor(alias: string, limit: number, current: number, gatingKind: "requests" | "cost"); } /** * Thrown when an active CostSession exceeds its USD budget. Distinct from * `BudgetExceededError` (which gates per-provider hour/day/month) so call * sites can recover differently: typically by closing the session and * informing the user, not by routing to a fallback provider. * * Use case: continuous screen-capture loops where one stuck-open window * could otherwise burn arbitrary dollars overnight. The session-scoped * cap is a hard backstop independent of the per-provider gates. */ declare class SessionBudgetExceededError extends LLMPortError { readonly sessionId: string; readonly budgetUSD: number; readonly spentUSD: number; readonly name: string; /** * Optional reason tag distinguishing which session-grain cap tripped * (e.g. `"tokens (50000 >= 50000)"`, `"tool_calls (8 >= 8)"`, * `"requests (100 >= 100)"`). When undefined the default USD cap * tripped. (alpha.20+) */ readonly grain?: string; /** * Backwards-compatible constructor. The first three args are the legacy * USD-cap shape; pass a fourth `grain` string when the cap that tripped * is one of the alpha.20+ token / tool_call / request session ceilings. * `budgetUSD` and `spentUSD` are repurposed as `cap` and `current` in * that case so the existing fields keep working for diagnostics. */ constructor(sessionId: string, budgetUSD: number, spentUSD: number, grain?: string); } /** * Root of the transient-failure tier (HTTP 502/503/504, network resets, * provider-side overload). Adapters should include this in default * fallback chains: another provider may serve the same request fine. */ declare class ServiceUnavailableError extends LLMPortError { readonly alias: string; readonly cause?: Error | undefined; readonly name: string; constructor(alias: string, message: string, cause?: Error | undefined); } /** * Thrown when a configured provider is unreachable, returned a non-typed * error, or is misconfigured. Subclass of `ServiceUnavailableError` so * the default fallback predicate routes it to the next provider. * * Reparenting note: in alpha.17 this extended `Error` directly. In alpha.18 * it extends `ServiceUnavailableError`. Existing `instanceof * ProviderUnavailableError` checks continue to work. */ declare class ProviderUnavailableError extends ServiceUnavailableError { readonly name: string; constructor(alias: string, cause: Error); } /** * Thrown by adapters when a model returns an empty/whitespace-only response * where one is structurally required (e.g. generateStructured needs JSON to * parse). Carries the model id + provider alias so the registry can route * to a fallback. Common cause: reasoning models that spent the entire output * budget on hidden reasoning tokens and produced no visible text. * * Reparenting note: in alpha.17 this extended `Error` directly. In alpha.18 * it extends `ServiceUnavailableError` so fallback predicates treat it as * a transient failure. */ declare class EmptyResponseError extends ServiceUnavailableError { readonly modelId: string; readonly hint?: string | undefined; readonly name: string; constructor(alias: string, modelId: string, hint?: string | undefined); } /** * Thrown when every provider in the task's fallback chain has been attempted * and none succeeded (each either errored, was budget-blocked, or was missing). */ declare class NoProvidersAvailableError extends LLMPortError { readonly taskType: string; readonly attempted: string[]; readonly reasons: Record; readonly name: string; constructor(taskType: string, attempted: string[], reasons: Record); } /** Thrown by validation strategies when generated structured output fails schema. */ declare class ValidationError extends LLMPortError { readonly issues: ZodIssue[]; readonly attempts: number; readonly name: string; constructor(issues: ZodIssue[], attempts: number); } /** Thrown when a content block kind is sent to an adapter that does not support it. */ declare class ContentBlockUnsupportedError extends LLMPortError { readonly adapter: string; readonly blockType: string; readonly name: string; constructor(adapter: string, blockType: string); } /** Thrown by the registry when env config is malformed. */ declare class ConfigError extends LLMPortError { readonly name: string; constructor(message: string); } /** * Thrown at Registry entry when a call is dispatched with neither `messages` * nor the legacy `prompt` set. Exactly one of the two must be supplied. * (alpha.26+) */ declare class MessagesRequiredError extends LLMPortError { readonly method: string; readonly name: string; constructor(method: string); } /** * Thrown at Registry entry when `messages` is supplied but empty. Every * provider requires at least one message. (alpha.26+) */ declare class EmptyMessagesError extends LLMPortError { readonly method: string; readonly name: string; constructor(method: string); } /** * Thrown by the `toMessages` helper when called without a `prompt`. The * helper is designed to migrate the legacy `{instructions, prompt}` shape; * calling it with no prompt is a caller bug. (alpha.26+) */ declare class PromptRequiredError extends LLMPortError { readonly name: string; constructor(); } /** * Thrown at Registry entry when a caller mixes the alpha.26 canonical * `messages` shape with the deprecated `{instructions, prompt}` shape. * Ambiguity is a caller bug worth surfacing. Post-alpha.27 (which removes * the deprecated fields) this error is unreachable. (alpha.26+) */ declare class MessagesConflictError extends LLMPortError { readonly method: string; readonly conflictingFields: readonly string[]; readonly name: string; constructor(method: string, conflictingFields: readonly string[]); } /** * Thrown by adapters whose underlying provider does not structurally * support mid-conversation system-role messages. Anthropic's `system` is * a top-level request field distinct from `messages`; Google Gemini's * `systemInstruction` is similar. Both providers reject a `messages` * array that contains a system-role message after any user or assistant * message; the adapter surfaces this typed error at the boundary rather * than silent flattening or a confusing provider error. * * Adapters whose provider tolerates mid-conversation system messages * (OpenAI, Ollama, Vercel) pass them through inline and never throw * this error. See per-adapter docs for the policy. * * (alpha.27+) */ declare class NonContiguousSystemError extends LLMPortError { readonly alias: string; readonly method: string; readonly messageIndex: number; readonly name: string; constructor(alias: string, method: string, messageIndex: number); } /** * Thrown by adapters when an image content block exceeds the provider's * per-image byte limit. Caught at the adapter boundary BEFORE the SDK call, * so the caller sees a typed error instead of an opaque 413/400 wrapped as * ProviderUnavailableError. * * Each adapter knows its own default limit (Anthropic 5MB, OpenAI 20MB, * Ollama model-dependent), and the limit can be overridden per-adapter at * port creation via `imageSizeLimitBytes`. * * `imageIndex` is the 0-indexed position of the offending image in the * caller's `prompt` ContentBlock[] array. */ declare class ImageTooLargeError extends LLMPortError { readonly alias: string; readonly imageIndex: number; readonly byteSize: number; readonly limitBytes: number; readonly name: string; constructor(alias: string, imageIndex: number, byteSize: number, limitBytes: number); } /** * Thrown by adapters when an image content block's URL form is malformed: * `file://` scheme, `data:` URI passed as `kind: "url"` instead of base64, * or a URL with no scheme. Caught at the adapter boundary BEFORE the SDK call. */ declare class InvalidImageUrlError extends LLMPortError { readonly alias: string; readonly url: string; readonly reason: string; readonly name: string; constructor(alias: string, url: string, reason: string); } /** * Thrown when the provider's account or billing headroom is exhausted. * Distinct from `AuthenticationError` (which reflects a genuine wrong-key * or invalid-credential problem) because the credential is valid; the * account just has no billing headroom. Distinct from * `BudgetExceededError` (which is port-internal gating) because this * fires on the provider's side. * * Walk-worthy: a different vendor with a fresh billing state can succeed. * A single provider's credit-exhaustion surface today varies across * response shapes (401 with a credit-exhaustion body, 403, 429 with * "credit_balance too low", 400 with a "your account has no credit" * message). Adapters classify into this typed class using the same * message patterns collected in `AGGRESSIVE_CREDIT_EXHAUSTION_PATTERNS`. * * Added in alpha.28 pre-work (TD-LLMP-19). Prior to alpha.28, downstream * consumers walked on `AuthenticationError` as a proxy for this condition; * that over-walked on genuine wrong-key auth failures. Separating the * classes lets `defaultShouldFallback` walk on `CreditExhaustionError` * while aborting on true `AuthenticationError`. */ declare class CreditExhaustionError extends LLMPortError { readonly alias: string; readonly cause?: Error | undefined; readonly name: string; constructor(alias: string, message: string, cause?: Error | undefined); } /** * Thrown when a provider returns HTTP 400 with an empty or unparseable * body (no descriptive error body explaining what was malformed). This * condition surfaces on Cerebras when a structured-output request uses * a strict schema the provider cannot handle, and on some OpenAI-compat * providers when the payload triggers an upstream provider-side bug that * short-circuits before the descriptive error is emitted. * * Walk-worthy: another provider may accept the same request. Distinct * from generic `BadRequestError` (which is unclassified 400 and most * likely identical across providers) because this specific mode has an * observed pattern of provider-specific quirks that walking recovers * from. * * Added in alpha.28 pre-work (TD-LLMP-19). Prior to alpha.28, downstream * consumers walked on generic `BadRequestError` as a proxy for this * condition; that over-walked on genuine malformed requests that fail * identically on every provider. Separating the classes lets * `defaultShouldFallback` walk on `ProviderMalformed400Error` while * aborting on true generic `BadRequestError`. */ declare class ProviderMalformed400Error extends BadRequestError { readonly name: string; } /** * Thrown when the adapter or registry code itself throws synchronously * (TypeError, ReferenceError, JS runtime bug, unchecked precondition * violation) BEFORE reaching the provider. Distinct from * `ServiceUnavailableError` (which reflects a provider-side failure) * because this is a bug in the port library, not a problem with the * provider. * * NOT walk-worthy: another provider will produce the same local error * at every hop. Walking the chain for a client-side bug multiplies * latency and cost, and misdirects operator diagnostic to provider * status pages when the fix is inside `@llm-ports`. * * Adapter code SHOULD wrap only the provider-call block (network call * plus immediate response handling) in the `errorMatchers` try/catch. * Local synchronous throws before the network call SHOULD propagate as * `AdapterInternalError` so the registry does not fail over on them. * * Added in alpha.28 pre-work (TD-LLMP-17). Prior to alpha.28, local * TypeErrors from adapter code were wrapped as `ServiceUnavailableError` * by `wrapProviderError`'s fallback branch, which then triggered futile * chain-wide failover with the same error re-thrown at every hop. */ declare class AdapterInternalError extends LLMPortError { readonly alias: string; readonly cause?: Error | undefined; readonly name: string; constructor(alias: string, message: string, cause?: Error | undefined); } /** * Predicates for typed-error-driven fallback. Pass one of these as the * `shouldFallback` argument when configuring per-call or registry-level * `runtimeFallback` (planned alpha.21 surface) instead of writing inline * `instanceof` chains or string-match checks. * * Semantics: * - `rateLimit`: true only for `RateLimitError`. Use when you want to * fall back ONLY on 429. * - `transient`: true for `RateLimitError` and any `ServiceUnavailableError` * subclass (including `ProviderUnavailableError` and `EmptyResponseError`). * Use for the conservative "retry on network-level transients" policy. * - `default`: true for any `LLMPortError` EXCEPT `BadRequestError` * subclasses (`ContextWindowExceededError`, `ContentPolicyViolationError`) * and `AuthenticationError`. Use for the "fall back on anything not * client-fixable" policy. This is the recommended default. * - `all`: true for any `LLMPortError`. Use when you want to fall back * on every library error, including client-fixable ones (rarely * correct, but offered for completeness). */ declare const errorMatchers: { rateLimit: (e: unknown) => boolean; transient: (e: unknown) => boolean; default: (e: unknown) => boolean; all: (e: unknown) => boolean; }; /** * Body-pattern list for credit-exhaustion / account-issue 400s. Matched * case-insensitively against `error.message` on a `BadRequestError`. * Expanded conservatively: the intent is "the account cannot serve ANY * call right now"; genuinely malformed requests (invalid parameter, * schema failure) fail identically on every provider and MUST NOT walk * the chain. * * Exported so consumers who want to extend this list (uncommon) or run * the same matcher outside the fallback path can reuse the canonical * regex set. */ declare const AGGRESSIVE_CREDIT_EXHAUSTION_PATTERNS: readonly RegExp[]; /** * The opinionated classifier bundled with `RegistryOptions.runtimeFallback: * "aggressive"`. Walks the provider chain on any provider-side signal that * means "try a different provider" — not just the narrow default of * `ProviderUnavailableError`. * * Bundled empirically after BEPA (Plan 29), HomeSignal, and SalesCoach * (Plan 30 §R2 + §5.4) each rebuilt the same classifier by hand. The * common thread: the default `runtimeFallback: "default"` only walks on * `ProviderUnavailableError`, which let credit-exhaustion 400s and * empty-response 200s abort the chain in production and caused hours-long * outages before the consumer patched their own classifier. * * Walks on: * 1. `ProviderUnavailableError` — the default (5xx, network, generic). * 2. `RateLimitError` — try the next provider immediately, don't wait * out the retry-after backoff on this one. * 3. `EmptyResponseError` — the adapter's own retries gave up; a * different provider may not exhibit the pattern. * 4. `ContextWindowExceededError` — this provider can't handle the * input; try a provider with a larger window. * 5. `BadRequestError` matching credit-exhaustion / account body * patterns (see {@link AGGRESSIVE_CREDIT_EXHAUSTION_PATTERNS}). * 6. Any raw error with `status >= 500` (defensive for adapters that * don't wrap 5xx as `ProviderUnavailableError`). * * Does NOT walk on: * - `AuthenticationError` (401/403 — credential needs to be fixed). * - Generic `BadRequestError` (malformed request — would fail everywhere). * - `ContentPolicyViolationError` (content-filter — separate concern). * - `BudgetExceededError` / `SessionBudgetExceededError` (port-internal * gating; walking would defeat the gate). * - `NoProvidersAvailableError` (chain-level; nothing more to try). * - Errors that aren't `LLMPortError` and don't look like a 5xx. * * Composability: consumers who want everything this classifier does PLUS * one extra condition can wrap it inline (`(e) => aggressiveShouldFallback(e) * || myExtraCheck(e)`). Consumers who want to disable one of these * categories can wrap and short-circuit similarly. */ declare function aggressiveShouldFallback(err: unknown): boolean; /** * The canonical walk-table policy for `RegistryOptions.runtimeFallback: * "default"` starting alpha.28. Codifies the outcome of the 2026-07-21 * cross-consumer review pass; supersedes hand-rolled per-consumer * classifiers (BEPA `src/ai/llm.ts:357-379`, ADW `registry.ts:170`, etc.) * that each mis-attributed one or more error classes. * * **Walks (transient / provider-varying):** the caller has a reasonable * chance of success on a different provider. * * - `RateLimitError` — this provider's 429; another provider may have * headroom. * - `ServiceUnavailableError` (and subclasses `ProviderUnavailableError`, * `EmptyResponseError`) — transient 5xx / SDK errors; walk. * - `ContextWindowExceededError` — different providers have different * context windows. A 150k-token request rejected by a 128k-window * provider can succeed on a 400k-window provider. * - `ContentPolicyViolationError` — different providers apply different * content policies. Anthropic refuses some things OpenAI accepts and * vice versa. * - `ImageTooLargeError` — different attachment size limits per provider. * - `ContentBlockUnsupportedError` — different multimodal capabilities. * - `CreditExhaustionError` — this provider's billing headroom is spent; * a different vendor's account is unaffected. * - `ProviderMalformed400Error` — provider returned 400 with an empty * or unparseable body (typically a provider-side bug on complex * schemas); another provider may accept the same request. * * **Aborts (deterministic / same across providers):** walking wastes N * provider API calls with the same identical error at every hop. * * - `AuthenticationError` — a truly wrong or expired credential does * not fix on the next provider, and each hop leaks the failure across * vendors. * - Generic `BadRequestError` (unclassified 400) — most likely identical * across providers (missing field, invalid JSON, malformed message * role). Consumers who observe a specific provider-quirk 400 that * walking recovers from should upgrade `wrapProviderError`'s * classification to emit `ProviderMalformed400Error` (or another * narrower typed class) instead of over-walking on the generic parent. * - `MessagesRequiredError`, `EmptyMessagesError`, `MessagesConflictError`, * `PromptRequiredError`, `NonContiguousSystemError` — contract-level * violations. Deterministic. Walking is pure waste. * - `InvalidImageUrlError` — universally invalid URL. Walking is waste. * - `AdapterInternalError` — port library's own bug. Walking multiplies * latency and cost, and misdirects operator diagnostic. The fix lives * inside `@llm-ports`, not in another provider. * - Unknown error classes — default is conservative: unclassified failures * are, empirically, most likely identical across providers. * * Consumers wire this via `runtimeFallback: { shouldFallback: * defaultShouldFallback }` on `createRegistryFromEnv`. To extend (e.g. add * a consumer-specific typed class), wrap: * * ```ts * shouldFallback: (err) => defaultShouldFallback(err) || (err instanceof MyClass) * ``` * * To narrow (e.g. abort on `ContentPolicyViolationError` because policy * variance is not useful for your workload), wrap: * * ```ts * shouldFallback: (err) => * err instanceof ContentPolicyViolationError * ? false * : defaultShouldFallback(err) * ``` * * Distinct from `aggressiveShouldFallback` (bundled with * `runtimeFallback: "aggressive"`): the aggressive preset matches on * message-body patterns (via `AGGRESSIVE_CREDIT_EXHAUSTION_PATTERNS`) * rather than the typed `CreditExhaustionError` class; use aggressive when * an adapter has not yet upgraded to emit the typed class, and use * `defaultShouldFallback` when your adapter stack emits the typed classes * consistently. */ declare function defaultShouldFallback(err: unknown): boolean; /** * Fire-and-forget invocation of the `onRetry` observability hook. * * Adapters fire this on every retry attempt for the reasons documented in * `RetryReason`. Hooks are called fire-and-forget: errors thrown synchronously * or rejected promises are swallowed silently so observability code can never * cancel a retry or crash the request. * * Hoisted from per-adapter copies in alpha.3 so every adapter shares the same * semantics. Adapters that wrote their own `emitRetry` (adapter-openai, * adapter-vercel) now import this instead. */ /** * Invoke `onRetry` with the given event, fire-and-forget. * * - If `onRetry` is undefined, returns immediately. * - If `onRetry` throws synchronously, the error is swallowed. * - If `onRetry` returns a rejected promise, the rejection is swallowed. * * Observability hooks are not allowed to affect retry behavior. This helper * enforces that invariant. */ declare function emitRetryEvent(onRetry: OnRetry | undefined, event: RetryEvent): void; /** * Runtime capability discovery utility — shared across all adapters. * * Providers don't expose programmatic capability discovery (no API endpoint * tells you "this model rejects custom temperature" or "this model doesn't * support response_format: json_object"). Hardcoded capability tables go * stale every time a new model ships. * * The pattern that works: * * 1. The adapter tries the call with the parameter included. * 2. If the provider returns a 400 matching a known "deprecated parameter" * shape, the adapter calls `learner.remember(modelId, { ... })`. * 3. The adapter strips the parameter and retries the call. * 4. Every subsequent call in this process applies the learned constraint * up front. No re-discovery. * * Each adapter contributes its own error classifiers (which provider error * shape signals which capability rejection) and its own static catalog of * known rejectors (so first-call discovery can be skipped for models we * already know reject the parameter). The pattern itself — the Map, the * accumulation, the user-override layering, the static-catalog seeding — is * what this module provides. * * Hoisted from adapter-openai's per-adapter copy in alpha.3 so every adapter * shares the same discovery machinery. */ /** * One entry in the static "we already know this model rejects X" catalog. * * Adapters maintain their own catalog (the exact model patterns are * provider-specific) and pass it to `learner.seedFromCatalog(modelId, catalog)` * at port creation time. Matching entries pre-seed the learned-constraint * Map so the first call skips the discovery round-trip. */ interface KnownModelConstraint { /** Regex matched against the model id, e.g. `/^claude-opus-4-5/`. */ pattern: RegExp; /** The constraint to remember when the pattern matches. */ constraints: Partial; } /** * Per-process learner of per-model capability constraints. Each call to * `createCapabilityLearner()` returns a fresh learner with its own Map. * * Adapters create one learner per adapter context (typically per LLMPort * instance) and reference it in their retry loops. The learner is internal * state of the adapter; it's not part of any port's public surface. */ interface CapabilityLearner { /** * Resolve the effective capabilities for a model. User-supplied * capabilities (passed via `pricingOverrides[modelId].capabilities`) * override learned ones; learned ones override defaults. */ get(modelId: string, userSupplied?: ModelCapabilities): ModelCapabilities; /** * Record a discovered constraint after the provider returns an error that * signals the capability rejection. Accumulates: subsequent calls add more * constraints to the same model entry without losing prior ones. */ remember(modelId: string, constraints: Partial): void; /** * Test-only: clear all learned state. Should not be used in production code. */ _reset(): void; /** * Seed the learner with static "we already know this model rejects X" * entries. Adapters call this at port creation with their per-provider * catalog. Pre-seeding skips the first-call discovery round-trip. * * Idempotent: re-seeding the same catalog adds the same constraints. The * underlying Map is set-based; duplicate entries are harmless. */ seedFromCatalog(modelId: string, catalog: readonly KnownModelConstraint[]): void; /** * True if the learner has already learned (or been seeded with) a given * capability flag for a model. Used by `emitFirstLearningWarning` to fire * the click-to-file URL once per modelId per process. */ hasLearned(modelId: string, capabilityFlag: keyof ModelCapabilities): boolean; } /** Factory: returns a fresh capability learner with no learned state. */ declare function createCapabilityLearner(): CapabilityLearner; /** * Notification mechanism for runtime-learned capability constraints. * * When an adapter discovers at runtime that a model rejects a parameter, * it builds a pre-filled GitHub New Issue URL and prints it via console.warn * once per modelId+capability per process. Users see the warning, can click * the URL, and submit a well-structured issue with model id + error message * + adapter version + SDK version already filled in. * * This is intentionally NOT telemetry: no automatic data exfiltration, no * phone-home endpoint, no opt-in flag. Maintainers see signal only when a * user takes an explicit action (clicking the URL). The pattern preserves * user privacy AND gives maintainers actionable signal when something new * shows up in the wild. */ /** * Inputs for the click-to-file warning + URL builder. * * Adapters populate this at the moment they learn a new constraint. The * `packageName` field is what appears as a label and in the title; it MUST * be the npm package name (e.g. `@llm-ports/adapter-anthropic`), not a * short alias. * * The `capability` field is the constraint name that was learned (e.g. * `temperatureLocked`, `jsonModeUnsupported`). Adapters can also pass * a free-form string for capabilities not in the standard `ModelCapabilities` * enum — the URL builder treats it as opaque. */ interface FirstLearningEvent { /** Full npm package name, e.g. `@llm-ports/adapter-anthropic`. */ packageName: string; /** Provider-side model id that exhibited the constraint, e.g. `claude-opus-4-5`. */ modelId: string; /** Constraint name that was learned, e.g. `temperatureLocked`. */ capability: string; /** The error message the provider returned (used to title the issue + verify). */ providerErrorMessage: string; /** The installed adapter version, e.g. `0.1.0-alpha.3`. */ adapterVersion: string; /** The installed SDK version (e.g. `@anthropic-ai/sdk` version). */ sdkVersion: string; /** * Base URL of the project's GitHub repository. The URL builder appends * `/issues/new?title=...&body=...&labels=...` to this. Default in the * builder is `https://github.com/baabakk/llm-ports`. */ repoUrl?: string; } /** * Build a pre-filled GitHub New Issue URL for a runtime-learned constraint. * * The URL opens GitHub's New Issue form with title, body, and labels already * populated from the event. The user reviews + edits + submits in seconds. * * Exported for testing and for adapters that want to surface the URL in a * non-console channel (logger, Sentry custom event, etc.). The default * caller is `emitFirstLearningWarning` below which prints the URL via * `console.warn`. */ declare function buildLearningIssueUrl(event: FirstLearningEvent): string; /** * Emit a `console.warn` with the click-to-file URL for a runtime-learned * constraint. Fires exactly once per (modelId, capability) pair per process. * * Adapters call this from inside their `rememberConstraint`-equivalent path * the first time a specific constraint is learned. Subsequent calls to * `remember` the same constraint stay silent (the adapter's retry loop * still operates normally; only the warning is gated). */ declare function emitFirstLearningWarning(event: FirstLearningEvent): void; /** Test-only: reset the per-process warning state. */ declare function _resetWarnedState(): void; /** * Idempotent error-wrapping helper for adapter implementations. * * Every adapter's `messages.create` / `chat.completions.create` call wraps * the inner SDK call in try/catch and pipes the error through this helper * before throwing it out of the LLMPort method. The contract: * * - Typed framework errors pass through unchanged. These are intentional * signals from upstream code; double-wrapping them would hide their * type from the caller's try/catch. * * - HTTP-shaped SDK errors get classified into the new alpha.18 typed * taxonomy: 400-context-window → ContextWindowExceededError; 400-policy * → ContentPolicyViolationError; 401/403 → AuthenticationError; 429 → * RateLimitError (with parsed retryAfterMs); 502/503/504 → * ServiceUnavailableError; everything else → ProviderUnavailableError. * * - Non-Error values (strings, undefined, primitives) are stringified * into an Error first, then classified. * * Hoisted from per-adapter copies in alpha.3; HTTP classification added in * alpha.18 (TD-LLMPORTS-TYPED-ERRORS). */ /** * Wrap an unknown caught error as a typed framework error. * * Idempotent on every `LLMPortError` subclass (including subclasses added * in alpha.18: `BadRequestError`, `AuthenticationError`, `RateLimitError`, * `ServiceUnavailableError`, and their descendants). * * For raw SDK errors, classifies into the right typed class by HTTP status * + message pattern. For non-Error inputs, stringifies first. * * Adapter authors can opt into more precise typing by extracting their * SDK-specific error fields (e.g. OpenAI's `param`, Anthropic's * `error.type`) and constructing the typed error directly; this helper is * the catch-all fallback when adapters haven't customized. * * The optional `modelId` argument, added in alpha.28 pre-work (TD-LLMP-16), * is threaded into the `ContextWindowExceededError` and * `ContentPolicyViolationError` constructors so the resulting error carries * the model name that was in play at request-construction time. Adapter * authors should pass `req.modelId` (or equivalent) at every call site * inside a per-call code path; call sites outside a per-call path (e.g. * `listModels`) may omit it. When omitted, the error's `modelId` field * falls back to the legacy `"(unknown)"` placeholder for backwards compat. */ declare function wrapProviderError(alias: string, err: unknown, modelId?: string): Error; /** * Convert a `MessageContent` (either a plain string or an array of content * blocks) into a single string. Used by adapter implementations when they * need to log a prompt, fall back from rich content to plain text, or * inject a string-shaped prompt into a provider that doesn't accept block * arrays for a given message role. * * Non-text blocks (image, audio, tool_use, tool_result) are rendered as * `[block-type ...]` placeholders. This preserves the structural information * without producing garbage if the caller later logs the string. * * Hoisted from per-adapter copies in alpha.3. Every adapter that previously * wrote its own `stringifyPrompt(content)` now imports this. */ /** * Render `MessageContent` as a string. Text blocks contribute their text * verbatim; other block types contribute a `[type ...]` placeholder. */ declare function stringifyContentBlocks(content: MessageContent): string; /** * JSON helpers used by adapters for structured-output and streaming-structured * paths. * * - `extractJSON(raw)`: parses a string that may include markdown fences * and surrounding prose into a JavaScript value. Throws on parse failure * (callers handle the failure via the retry-with-feedback validation * strategy upstream). Falls back to `jsonrepair` when plain JSON.parse * fails, which catches trailing commas, single quotes, smart quotes, * Python-style None/True/False, unquoted keys, comments, and most other * LLM-quirk syntactic issues. * * - `tryParsePartialJSON(buffer)`: best-effort parse of an in-progress * streaming buffer. Returns `null` if no JSON can yet be recovered; * returns the parsed value if balancing the buffer's open braces / * brackets and trimming trailing commas produces a valid parse. * * Hoisted from per-adapter copies in alpha.3. jsonrepair fallback added in * alpha.5 to reduce retry-with-feedback round-trips on syntactic quirks. */ /** * Parse a JSON value out of a string that may be wrapped in markdown fences * or have leading/trailing prose. Throws on parse failure. * * Strategy: * 1. If wrapped in a ```json ... ``` or ``` ... ``` fence, extract the * inner content. * 2. Find the first `{` and the last `}`. If both exist and `{` precedes * `}`, parse the slice. * 3. If the parse fails, run the candidate through `jsonrepair` and try * again. This catches trailing commas, smart quotes, single quotes, * Python literals, unquoted keys, comments, and most syntactic LLM * quirks before we waste a retry-with-feedback round-trip. * 4. If repair also fails, surface the underlying SyntaxError to the * caller (validation strategy decides whether to retry against the LLM). */ declare function extractJSON(raw: string): unknown; /** * Best-effort parse of an in-progress streaming JSON buffer. * * Returns the parsed value if either: * - The buffer is already complete and parses cleanly, OR * - Balancing the buffer's open `{`/`[` against close `}`/`]` and trimming * trailing commas produces a valid parse. * * Returns `null` if no `{` is present yet, or if no balancing strategy * recovers a valid JSON value. Adapters call this on every streaming chunk * append; the result is yielded as a `Partial` to consumers. */ declare function tryParsePartialJSON(buffer: string): unknown | null; /** * Token-usage helpers shared across adapters. * * Each adapter has its own `parseUsage(response)` because provider response * shapes differ. But once parsed into a `TokenUsage` value, the math (adding * usage across multiple turns of an agent loop, or across multiple retries * of a structured-output call) is identical. * * Hoisted from per-adapter copies in alpha.3. */ /** * Add two `TokenUsage` values. Preserves the optional `cacheReadTokens` and * `reasoningTokens` fields when at least one operand has them. * * Used by adapters in `runAgent` (each tool-use step contributes usage) and * in `generateStructured` (when retry-with-feedback fires; each attempt * contributes usage). */ declare function mergeTokenUsage(a: TokenUsage, b: TokenUsage): TokenUsage; /** * Deterministic programmatic repair for Zod validation failures. * * Inspects a `ZodError`'s issues and applies type-coercion / null-deletion * fixes BEFORE the validation-strategy fires a retry-with-feedback round-trip * against the LLM. Each repair pattern represents a known LLM quirk that an * LLM round-trip is overkill to fix. * * The 8 patterns this catches (each one avoids ~1 LLM retry): * * 1. `null` where a non-null type is expected → delete key * (lets `.optional()` schemas accept the absence) * 2. string `"9"` where `number` is expected → `Number("9")` → `9` * 3. string `"true"` / `"false"` where `boolean` is expected → real booleans * 4. number `9` where `string` is expected → `String(9)` → `"9"` * 5. enum value with case / whitespace / markdown drift * (`"HIGH"`, `"**low**"`, `'"medium"'`, "Low ") → * strip wrappers + `.toLowerCase().trim()` * 6. `null` inside an optional union (`z.string().nullable().optional()`) → * delete key * 7. (alpha.13+) stringified JSON where object/array expected: * `'{"a":1}'` → `JSON.parse(...)` → `{a: 1}` when the string starts/ends * with `{}` or `[]`. Catches the "model double-encodes nested objects" * quirk seen on some compat providers (e.g. MiniMax returns * `reasoning: "{\"experience\": ...}"` for an object-typed field). * 8. (alpha.13+) array where object expected with a single-element array * containing an object: `[{...}]` → `{...}`. Catches "model wrapped a * singular field as an array" misreads of the schema. * * Strategy: * * 1. Run `schema.safeParse(raw)`. * 2. If it fails, call `attemptRepair(raw, error)` to produce a repaired * candidate (does NOT mutate the input). * 3. Re-run `schema.safeParse(repaired)`. * 4. If the second parse succeeds, return it. Saves an LLM round-trip. * 5. If the second parse still fails, hand off to the validation strategy * (retry-with-feedback OR fallback-to-next-provider, configured at port * creation). * * Ported from BEPA (Babak's Executive Personal Assistant) where this repair * has been running in production for ~6 months. The 6 patterns are exactly * the ones BEPA observed across millions of LLM calls against Claude, * GPT, GPT-OSS, Qwen, Cerebras gpt-oss, and Ollama models. * * Note on Zod compatibility: this code reads `issue.code` and `issue.expected` * which are stable across Zod v3 + v4. The `invalid_enum_value` code name * changed to `invalid_value` in Zod v4; we match both. */ /** * Apply deterministic repairs to `raw` based on a `ZodError`. Returns a * structurally-cloned, repaired copy. Does not mutate the input. * * Safe to call even if `raw` is not an object — non-object input is returned * as-is. */ declare function attemptValidationRepair(raw: unknown, error: ZodError): unknown; /** * Adapter-boundary validation for `ImageBlock` content. * * Catches two classes of errors before the SDK call so the caller sees a * typed error instead of an opaque provider 4xx wrapped as * `ProviderUnavailableError`: * * - **Size**: base64-encoded image exceeds the provider's documented limit. * Throws `ImageTooLargeError` carrying the byte size + the limit. * * - **URL shape**: URL-form image with a `file://`, `data:`, or * no-scheme URL. Throws `InvalidImageUrlError` carrying the offending * URL + reason. * * Each adapter calls `validateImageBlocks(blocks, opts)` on every outgoing * `ContentBlock[]` before constructing the provider-native payload. * * Limits and behavior are intentionally adapter-tunable: Anthropic ships * with a 5MB default, OpenAI with 20MB, Ollama with no enforced limit (model- * dependent; caller responsibility). Each adapter wires its own default * through `validateImageBlocks({ limitBytes })`. */ interface ValidateImageOptions { /** Provider alias for error messages. */ alias: string; /** * Maximum bytes per base64 image. If undefined, size validation is skipped * (use this for Ollama where the limit is model-dependent). */ limitBytes?: number; /** * Whether to allow `file://` URLs. Default false — file URLs almost always * indicate a caller mistake (the file is on the caller's machine; the * provider can't reach it). */ allowFileUrl?: boolean; } /** * Validate every ImageBlock in a ContentBlock array. Throws on the first * violation with a typed error. * * Non-image blocks are skipped. Recursively descends into ToolResult blocks * since those can carry nested ImageBlocks. */ declare function validateImageBlocks(blocks: ReadonlyArray, opts: ValidateImageOptions): void; /** * Validate an image URL's shape. Rejects `file://`, `data:`, and * no-scheme strings. Accepts `http://` and `https://`. Use `allowFileUrl` * to override (test environments may want it). */ declare function validateImageUrl(url: string, alias: string, allowFileUrl: boolean): void; /** * AbortSignal entry-time check helper for adapter port methods. * * Each adapter calls `throwIfAborted(signal)` at the top of every port * method that accepts a `signal?: AbortSignal`. This avoids the boilerplate * of "if (signal?.aborted) throw signal.reason ?? new Error(...)" repeated * at every entry, and ensures consistent error shape across adapters. * * When the signal is undefined or not yet aborted, this is a no-op. * * The mid-flight cancellation (the more important half of the contract) * happens by passing `signal` to the underlying SDK call. This helper only * covers the entry-time fast-path. */ /** * Throw if the given AbortSignal has already been aborted. No-op when * `signal` is undefined or not yet aborted. * * Honors `signal.reason` when present (modern AbortController convention); * falls back to a generic `AbortError`-shaped error otherwise. */ declare function throwIfAborted(signal: AbortSignal | undefined): void; /** * `toMessages`, `sys`, `usr` — migration + construction helpers for the * canonical `messages: LLMMessage[]` input introduced in alpha.26. * * `toMessages(instructions, prompt)` is the one-line migration shim from the * legacy `{instructions, prompt}` shape. `sys()` and `usr()` are idiomatic * message-array constructors for hand-written call sites. * * None of these helpers do runtime validation beyond what's needed for * shape correctness — they're primitives, not gate-keepers. Empty strings, * empty content blocks, and other edge cases pass through and let the * provider throw its native error. * * Added in `0.1.0-alpha.26` (issue #TBD). */ /** * Convert the legacy `{instructions, prompt}` shape into the canonical * `messages: LLMMessage[]` shape. The one-line migration shim for * consumers upgrading from alpha.25 or earlier. * * Semantics: * - If `instructions` is a non-empty string, emits a system-role message * with that content first. * - Emits a user-role message with `prompt` as the content. * - Throws `PromptRequiredError` if `prompt` is missing — the shim is * designed for the migrate-single-turn-call use case, and a missing * prompt is a caller bug. * * @example * port.generateText({ * taskType: "triage", * messages: toMessages(SYSTEM_PROMPT, userInput), * }); */ declare function toMessages(instructions: string | undefined, prompt: MessageContent): LLMMessage[]; /** * Idiomatic system-role message constructor. * * Accepts a plain string only. System prompts are almost always text; the * rare multimodal-system case constructs via object literal * (`{ role: "system", content: [...] }`). * * @example * port.generateText({ * taskType: "triage", * messages: [sys("Classify the message urgency."), usr(rawEmailBody)], * }); */ declare function sys(content: string): LLMMessage; /** * Idiomatic user-role message constructor. * * Accepts either a plain string or a structured `MessageContent` array * (text + image + audio content blocks). * * @example * port.generateText({ * taskType: "describe", * messages: [ * sys("Describe the image concisely."), * usr([{ type: "text", text: "What's in this?" }, imageBlock]), * ], * }); */ declare function usr(content: MessageContent): LLMMessage; /** * Shared adapter helper for resolving the canonical `messages` + `instructions` * pair from the alpha.26 dual-shape call options. * * The Registry normalizes `{ instructions, prompt }` into `messages` before * dispatch, so in practice this helper mostly reads `options.messages`. * When called directly (bypassing the Registry), the helper also honors * the legacy `{ instructions, prompt }` fields. * * Semantics: * - If `options.messages` is set: extract the LEADING contiguous system- * role messages into a concatenated `instructions` string (Anthropic * + Google adapters use a separate system field; the OpenAI shape keeps * system inline but the helper centralizes the transform for * consistency). Remaining messages become the user-visible message * content. * - Non-contiguous system messages (system in the middle of a * conversation) pass through inline unchanged. * - If `options.messages` is unset, fall back to a single-user-message * shape from `options.prompt` and `options.instructions`. * - When `options.messages` fully consumes into system content (no user * turn), returns an empty messages array — the caller adapter can * decide to error or synthesize a placeholder. * * Added in `0.1.0-alpha.26`. */ /** * Return the "user-facing" message content when the caller uses the legacy * `{prompt}` shape. Returns `undefined` when `messages` is set — the caller * should use `messages` directly in that case. */ declare function resolveCanonicalMessages(options: { messages?: LLMMessage[]; instructions?: string; prompt?: MessageContent; }): { messages: LLMMessage[]; instructions: string | undefined; }; /** * `withObservabilityContext(port, context)` — the scoped-port wrapper * for caller-provided observability context, per Plan 58 v0.4 §4.2. * * The port's public interface (`LLMPort`) stays untouched. Consumers * who want to attach correlation, W3C Trace Context, Baggage, * attributes, or an HMAC fingerprint key to every call flowing through * a port wrap it once at the workflow root: * * const scoped = withObservabilityContext(port, { * operation_id: workflowOperationId, * traceparent: incomingHeader, * baggage: [{ key: "tenant_id", value: tenantId }], * attributes: { region: "us-west" }, * fingerprint_key: process.env.OBS_HMAC_KEY, * }); * * await scoped.generateText({...}); * * At alpha.28 the wrapper's runtime behavior is minimal: it stores the * context in a WeakMap keyed by the returned port instance and forwards * every method call to the underlying port unchanged. `getObservabilityContext(port)` * retrieves the context for a wrapped port; the runtime instrumentation * that consumes it lands in alpha.29 (runtime instrumentation release). * * Why a WeakMap: consumers who pass wrapped ports around retain the * context association without leaking (once no reference to the wrapped * port survives, the context entry is garbage-collected). Alternative * of storing the context on the port object itself would mutate the * shape and clash with the "no LLMPort interface change" commitment. * * Why not AsyncLocalStorage (option (b) from the earlier design * decision): scoped-port wrapper is cross-runtime (works in every JS * runtime including workers where ALS is missing or partial); testable * without ambient state; explicit at the call site whether context is * scoped; subprocess callers can construct their own scoped emitter * from @llm-ports/observability-contract without needing a port. * ALS can be layered on top later as an opt-in propagator for Node- * only consumers who prefer ambient magic. */ /** * Wrap a port so that all calls through the returned port carry the * caller-provided `ObservabilityContext`. The returned port is * indistinguishable from the input at the public LLMPort interface; * consumers who need the context call `getObservabilityContext(port)`. * * Wrapping is composable: `withObservabilityContext(scoped, moreContext)` * merges `moreContext` over the previous scope. Fields present on * `moreContext` override; fields absent inherit. Baggage arrays are * concatenated (later entries win on duplicate keys). */ declare function withObservabilityContext(port: T, context: ObservabilityContext): T; /** * Retrieve the ObservabilityContext associated with a wrapped port. * Returns undefined for ports that were not wrapped via * `withObservabilityContext`. * * Alpha.28 exposes this as the low-level reader that alpha.29's runtime * instrumentation calls to stamp context onto emitted events. * Consumers writing their own emit paths (per §4.13 non-port callers) * also use this to read a context they scoped at a higher layer. */ declare function getObservabilityContext(port: LLMPort): ObservabilityContext | undefined; export { AGGRESSIVE_CREDIT_EXHAUSTION_PATTERNS, AdapterInternalError, type AdapterRegistration, type AgentResult, type ArtifactRef, type AudioBlock, type AudioSource, AuthenticationError, type BackoffConfig, BadRequestError, type BatchEmbeddingOptions, type BatchEmbeddingResult, type BudgetBackend, type BudgetCheckResult, BudgetExceededError, type BudgetGate, type BudgetLimit, type BudgetScope, type BudgetScopeRef, type CacheControl, type CacheHitEvent, type CapabilityLearner, ConfigError, type ContentBlock, ContentBlockUnsupportedError, ContentPolicyViolationError, ContextWindowExceededError, type CostBackend, type CostCheckResult, type CostEvent, type CostLimit, CostSession, type CostUsage, CreditExhaustionError, DEFAULT_VALIDATION_STRATEGY, type DeprecationDetails, type EmbeddingOptions, type EmbeddingResult, type EmbeddingsPort, EmptyMessagesError, EmptyResponseError, type FallbackCause, type FallbackEvent, type FirstLearningEvent, type GenerateStructuredOptions, type GenerateStructuredResult, type GenerateTextOptions, type GenerateTextResult, type ImageBlock, type ImageSource, ImageTooLargeError, InMemoryBudget, InMemoryCost, InvalidImageUrlError, type JitterStrategy, type KnownModelConstraint, type LLMMessage, type LLMPort, LLMPortError, type LLMPriority, type MessageContent, type MessageRole, MessagesConflictError, MessagesRequiredError, type ModelCapabilities, type ModelPricing, type ModelSelection, NoProvidersAvailableError, NonContiguousSystemError, type ObservabilityHooks, type OnCacheHit, type OnCost, type OnFallback, type OnRetry, type OnTokenUsage, type OnValidationRetry, type OpenCostSessionOptions, type ParseConfigOptions, type PricingFreshnessAdapterReport, type PricingFreshnessReport, PromptRequiredError, type ProviderEntry, ProviderMalformed400Error, type ProviderModelInfo, ProviderUnavailableError, RateLimitError, Registry, type RegistryConfig, type RegistryOptions, type RerankInput, type RerankPort, type RerankResult, type RerankedDocument, type RetryEvent, type RetryReason, type RunAgentOptions, STREAM_COMPLETE_CALLBACK_KEY, ServiceUnavailableError, SessionBudgetExceededError, type SessionGrainLimits, type StreamCompleteCallback, type StreamCompleteMetadata, type StreamStructuredOptions, type StreamTextOptions, type TaskConfig, type TaskType, type TextBlock, type TokenUsage, type TokenUsageEvent, type ToolDefinition, type ToolResultBlock, type ToolUseBlock, type ValidateImageOptions, ValidationError, type ValidationFailureContext, type ValidationRetryCause, type ValidationRetryEvent, type ValidationStrategy, type WarningState, _resetWarnedState, aggressiveShouldFallback, attachStreamCompleteCallback, attemptValidationRepair, buildCorrectionPrompt, buildLearningIssueUrl, computeBackoffDelay, computeChatCost, computeEmbeddingCost, createCapabilityLearner, createRegistryFromEnv, createWarningState, declareTasks, defaultShouldFallback, deriveCacheHit, deriveValidationRetryFromAdapterRetry, emitCacheHit, emitCost, emitFallback, emitFirstLearningWarning, emitRetryEvent, emitTokenUsage, emitValidationRetry, errorMatchers, extractJSON, extractText, failValidation, getObservabilityContext, getTaskConfig, isStringContent, mergeTokenUsage, parseRegistryConfig, readStreamCompleteCallback, resolveCanonicalMessages, stringifyContentBlocks, sys, throwIfAborted, toBlocks, toMessages, tryCollapseToText, tryParsePartialJSON, usr, validateImageBlocks, validateImageUrl, warnDeprecated, withObservabilityContext, wrapProviderError };