/** * The model list of a NATIVE BYOK provider (OpenAI, Anthropic, Google), as * the admin "Models" tab proposes it (MIN-416). * * Source: the public OpenRouter index (`/api/v1/models?output_modalities=all`, * readable without a key — see lib/server/agent/openrouter-index.ts). Each entry carries a * `vendor/model` id; a BYOK field expects the PROVIDER's own namespace * (`claude-sonnet-5`, not `anthropic/claude-sonnet-5`), so the prefix is * stripped after filtering. This keeps the list automatically current — * neither Anthropic nor OpenAI publishes their catalog without credentials, * while OpenRouter's mirrors both within hours. * * Pure module: no server-only import, shared by the API route (server) and * the admin dashboard (client). The fetch itself lives server-side. */ import { dedupeModelVariants } from "@/lib/model-variants"; import { BYOK_MODEL_KEYS, type ByokModelKey } from "@/lib/ai-surfaces"; import type { ModelCatalogCapability } from "@/lib/model-catalog-capability"; /** Providers whose native ids can be derived from an OpenRouter `vendor/…` id. */ export const BYOK_CATALOG_PROVIDERS = ["openai", "anthropic", "google"] as const; export type ByokCatalogProvider = (typeof BYOK_CATALOG_PROVIDERS)[number]; /** OpenRouter's vendor segment for each supported provider. */ const VENDOR_PREFIXES: Record = { openai: "openai/", anthropic: "anthropic/", google: "google/", }; /** Non-conversational names excluded only from text-model pickers. */ const NON_CHAT_RE = /(embed(?:ding)?|whisper|tts|dall-e|moderation|audio|image|imagen|veo|realtime|transcribe|rerank)/i; /** Minimal shape this module needs from an index entry. */ export interface ByokCatalogSourceModel { id: string; name?: string; /** Router entries (`openrouter/auto`, `~vendor/latest`) are switches, not models. */ router?: boolean; /** Models whose output is not text have no place in a settings picker. */ textOutput?: boolean; /** Output capabilities published by OpenRouter. */ outputModalities?: string[]; } export interface ByokCatalogEntry { /** Native id, in the provider's own namespace (`claude-sonnet-5`). */ id: string; /** Display name published by OpenRouter. */ name: string; } /** * The provider's native models, derived from an OpenRouter-shaped list: * filtered on the vendor prefix and requested capability, cleared of routers * and version duplicates, then stripped of the prefix. */ export function byokModelsForProvider( models: readonly ByokCatalogSourceModel[], provider: ByokCatalogProvider, capability: ModelCatalogCapability = "text", ): ByokCatalogEntry[] { const prefix = VENDOR_PREFIXES[provider]; return dedupeModelVariants( models .filter((m) => { if (m.router === true) return false; if (capability === "transcription") { return m.outputModalities?.includes("transcription") === true; } if (capability === "embedding") { return m.outputModalities?.includes("embeddings") === true; } return m.textOutput !== false; }) .filter((m) => m.id.startsWith(prefix)) .filter((m) => capability !== "text" || !NON_CHAT_RE.test(m.id.slice(prefix.length))) .map((m) => ({ id: m.id.slice(prefix.length), name: m.name ?? m.id })), ).sort((a, b) => a.id.localeCompare(b.id)); } export function isByokCatalogProvider(value: string | null | undefined): value is ByokCatalogProvider { return (BYOK_CATALOG_PROVIDERS as readonly string[]).includes(value ?? ""); } /** * The call-type tail of a BYOK config key (`assistant_model`, * `transcription_model`…), or `null` for other shapes. * * Keys are matched longest-first so future overlapping names cannot produce * an ambiguous provider split. */ export function modelKeyFromByokConfigKey(key: string): ByokModelKey | null { const sortedKeys = [...BYOK_MODEL_KEYS].sort((a, b) => b.length - a.length); return sortedKeys.find((modelKey) => key === modelKey || key.endsWith(`_${modelKey}`)) ?? null; } /** * Which provider a BYOK `app_config` key belongs to, or `null` when the key * is not a BYOK default of a catalog-backed provider. * * The key shape is `byok_default__` — one per call type * (`byokFeatureDefaultModelKey`, lib/ai-surfaces.ts). */ export function byokProviderFromConfigKey(key: string): ByokCatalogProvider | null { if (!key.startsWith("byok_default_")) return null; const rest = key.slice("byok_default_".length); const provider = modelKeyFromByokConfigKey(rest) ? rest.slice(0, rest.length - modelKeyFromByokConfigKey(rest)!.length - 1) : null; return isByokCatalogProvider(provider) ? provider : null; }