/** * Provider routing — the TypeScript port of the Rust reference engine's * `providers.rs`, `quirks.rs` and `resolution.rs`. * * Three concerns, one module because they are one story: **which** model a given * activity should use, **what** wire quirks that concrete model has, and — when * the route points at a LiteLLM-style gateway — **which** upstream model a * semantic alias actually resolves to. * * - {@link ProviderRegistry} holds provider credentials/URLs and a * {@link ModelRouting} table mapping each {@link Activity} to a * {@link ModelSlot}. {@link ProviderRegistry.llmConfigFor} walks the slot's * fallback chain until it finds a registered provider. * - {@link quirksForModel} looks up per-model wire quirks by substring on the * concrete upstream name. * - {@link buildModelInfoUrl} / {@link parseModelInfo} / {@link fetchModelInfo} * recover the gateway's alias → upstream map from `GET /model/info`. * * The on-disk JSON shape is shared with the Rust CLI (`~/.smooth/providers.json`), * so the serialized keys are snake_case and must stay byte-compatible: the same * file is written by one engine and read by another. Legacy `thinking` / * `planning` field names still deserialize onto the merged `reasoning` slot. * * Routing values are pinned across all five engines by the shared corpus at * `spec/providers/routing.json` — a slot that resolves to the wrong model or * base URL sends real traffic and real money somewhere nobody intended, and it * looks like it is working. */ import type { ChatClientLike } from './agent.js'; /** * The wire dialect a provider speaks. The string values match the Rust * reference's serde output so `providers.json` round-trips between engines. */ export declare enum ApiFormat { /** The OpenAI `/chat/completions` dialect. */ OpenAiCompat = "OpenAiCompat", /** Anthropic's native `/messages` dialect. */ Anthropic = "Anthropic" } /** Connection detail for a single LLM provider. */ export interface ProviderConfig { id: string; apiUrl: string; apiKey: string; apiFormat: ApiFormat; defaultModel: string; } /** OpenRouter — an OpenAI-compatible proxy for many models. */ export declare function openRouterProvider(apiKey: string): ProviderConfig; /** The OpenAI direct API. */ export declare function openAiProvider(apiKey: string): ProviderConfig; /** The Anthropic native API. */ export declare function anthropicProvider(apiKey: string): ProviderConfig; /** A local Ollama instance — no API key needed. */ export declare function ollamaProvider(): ProviderConfig; /** The Google Gemini API (OpenAI-compatible surface). */ export declare function googleProvider(apiKey: string): ProviderConfig; /** Moonshot AI's general-purpose API (OpenAI-compatible). */ export declare function kimiProvider(apiKey: string): ProviderConfig; /** Moonshot's coding-optimized API (Anthropic-compatible). */ export declare function kimiCodeProvider(apiKey: string): ProviderConfig; /** LLM Gateway — a unified API for 210+ models. */ export declare function llmGatewayProvider(apiKey: string): ProviderConfig; /** * The hosted LiteLLM-backed gateway run by Smoo AI. * * One API key, one URL, OpenAI-compatible. The gateway handles provider * selection, billing, moderation and cost tracking server-side, so consumers * reference models by semantic aliases (`smooth-coding`, `smooth-judge`, …) that * the gateway maps to whichever underlying model is currently best — upgrades * ship server-side with no client release. * * `SMOOAI_GATEWAY_URL` overrides the base URL. Only an ABSENT variable takes the * default: a set-but-empty override yields an empty base URL, matching Rust. */ export declare function smooaiGatewayProvider(apiKey: string): ProviderConfig; /** A ready-made provider + routing configuration. */ export declare enum Preset { /** The hosted Smoo AI gateway — the recommended default. */ SmoaiGateway = "SmoaiGateway", /** Chinese frontier models via OpenRouter — the cheapest option. */ OpenRouterLowCost = "OpenRouterLowCost", /** Chinese frontier models via LLM Gateway. */ LlmGatewayLowCost = "LlmGatewayLowCost", /** OpenAI models. */ OpenAI = "OpenAI", /** Anthropic Claude models. */ Anthropic = "Anthropic" } /** One row of {@link ALL_PRESETS}: CLI name, display label, description. */ export interface PresetInfo { name: string; label: string; description: string; } /** * Every preset. The first entry is the recommended default — `th auth login` * shows them in this order. */ export declare const ALL_PRESETS: readonly PresetInfo[]; /** Parse a preset name or alias. Returns `undefined` for unknown names. */ export declare function presetFromName(name: string): Preset | undefined; /** The provider id a preset requires. */ export declare function presetProviderId(preset: Preset): string; /** * Selects which model slot a call routes through. Six semantic slots: the legacy * `Thinking` + `Planning` split collapsed into {@link Activity.Reasoning}, and the * legacy "default" alias is served by {@link Activity.Coding}. */ export declare enum Activity { /** The outer coding loop — the workhorse slot, which also serves the legacy "default" call path. */ Coding = "Coding", /** Deep reasoning / planning / chain-of-thought. */ Reasoning = "Reasoning", /** Code review, critique, adversarial checks. */ Reviewing = "Reviewing", /** LLM-as-a-judge: yes/no verdicts, low latency, used by Narc guardrails and bench scoring. */ Judge = "Judge", /** Context compression during long agent runs. */ Summarize = "Summarize", /** * Small, latency-sensitive utility calls: session auto-naming, short titles, * one-liner summaries, autocomplete. Sub-second first token, short output, no * tool use — don't pay Sonnet-plus prices to name a session. */ Fast = "Fast" } /** A provider id + model name, with an optional fallback used when the provider is not registered. */ export interface ModelSlot { provider: string; model: string; fallback?: ModelSlot; } /** Build a slot with no fallback. */ export declare function modelSlot(provider: string, model: string): ModelSlot; /** Return a copy of `slot` with `fallback` attached. */ export declare function withFallback(slot: ModelSlot, fallback: ModelSlot): ModelSlot; /** * The per-activity routing table. * * Six semantic slots plus a `default` slot kept for wire compatibility: no * {@link Activity} routes through `default` directly ({@link Activity.Coding} * serves the default path), but the field stays so pre-collapse configs load. */ export interface ModelRouting { coding: ModelSlot; /** Merged deep-reasoning slot. Absent in older files (which carry `thinking`); falls back to `default`. */ reasoning?: ModelSlot; reviewing: ModelSlot; judge: ModelSlot; summarize: ModelSlot; default: ModelSlot; /** Optional on disk: pre-`fast` files fall back to `default` at lookup time. */ fast?: ModelSlot; /** Legacy field, deserialized but ignored at lookup time — `reasoning` absorbed it. */ planning?: ModelSlot; } /** * The neutral, provider-agnostic routing every slot starts on: the well-known * `openrouter` provider id with a placeholder `auto` model, so the library ships * no opinion about a specific hosted gateway. Consumers opt into the Smoo AI * gateway via {@link Preset.SmoaiGateway} explicitly. */ export declare function defaultModelRouting(): ModelRouting; /** The slot for an activity. `Reasoning` and `Fast` fall back to `default` when absent. */ export declare function slotFor(routing: ModelRouting, activity: Activity): ModelSlot; /** * A fully resolved route: the provider connection plus the model the activity * picked. Feed `apiUrl`/`apiKey` to {@link createGatewayClient}. */ export interface LlmConfig { apiUrl: string; apiKey: string; model: string; maxTokens: number; temperature: number; apiFormat: ApiFormat; } /** Registered providers plus the per-activity routing table. */ export declare class ProviderRegistry { private readonly providers; /** The per-activity table. Reassign a slot to re-point a route. */ routing: ModelRouting; /** * A registry pre-configured with a preset: registers the preset's provider * and installs routing tuned for the preset's goals (cost, quality, latency). */ static fromPreset(preset: Preset, apiKey: string): ProviderRegistry; /** * A minimal registry from `SMOOTH_API_KEY` (required), `SMOOTH_PROVIDER` * (defaults to `openrouter`) and `SMOOTH_MODEL` (optional). Returns * `undefined` when `SMOOTH_API_KEY` is unset — never a keyless client. */ static fromEnv(): ProviderRegistry | undefined; /** Read a registry from a JSON file (e.g. `~/.smooth/providers.json`). */ static loadFromFile(path: string): ProviderRegistry; /** Deserialize a registry from the JSON shape {@link ProviderRegistry.toJson} writes. */ static fromJson(json: string): ProviderRegistry; /** Add (or replace) a provider configuration. */ registerProvider(config: ProviderConfig): void; /** Drop a provider by id. */ removeProvider(id: string): void; /** Look up a provider by id. */ getProvider(id: string): ProviderConfig | undefined; /** Every registered provider id, sorted. */ listProviders(): string[]; /** Point every routing slot at `providerId` using its default model. */ setDefaultProvider(providerId: string): void; /** Install a custom routing table. */ withRouting(routing: ModelRouting): this; private resolveSlot; /** * Resolve the route for an activity. Throws when the slot's provider — and * every fallback — is unregistered, rather than silently substituting some * other provider. */ llmConfigFor(activity: Activity): LlmConfig; /** Resolve the wire-compat `default` slot. */ defaultLlmConfig(): LlmConfig; /** * Build a gateway client for an activity's resolved route — the one line * between "which model should this call use" and a client that speaks to it. * * The client is OpenAI-compatible; an {@link ApiFormat.Anthropic} provider is * rejected rather than silently spoken to in the wrong dialect. */ clientFor(activity: Activity): { client: ChatClientLike; config: LlmConfig; }; /** Serialize to the on-disk JSON shape, snake_case keys and all. */ toJson(pretty?: boolean): string; /** Write the registry as pretty-printed JSON, creating parent directories. */ saveToFile(path: string): void; } /** * Per-model wire-format flags. Populate a field only when the quirk is worth the * branch — every conditional is a place for drift. * * When routing through a LiteLLM-style gateway the concrete upstream model only * reveals itself in response headers (`x-litellm-model-name`), by which point the * request is already sent. So prefer always-safe request shapes over per-model * conditionals, and keep this table for the cases where the strict form does not * work everywhere. */ export interface ModelQuirks { /** When `false`, force `parallel_tool_calls` off even if the agent config requests it. */ allowParallelTools?: boolean; /** Ask the client to be extra careful about tool_call echo shape. Nothing reads this yet. */ strictToolCallJson: boolean; } /** * Look up quirks by concrete upstream name. Matching is case-insensitive and * substring-based, so minor version drift (`qwen3-coder-plus-2025-04`) still hits * the `qwen3-coder` entry. Returns safe defaults when nothing matches. */ export declare function quirksForModel(upstream: string): ModelQuirks; /** The quirk table's canonical keys, for diagnostics. */ export declare function quirkKeys(): string[]; /** Every quirk entry matching an upstream name. Usually one wins; the full set is kept for tests. */ export declare function quirksDebugSnapshot(upstream: string): Record; /** One routing entry returned by a gateway's `/model/info`. */ export interface ResolvedModel { /** The name callers use (e.g. `smooth-coding`). */ alias: string; /** The concrete model (e.g. `moonshot/kimi-k2-thinking`), when the gateway surfaces it. */ upstream?: string; /** Stable id from `model_info.id`, useful for tracing a rename. */ id?: string; } /** * Derive the `/model/info` URL from a provider's OpenAI-compat `apiUrl` * (e.g. `https://llm.smoo.ai/v1`). Stripping `/v1` is safe: `/model/info` lives * at the gateway root in every LiteLLM deployment seen. */ export declare function buildModelInfoUrl(apiUrl: string): string; /** * Parse a `/model/info` response body into an alias → entry map, sorted by alias * so diagnostics print the same order every run (Rust returns a `BTreeMap`). * * Throws when the body is not valid JSON or is missing the `data` array. */ export declare function parseModelInfo(body: string): Map; /** * Ask a LiteLLM gateway for its alias → upstream map. * * A 401 means the provider's API key is missing or rejected; either way the * caller cannot see the mapping. */ export declare function fetchModelInfo(apiUrl: string, apiKey: string, timeoutMs?: number): Promise>; //# sourceMappingURL=providers.d.ts.map