/** * @packageDocumentation * Provider / API-key detection and per-provider model listing. * * This is the data layer that powers the first-run configuration dialog (CFG-2), * ACP model selection (CFG-5) and downstream propagation (CFG-6). It answers two * questions without ever instantiating an LLM: * * 1. **Which providers are usable on this machine?** — by inspecting the * environment (and config) for API keys, and by probing for a local Ollama. * 2. **What models does each usable provider offer?** — a live `GET /v1/models` * query for providers that expose an OpenAI-compatible (or, for Anthropic, a * native) models endpoint, falling back to a curated ⭐ "preferred" / tested * list when the live query is unavailable, errors, or is empty. * * Live discovery (CFG-12) is best-effort and never fatal: a bad key, an offline * machine, or a malformed response simply degrades to the curated catalog so the * first-run dialog always has something to show. The curated `preferredModels` * therefore do double duty — the ⭐ ranking overlay over live ids **and** the * offline/timeout fallback. * * The provider ids here are the same strings used by `LLMConfig.type` and * the provider factory in `#src/providers/.js`, so a selected * `{ providerId, model }` maps directly onto a `RawGthConfig.llm`. */ import { type ConfigType } from '#src/config.js'; import type { MissingProviderKeyDetails } from '#src/config/providerKeys.js'; /** * Provider identifiers understood by model discovery. These match the provider * factory module names (`#src/providers/.js`) and `LLMConfig.type`, * plus `ollama` for locally-served models. */ export type ProviderId = ConfigType | 'ollama'; /** * A single model offered by a provider. */ export interface ModelInfo { /** Model id as it should be written into config (`llm.model`). */ id: string; /** * Whether this is a ⭐ "preferred" / tested model. The first-run dialog * should surface preferred models first / pre-selected. */ preferred: boolean; } /** * How a provider's live model catalog can be queried. * * - `openai` — an OpenAI-compatible `GET /v1/models` (`{ data: [{ id }] }`). * Covers openai, openrouter, groq, deepseek, xai and ollama (local). * - `anthropic` — Anthropic's native `GET /v1/models` * (`{ data: [{ type, id, display_name }] }`) with `x-api-key` auth. * - `google` — Google AI Studio's native ListModels * (`GET /v1beta/models` → `{ models: [{ name, supportedGenerationMethods }] }`) * with `x-goog-api-key` auth. Chat capability is decided by a data-driven * `supportedGenerationMethods` check rather than a name regex (google-genai). * - `none` — no cheap live endpoint; stay on the curated list (vertexai, * which authenticates via gcloud ADC rather than an API key). */ export type DiscoveryKind = 'openai' | 'anthropic' | 'google' | 'none'; /** * Per-provider live-discovery adapter. Bundled on the {@link ProviderDescriptor} * so {@link discoverModels} can be fully data-driven. */ export interface DiscoveryConfig { kind: DiscoveryKind; /** * The models endpoint to fetch. `host` is supplied only for ollama (the * resolved daemon host); cloud providers ignore it and return a fixed URL. */ modelsUrl?: (host?: string) => string; /** Build the auth headers from the resolved API key (may be empty for ollama). */ authHeader?: (key: string) => Record; /** * Keep only chat-capable model ids. Live endpoints return embeddings, TTS, * whisper, image, guard, etc. alongside chat models; this prunes them. * When omitted, all returned ids are kept. */ filter?: (id: string) => boolean; } /** * Static description of a provider: how to detect its key, how to discover its * live models, and which models we recommend. Used to build * {@link DetectedProvider}s. */ export interface ProviderDescriptor { id: ProviderId; /** Human-friendly name for dialogs. */ label: string; /** * Environment variables that, when set to a non-empty value, indicate this * provider has a usable API key. Checked in order; the first match wins. * Empty for providers that don't authenticate via an env var (`vertexai`, * which uses gcloud ADC, and `ollama`, which is local). */ apiKeyEnvironmentVariables: string[]; /** * Curated ⭐ "preferred" / tested models for this provider, most-recommended * first. Used as the ⭐ ranking overlay over live-discovered ids and as the * fallback catalog when live discovery is unavailable. */ preferredModels: string[]; /** Live-discovery adapter for this provider. */ discovery: DiscoveryConfig; /** * True when usability cannot be determined from an env var alone. * `vertexai` relies on gcloud Application Default Credentials and `ollama` * on a running local daemon, so both are reported as `available: false` by * env inspection (ollama is then confirmed by a live probe in * {@link detectProviders}). */ requiresExternalAuth?: boolean; } /** Default Ollama host, matching the Ollama CLI/library default. */ export declare const DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434"; /** * Provider registry. The curated `preferredModels` are the models we have * tested with Gaunt Sloth's agent loop; defaults mirror the `init` templates in * each `#src/providers/.js` factory. * * CFG-22 (2026-07-24): the CLOUD curated lists were trimmed to 1-2 current-gen ids/provider * (head-first = the runtime default via getCuratedFallbackModel). Every surviving cloud id was live * tool-call-validated with a `gth eval` identity matrix (a marker-read that proves a real tool * call), with two exceptions: vertexai (no ADC here → mirrors the google-genai head) and ollama * (local ids, out of the cloud-trim scope — kept as-is, validated against the local daemon if * present). A dead id was caught and dropped: groq's qwen 404'd (`model_not_found`). */ export declare const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[]; /** * Result of detecting one provider. */ export interface DetectedProvider { id: ProviderId; label: string; /** * True when the provider looks usable on this machine: an API key env var is * set, or (for ollama) a local daemon responded. */ available: boolean; /** * The environment variable that supplied the key, when {@link available} via * an env var. Undefined for env-less providers (vertexai, ollama). */ apiKeyEnvironmentVariable?: string; /** True when this provider authenticates outside of an env var. */ requiresExternalAuth: boolean; /** Models offered by this provider, ⭐ preferred ones flagged. */ models: ModelInfo[]; } /** * Resolve the API key for a cloud provider by checking its env vars in order. * @returns the matching env var name, or undefined when none is set. */ export declare function findApiKeyEnvVar(descriptor: ProviderDescriptor): string | undefined; /** * Decide whether an LLM spec has **no resolvable API key at all** for its provider, and if so * report which provider and which variable names would have supplied one. * * This is the classifier the config loader uses to tell a missing secret apart from a provider * that broke for some other reason, and it answers that question from the ENVIRONMENT rather than * from the text of whatever error a provider SDK happened to throw. Message-sniffing would be * wrong on both sides: SDK wording is not ours to depend on, and several providers word the same * failure differently (some name the variable, some do not). * * Returns `undefined` — meaning "not a missing-key failure" — when any of these hold: * - the provider is unknown here, or authenticates outside an env-var key (vertexai via ADC, * ollama locally), so there is no key to be missing; * - the config carries an inline `llm.apiKey`; * - the config names an `llm.apiKeyEnvironmentVariable` that IS set; * - any variable the provider accepts is set. * * The last two matter for correctness, not tidiness: classifying a provider outage as a missing * key when a key was in fact present would defeat the whole distinction. */ export declare function findMissingProviderKey(providerType: string | undefined, llmConfig: { apiKey?: unknown; apiKeyEnvironmentVariable?: unknown; } | undefined): MissingProviderKeyDetails | undefined; /** * Build the {@link ModelInfo} list for a provider given the descriptor and an * optional set of model ids known to actually exist (e.g. from a live * `/v1/models` query or the local Ollama daemon). * * - When `discoveredModels` is omitted, the curated `preferredModels` are * returned, all flagged ⭐ preferred. * - When provided, every discovered model is listed; those that also appear in * the curated `preferredModels` are flagged ⭐ preferred. The list is ordered * **preferred first** (in curated order, so the recommended default sits on top * and is pre-selected), then every other discovered model **alphabetically** — * so a large catalog (e.g. OpenRouter's 200+ models) is navigable instead of * arriving in the endpoint's arbitrary order. */ export declare function buildModelList(descriptor: ProviderDescriptor, discoveredModels?: string[]): ModelInfo[]; /** * The single fallback source of truth for a provider's default model id (CFG-14). * * Returns the top curated ⭐ `preferredModels` entry for the provider — the one * id that provider factories fall back to when a config carries no `model`, and * the id first-run writes when live discovery is impossible. Centralising it here * means the fallback lives in exactly ONE place (the curated registry above) * instead of being duplicated as a literal in every `providers/.ts`. * * Every registered provider is required to carry at least one curated model, so * this always resolves for a known id; it throws for an unknown provider or a * provider whose `preferredModels` is empty (a developer error — the invariant * that the fallback source is complete would otherwise fail silently). * * @returns the curated default model id (always defined for a known provider). */ export declare function getCuratedFallbackModel(providerId: ProviderId): string; /** * Build the minimal `.gsloth.config.json` body an `init` template writes for a * provider (CFG-14). When `model` is omitted, the `model` key is left OUT of the * config entirely, so the provider factory resolves it at run time from * {@link getCuratedFallbackModel} — the single curated source that tracks the * installed version, rather than a literal frozen into the user's file that 404s * once the model is retired. * * A `$schema` pointer ({@link CONFIG_SCHEMA_POINTER}) is written first so editors offer * autocomplete/validation against the shipped JSON Schema (GS2-1). `$schema` is a known, * runtime-ignored config field (see the zod schema), so it never affects loading. */ export declare function buildInitConfigContent(providerId: ProviderId, model?: string, options?: { autocompact?: number | null; }): string; /** * CFG-21 — the timeout for the *interactive* first-run model fetch. The dialog is * blocking on a human who just picked a provider, so it can afford to wait a lot * longer than the background probes for a large cloud catalog (OpenRouter ~300 * models) to arrive over a cold connection, rather than losing the 2s race and * silently dropping to a 3-item curated stub. Threaded per-call so ONLY the dialog * uses it — `detectProviders` / `resolveInitModel` stay on the short * `DISCOVERY_TIMEOUT_MS` probe (a long global would hang the *provider* step * whenever the local Ollama daemon is down). */ export declare const INTERACTIVE_MODEL_FETCH_TIMEOUT_MS = 12000; /** * Provenance of a returned model list (CFG-21). Distinguishes the three outcomes so * an interactive caller can tell an honest "couldn't reach the provider" degrade * apart from a by-design curated list: * * - `live` — a successful, non-empty live `/v1/models` query. * - `fallback` — a live query WAS attempted (key present / ollama probed) but * failed: network error/timeout, non-2xx, or an empty/malformed payload. THIS is * the "couldn't reach" case the first-run dialog surfaces. * - `curated` — no live query was attempted, by design: a `kind:'none'` provider * (vertexai) or a cloud provider with no API key present. The * curated catalog is the expected result here, NOT a degrade, so the dialog must * NOT show a "couldn't reach" notice for it. */ export type ModelDiscoveryStatus = 'live' | 'fallback' | 'curated'; /** Per-call discovery options (CFG-21). */ export interface ModelDiscoveryOptions { /** * Live-fetch timeout in ms. Defaults to the short `DISCOVERY_TIMEOUT_MS` so * every existing caller is unchanged; the interactive first-run dialog passes the * generous {@link INTERACTIVE_MODEL_FETCH_TIMEOUT_MS}. */ timeoutMs?: number; } /** A model list plus its {@link ModelDiscoveryStatus} provenance (CFG-21). */ export interface ModelDiscoveryResult { models: ModelInfo[]; status: ModelDiscoveryStatus; } /** * Discover the models for a single provider. * * - `kind: 'none'` → returns the curated `buildModelList(descriptor)`. * - `kind: 'openai' | 'anthropic'` → fetches the models endpoint with a short * timeout, parses `data[].id`, applies the chat-only `filter`, and overlays * the ⭐ preferred flags via `buildModelList(descriptor, liveIds)`. * - `kind: 'google'` → fetches the native ListModels endpoint, parses * `models[].name` keeping only `generateContent`-capable entries and stripping * the `models/` prefix, then overlays the ⭐ preferred flags likewise. * * Best-effort: any error / non-2xx / malformed / empty payload falls back to * the curated list. This function NEVER throws — a bad key must degrade to the * curated catalog, not break first-run config. * * Cloud providers are only probed live when an API key is present; without a key * the curated catalog is returned directly. Ollama (no key, local daemon) is * always probed. */ export declare function discoverModels(providerId: ProviderId, options?: ModelDiscoveryOptions): Promise; /** * List the models for a single provider. * * Live-discovers from the provider's models endpoint where possible (with a * curated fallback); returns the curated set for `kind: 'none'` providers. Does * not require the provider to be "available". * * `options.timeoutMs` (CFG-21) overrides the default short live-fetch timeout; the * bare `listModels(providerId)` call is unchanged. */ export declare function listModels(providerId: ProviderId, options?: ModelDiscoveryOptions): Promise; /** * CFG-21 — like {@link discoverModels} but also returns the model list's * {@link ModelDiscoveryStatus} provenance, so an interactive caller (the first-run * dialog) can tell an honest "couldn't reach the provider" degrade (`fallback`) * apart from a by-design curated list (`curated`) and a real live catalog (`live`). * Kept as a separate export so `listModels` / `discoverModels` keep their existing * `(providerId) => ModelInfo[]` signatures other callers rely on. * * Never throws for a discovery failure (that degrades to `fallback`); it only * throws for an unknown provider id, mirroring {@link discoverModels}. */ export declare function discoverModelsWithProvenance(providerId: ProviderId, options?: ModelDiscoveryOptions): Promise; /** * Resolve the model id that `init` should bake into a generated config for a * provider (CFG-14) — the safeguard against writing a speculative id that 404s * once the model is retired. * * - **Live discovery possible** (a key / running daemon and a responding * `/v1/models` endpoint): returns a *verified-present* id — the highest-ranked * curated ⭐ id that is actually in the live list (matched tolerant of an * `:latest` suffix), or, when no curated id is live, the first live id. * - **Live discovery impossible** (`kind: 'none'`, no key, offline, empty * catalog): returns `undefined`. This is the ONLY speculative path, and it * deliberately declines to invent a literal: the caller OMITS `model` so the * provider factory falls back to {@link getCuratedFallbackModel} at run time * (a single curated source that upgrades with the installed version). * * Never throws — a bad key or network error degrades to `undefined`, mirroring * {@link discoverModels}. */ export declare function resolveInitModel(providerId: ProviderId): Promise; /** * Detect every known provider on this machine and list its models. * * - Cloud providers are `available` when one of their API-key env vars is set; * their model list is live-discovered (curated fallback) when a key is present. * - `vertexai` is reported with `requiresExternalAuth: true` and * `available: false`; usability via gcloud ADC cannot be cheaply verified * here and is left to the caller / a live run. * - `ollama` is `available` when the local daemon's `/v1/models` responds, and * its model list is that live inventory. * * @param options.includeUnavailable when true (default), every provider is * returned (so a dialog can offer "set a key" flows); when false, only * available providers are returned. */ export declare function detectProviders(options?: { includeUnavailable?: boolean; }): Promise;