Ctrl K
ComunidadGuías…

lib/byok-model-catalog.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * 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<ByokCatalogProvider, string> = {
  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_<provider>_<modelKey>` — 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;
}