Ctrl K
ComunidadGuías…

utils/publishedCatalogs.ts

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

Archivo original ↗Preguntar a KiBot
import type { ProviderConfig } from "../models.ts";

type ModelDef = ProviderConfig["models"][string];
type ReasoningOptions = { type: string; values?: string[] }[] | undefined;

export type ModelsDevCatalog = Record<
  string,
  {
    models: Record<
      string,
      {
        name?: string;
        canonical_model_id?: string;
        status?: string;
        tool_call?: boolean;
        cost?: { input?: number; output?: number };
        reasoning_options?: ReasoningOptions;
      }
    >;
  }
>;

export interface PublishedCatalogs {
  /** `https://models.dev/api.json` */
  modelsDev: ModelsDevCatalog;
  /** `https://ai-gateway.vercel.sh/v1/models`, whose ladders outrank models.dev's for `vercel/*` */
  gateway: { data: { id: string; reasoning_options?: ReasoningOptions }[] };
  /** the ids each route's own API lists: Zen, Go, OpenRouter and the Vercel gateway */
  served: Record<RouteProvider, ReadonlySet<string>>;
}

type Listing = { data: { id: string }[] };

async function getJson<T>(url: string, signal: AbortSignal): Promise<T> {
  const response = await fetch(url, { signal });
  if (!response.ok) throw new Error(`${url} answered ${response.status}`);
  return JSON.parse(await response.text());
}

/** every catalog `applyPublishedCatalogs` reads, fetched together; rejects when any one fails */
export async function fetchPublishedCatalogs(signal: AbortSignal): Promise<PublishedCatalogs> {
  const [modelsDev, gateway, zen, go, openrouter] = await Promise.all([
    getJson<ModelsDevCatalog>("https://models.dev/api.json", signal),
    getJson<PublishedCatalogs["gateway"]>("https://ai-gateway.vercel.sh/v1/models", signal),
    getJson<Listing>("https://opencode.ai/zen/v1/models", signal),
    getJson<Listing>("https://opencode.ai/zen/go/v1/models", signal),
    getJson<Listing>("https://openrouter.ai/api/v1/models", signal),
  ]);
  const ids = (listing: Listing) => new Set(listing.data.map((m) => m.id));
  return {
    modelsDev,
    gateway,
    served: {
      opencode: ids(zen),
      "opencode-go": ids(go),
      openrouter: ids(openrouter),
      vercel: ids(gateway),
    },
  };
}

/** the providers that re-host another vendor's models, so their ids can be looked up rather than kept */
const ROUTE_PROVIDERS = ["opencode", "opencode-go", "openrouter", "vercel"] as const;
type RouteProvider = (typeof ROUTE_PROVIDERS)[number];
const isRoute = (provider: string): provider is RouteProvider =>
  ROUTE_PROVIDERS.some((route) => route === provider);

/**
 * models Zen LISTS in `/zen/v1/models` but will not actually serve — the endpoint answers 400
 * `[NOT_FOUND] ... not deployed`, on both Zen tiers. Membership is not availability, so these are
 * subtracted wherever a served list is read. Hand-kept, since only a call to the model can tell: add
 * one when `models-live` catches it, and delete it once the model completes a run again.
 */
export const ZEN_UNDEPLOYED: readonly string[] = [
  // 2026-08-28: 400 on both tiers, while kimi-k2.6 and kimi-k3 answer 200 on the same key.
  "kimi-k2.7-code",
];

/** where a dead free Zen model's stored picks land */
const FREE_ZEN_FALLBACK = "opencode/big-pickle";

/** free Zen lines declined on their terms or their output, never listed on their own. see wiki/models-catalog.md */
const DECLINED_FREE_ZEN = ["nemotron-", "ling-", "ring-"];

/** the Go relay rejects `reasoning_effort` although models.dev lists rungs for it */
const KEEP_BUNDLED_LADDER = new Set(["opencode-go/glm-flash"]);

type Ctx = { catalogs: PublishedCatalogs; providers: Record<string, ProviderConfig> };

const split = (spec: string) => [
  spec.slice(0, spec.indexOf("/")),
  spec.slice(spec.indexOf("/") + 1),
];

function isServed(ctx: Ctx, spec: string): boolean {
  const [route, id] = split(spec);
  if (!isRoute(route)) return true;
  if (route.startsWith("opencode") && ZEN_UNDEPLOYED.includes(id)) return false;
  return ctx.catalogs.served[route].has(id);
}

/** live ids on `route` that models.dev gives this canonical id */
function idsFor(ctx: Ctx, route: string, canonical: string): string[] {
  return Object.entries(ctx.catalogs.modelsDev[route]?.models ?? {})
    .filter(([, m]) => m.canonical_model_id === canonical && m.status !== "deprecated")
    .map(([id]) => `${route}/${id}`);
}

/**
 * of several ids sharing one canonical id, the one every other extends: `x` over `x-fast`, `x-256k`,
 * `x-contributor` or a dated `x-1017`. undefined when no such base exists.
 */
function baseOf(ids: string[]): string | undefined {
  return ids.find((base) => ids.every((id) => id.startsWith(base)));
}

/**
 * models.dev's id for the model `spec` names, when `spec` is the base of the ids sharing it on its own
 * provider. A variant that shares its base model's id (`-pro`, a contributor tier) would otherwise
 * collapse onto the base on every route.
 */
function canonicalOf(ctx: Ctx, spec: string): string | undefined {
  const [provider, id] = split(spec);
  const canonical = ctx.catalogs.modelsDev[provider]?.models[id]?.canonical_model_id;
  if (!canonical) return undefined;
  return baseOf(idsFor(ctx, provider, canonical)) === spec ? canonical : undefined;
}

/** the base of the ids `route` serves for `canonical` */
function routeIdFor(ctx: Ctx, route: string, canonical: string | undefined): string | undefined {
  if (!canonical) return undefined;
  return baseOf(idsFor(ctx, route, canonical).filter((spec) => isServed(ctx, spec)));
}

/** the canonical id every vendor entry under `key` agrees on, so a route copy knows which model it re-hosts */
function siblingCanonical(ctx: Ctx, key: string): string | undefined {
  const ids = Object.entries(ctx.providers)
    .filter(([provider]) => !isRoute(provider))
    .flatMap(([, config]) => {
      const def = config.models[key];
      return def && !def.routing && !def.fallback ? [def] : [];
    })
    .map((def) => canonicalOf(ctx, def.resolve));
  return ids.length > 0 && ids.every((id) => id && id === ids[0]) ? ids[0] : undefined;
}

/** the effort ladder `spec`'s catalog publishes, or undefined when the catalog doesn't list it */
function publishedLadder(
  ctx: Ctx,
  spec: string
): { ladder: readonly string[] | undefined } | undefined {
  const [provider, id] = split(spec);
  const model =
    provider === "vercel"
      ? ctx.catalogs.gateway.data.find((m) => m.id === id)
      : ctx.catalogs.modelsDev[provider]?.models[id];
  if (!model) return undefined;
  return { ladder: model.reasoning_options?.find((o) => o.type === "effort")?.values };
}

type ModelsDevModel = ModelsDevCatalog[string]["models"][string];

const isZeroCost = (model: ModelsDevModel) => model.cost?.input === 0 && model.cost?.output === 0;

/** a free Zen id that only discounts a model Zen also sells, which the paid alias already covers */
function isFreeTwin(ctx: Ctx, model: ModelsDevModel): boolean {
  const canonical = model.canonical_model_id;
  if (!canonical) return false;
  return Object.values(ctx.catalogs.modelsDev.opencode?.models ?? {}).some(
    (other) => other.canonical_model_id === canonical && !isZeroCost(other)
  );
}

/**
 * every free Zen promo models.dev lists that the registry doesn't, keyed by its own id: a runner
 * that keeps its bundled registry passes an unknown slug through raw, so the slug must be Zen's id.
 * a dead one is kept, falling back to Big Pickle, so a pick stored while it lived still resolves.
 */
function freeZenPromos(ctx: Ctx): Record<string, ModelDef> {
  const zen = ctx.providers.opencode?.models ?? {};
  const listed = new Set(Object.values(zen).map((def) => def.resolve));
  const promos = Object.entries(ctx.catalogs.modelsDev.opencode?.models ?? {}).flatMap(
    ([id, model]): [string, ModelDef][] => {
      const resolve = `opencode/${id}`;
      if (
        !id.endsWith("-free") ||
        listed.has(resolve) ||
        zen[id] ||
        !isZeroCost(model) ||
        !model.tool_call ||
        DECLINED_FREE_ZEN.some((prefix) => id.startsWith(prefix)) ||
        isFreeTwin(ctx, model)
      ) {
        return [];
      }
      const def: ModelDef = {
        displayName: (model.name ?? id).replace(/ Free$/, ""),
        description: "Free on Zen for a limited time; data may be used to improve it",
        resolve,
        isFree: true,
      };
      const dead = model.status === "deprecated" || !isServed(ctx, resolve);
      return [[id, dead ? { ...def, fallback: FREE_ZEN_FALLBACK } : def]];
    }
  );
  return Object.fromEntries(promos);
}

/** a free Zen model models.dev deprecates or Zen stops serving, while Big Pickle is still served */
function isDeadFreeZen(ctx: Ctx, slug: string, def: ModelDef): boolean {
  if (!def.isFree || def.fallback || !slug.startsWith("opencode/") || slug === FREE_ZEN_FALLBACK) {
    return false;
  }
  const pickle = ctx.providers.opencode?.models["big-pickle"];
  if (!pickle || !isServed(ctx, pickle.resolve)) return false;
  const [, id] = split(def.resolve);
  return (
    ctx.catalogs.modelsDev.opencode?.models[id]?.status === "deprecated" ||
    !isServed(ctx, def.resolve)
  );
}

function withPublished(ctx: Ctx, slug: string, def: ModelDef): ModelDef {
  if (def.routing || def.fallback) return def;
  if (isDeadFreeZen(ctx, slug, def)) return { ...def, fallback: FREE_ZEN_FALLBACK };
  const [provider, key] = split(slug);
  const resolve =
    (!def.pinned &&
      !def.isFree &&
      isRoute(provider) &&
      routeIdFor(ctx, provider, siblingCanonical(ctx, key))) ||
    def.resolve;
  const openRouterResolve =
    def.openRouterResolve &&
    ((!def.pinned && routeIdFor(ctx, "openrouter", canonicalOf(ctx, resolve))) ||
      def.openRouterResolve);
  if (KEEP_BUNDLED_LADDER.has(slug)) return { ...def, resolve, openRouterResolve };
  const direct = publishedLadder(ctx, resolve);
  const router = openRouterResolve ? publishedLadder(ctx, openRouterResolve) : undefined;
  return {
    ...def,
    resolve,
    openRouterResolve,
    effort: direct ? direct.ladder : def.effort,
    // `[]`, not undefined: an absent openRouterEffort means "the direct route's ladder"
    openRouterEffort: router ? (router.ladder ?? []) : def.openRouterEffort,
  };
}

/**
 * The registry as the published catalogs describe it today. A route copy of a vendor model takes
 * the one id its route serves under that model's canonical id, `openRouterResolve` likewise; each
 * route's effort ladder is the one it publishes; a free Zen model the registry lacks is listed;
 * and a dead free Zen model falls back to Big Pickle. Whatever the catalogs can't settle
 * unambiguously keeps the value written in `action/models.ts`, as does every `pinned` entry's id.
 * See wiki/model-resolution.md.
 */
export function applyPublishedCatalogs(params: Ctx): Record<string, ProviderConfig> {
  return Object.fromEntries(
    Object.entries(params.providers).map(([key, config]) => {
      const models =
        key === "opencode" ? { ...config.models, ...freeZenPromos(params) } : config.models;
      return [
        key,
        {
          ...config,
          models: Object.fromEntries(
            Object.entries(models).map(([id, def]) => [
              id,
              withPublished(params, `${key}/${id}`, def),
            ])
          ),
        },
      ];
    })
  );
}