/** * Model registry — single source of truth for available models. * * Backends register their models during initialization. Frontends read * from the registry to build dynamic model pickers, resolve aliases, * and query capabilities. No model names are hardcoded outside this * system and the backend-specific model definition files. */ import type { ReasoningEffortLevel } from "../types.js"; // ── Types ──────────────────────────────────────────────────────────────────── export type ModelInfo = { /** Canonical model ID as registered by the backend. */ id: string; /** Human-readable display name for UIs (e.g. "Sonnet 4.6"). */ displayName: string; /** Short description for setup wizard (e.g. "fast, balanced"). */ description?: string; /** Aliases that resolve to this model. */ aliases: string[]; /** Provider identifier (e.g. "anthropic", "openai"). */ provider: string; /** Reasoning/effort levels this model accepts. */ supportedReasoningLevels?: ReasoningEffortLevel[]; /** Backend-reported default reasoning level, when available. */ defaultReasoningLevel?: ReasoningEffortLevel; /** Model to fall back to on overload/timeout. */ fallback?: string; }; // ── Registry state ────────────────────────────────────────────────────────── const models = new Map(); const aliasIndex = new Map(); const providerPrefixes: string[] = []; /** * Register a provider-specific prefix that the fuzzy * alias resolver will strip when matching family names. Backends call * this during initialization so core stays provider-agnostic. */ export function registerProviderPrefix(prefix: string): void { const lower = prefix.toLowerCase(); if (!providerPrefixes.includes(lower)) { providerPrefixes.push(lower); } } function resolveGenericFamilyAlias(input: string): string | null { const trimmed = input.trim().toLowerCase(); if (!trimmed) return null; let base = trimmed; for (const prefix of providerPrefixes) { if (base.startsWith(prefix)) { base = base.slice(prefix.length); break; } } const tokens = base.replace(/\./g, "-").split("-").filter(Boolean); if (tokens.length === 0) return null; let boundary = tokens.length; while (boundary > 0 && /^\d+$/.test(tokens[boundary - 1] ?? "")) { boundary -= 1; } const family = tokens.slice( 0, boundary === tokens.length ? tokens.length : boundary, ); if (family.length === 0) return null; return family.join("-"); } // ── Registration ──────────────────────────────────────────────────────────── /** Register one or more models. Idempotent — re-registration overwrites. */ export function registerModels(infos: ModelInfo[]): void { for (const info of infos) { // Clear stale aliases from any previous registration of this model ID const prev = models.get(info.id); if (prev) { aliasIndex.delete(prev.id.toLowerCase()); for (const alias of prev.aliases) { aliasIndex.delete(alias.toLowerCase()); } } models.set(info.id, info); // Index the canonical ID itself as an alias aliasIndex.set(info.id.toLowerCase(), info.id); for (const alias of info.aliases) { aliasIndex.set(alias.toLowerCase(), info.id); } } } // ── Queries ───────────────────────────────────────────────────────────────── /** Get a model by canonical ID. */ export function getModel(id: string): ModelInfo | undefined { return models.get(id); } /** List all registered models, optionally filtered by provider. Returned in registration order. */ export function getModels(provider?: string): ModelInfo[] { const result = [...models.values()]; if (provider) { return result.filter((m) => m.provider === provider); } return result; } /** * Resolve a user input (alias or full ID) to the canonical model ID. * Returns the input unchanged if no match is found (passthrough for * unknown/custom model names). */ export function resolveModelId(input: string): string { const lower = input.trim().toLowerCase(); const direct = aliasIndex.get(lower); if (direct) return direct; const genericAlias = resolveGenericFamilyAlias(input); if (genericAlias) { const resolved = aliasIndex.get(genericAlias); if (resolved) return resolved; } return input.trim(); } /** * Resolve a user input to the full ModelInfo, or undefined if not found. */ export function resolveModel(input: string): ModelInfo | undefined { const id = resolveModelId(input); return models.get(id); } /** Get the fallback model ID for a given model, or null if none configured. */ export function getFallbackModel(modelId: string): string | null { return resolveModel(modelId)?.fallback ?? null; } /** * Get the default model. Prefers the canonical "default" model ID if * registered, otherwise returns the first registered model, otherwise * falls back to the literal string "default". */ export function getDefaultModel(): string { if (models.has("default")) return "default"; const first = models.values().next(); if (!first.done) return first.value.id; return "default"; } // ── Provider-scoped clearing ──────────────────────────────────────────────── /** Remove all models for a specific provider (and their aliases). */ export function clearModelsByProvider(provider: string): void { for (const [id, info] of models) { if (info.provider !== provider) continue; aliasIndex.delete(id.toLowerCase()); for (const alias of info.aliases) { aliasIndex.delete(alias.toLowerCase()); } models.delete(id); } } /** Clear the entire registry. For tests only. */ export function clearModels(): void { models.clear(); aliasIndex.clear(); providerPrefixes.length = 0; }