Ctrl K
ComunidadGuías…

test/models.test.ts

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

Archivo original ↗Preguntar a KiBot
import { describe, expect, it } from "vitest";
import {
  getModelEnvVars,
  modelAliases,
  resolveCliModel,
  resolveDisplayAlias,
  resolveModelRung,
  resolveOpenRouterModel,
} from "../models.ts";

describe("GPT Astra routing and effort", () => {
  it.each([
    ["openai", "openai"],
    ["opencode", "opencode"],
    ["openrouter", "openrouter/openai"],
    ["vercel", "vercel/openai"],
  ])("resolves %s without offering disabled reasoning", (provider, route) => {
    const slug = `${provider}/gpt-astra`;
    expect(resolveCliModel(slug)).toBe(`${route}/gpt-6-astra`);
    expect(resolveOpenRouterModel(slug)).toBe(
      provider === "vercel" ? undefined : "openrouter/openai/gpt-6-astra"
    );
    for (const useOpenRouter of [false, true]) {
      expect(resolveModelRung({ slug, useOpenRouter, position: 0 })).toBe("low");
      expect(resolveModelRung({ slug, useOpenRouter, position: 1 })).toBe("max");
    }
  });
});

describe("OpenCode Go GLM Flash effort", () => {
  it("omits direct effort while retaining the OpenRouter ladder", () => {
    const slug = "opencode-go/glm-flash";
    expect(resolveModelRung({ slug, useOpenRouter: false, position: 0.5 })).toBeUndefined();
    expect(resolveModelRung({ slug, useOpenRouter: true, position: 0.5 })).toBe("high");
  });
});

// ── pure alias-registry invariants ──────────────────────────────────────────────
//
// these tests validate our alias data structure without hitting external APIs.
// network-dependent checks (models.dev / OpenRouter catalog drift, latest-model
// snapshot) live in models-catalog.main.test.ts and run on main pushes plus
// `pullfrog/models-bump` PRs (the bot's bump branch, gated in test.yml).

// models that have no OpenRouter equivalent and require BYOK.
// add a model here ONLY when it genuinely doesn't exist on both models.dev and
// OpenRouter — or, as with the vercel/* gateway aliases, when rerouting over
// OpenRouter would defeat the pick: a Vercel AI Gateway alias exists to send
// traffic through the user's own gateway (their observability, their billing),
// so a Router-proxy equivalent is deliberately withheld and the picker locks
// the entry until an AI_GATEWAY_API_KEY is stored.
// the models-bump cron flags entries that become fillable (see rule 9 in models-bump.yml).
const BYOK_ONLY_MODELS = new Set<string>([
  "vercel/claude-opus",
  "vercel/claude-sonnet",
  "vercel/claude-haiku",
  "vercel/gpt-sol",
  "vercel/gpt-astra",
  "vercel/gpt-terra",
  "vercel/gpt-luna",
  "vercel/gemini-pro",
  "vercel/gemini-carbon",
  "vercel/gemini-flash",
  "vercel/deepseek-pro",
  "vercel/deepseek-flash",
  "vercel/glm",
  "vercel/kimi-k3",
  "vercel/muse-spark",
  "vercel/muse-wonder",
  "vercel/muse-ember",
  "vercel/mistral-large",
  // same reason as the vercel/* entries, one step further: these aliases exist
  // to spend a Kimi MEMBERSHIP, and OpenRouter cannot resell one — its
  // `moonshotai/*` ids are the pay-as-you-go route the user picked these to
  // avoid. never fillable, so rule 9 can't reach them (the scanner exact-matches
  // `kimi-for-coding/<id>`, which OpenRouter does not carry). see wiki/kimi-code.md.
  "kimi-for-coding/kimi-k3",
  "kimi-for-coding/kimi-k3-256k",
  "kimi-for-coding/kimi-k2",
  "kimi-for-coding/kimi-k2-highspeed",
]);

describe("openRouterResolve completeness", () => {
  for (const alias of modelAliases) {
    if (alias.isFree) continue;
    // routing slugs (e.g. bedrock/byok) are inherently BYOK — there's no
    // single model to map to OpenRouter because the actual model ID is read
    // from a per-run env var.
    if (alias.routing) continue;
    // deprecated/disabled aliases never run as-is — resolution redirects
    // through the fallback, whose own openRouterResolve is validated here.
    if (alias.fallback) continue;
    if (BYOK_ONLY_MODELS.has(alias.slug)) continue;
    it(`${alias.slug} has openRouterResolve`, () => {
      expect(
        alias.openRouterResolve,
        `non-free model "${alias.slug}" is missing openRouterResolve — add it or add to BYOK_ONLY_MODELS`
      ).toBeDefined();
    });
  }

  for (const alias of modelAliases) {
    if (!alias.isFree) continue;
    it(`${alias.slug} (free) does not need openRouterResolve`, () => {
      expect(alias.openRouterResolve).toBeUndefined();
    });
  }

  for (const alias of modelAliases) {
    if (!alias.routing) continue;
    it(`${alias.slug} (routing slug) has no openRouterResolve`, () => {
      expect(alias.openRouterResolve).toBeUndefined();
    });
  }
});

describe("fallback chain resolution", () => {
  for (const alias of modelAliases.filter((a) => a.fallback)) {
    it(`${alias.slug} fallback chain resolves to a non-deprecated model`, () => {
      const resolved = resolveCliModel(alias.slug);
      expect(
        resolved,
        `fallback chain for "${alias.slug}" does not resolve to a non-deprecated model`
      ).toBeDefined();
    });
  }
});

// ── isFree invariants — sanity-check the catalog data shape ─────────────────────
//
// these catch the latent regressions that produced issue #691:
//   - opencode/gpt-5-nano was marked `isFree` despite costing $0.05/M
//     (no static check existed; demoted to paid in the same PR adding these tests)
//   - opencode/mimo-v2-pro-free was free + fallback to big-pickle (correct shape),
//     but nothing enforced that the terminal of an isFree fallback chain is itself
//     free. if someone repointed big-pickle's fallback at a paid model, all of mimo
//     and big-pickle's users would silently start hitting a paid endpoint.
//
// the cost.input check itself is network-dependent (lives in
// models-catalog.main.test.ts); these are the static sibling that runs on every PR.
describe("isFree invariants", () => {
  for (const alias of modelAliases.filter((a) => a.isFree)) {
    it(`${alias.slug} lives under the opencode provider`, () => {
      expect(
        alias.provider,
        `isFree alias "${alias.slug}" must be under "opencode" (Zen's keyless gate is opencode-only)`
      ).toBe("opencode");
    });

    // #1077 — `isFree` is about COST, not credentials. Zen refuses a keyless
    // request with `No provider available`, so a free alias that declared
    // `envVars: []` waved every static gate through and the run died at session
    // start with no missing-key CTA. free Zen models still need the key.
    it(`${alias.slug} requires the provider credential`, () => {
      expect(
        getModelEnvVars(alias.slug),
        `isFree alias "${alias.slug}" is free of CHARGE, not of credential — Zen still needs OPENCODE_API_KEY`
      ).toEqual(["OPENCODE_API_KEY"]);
    });

    it(`${alias.slug} has no openRouterResolve`, () => {
      expect(
        alias.openRouterResolve,
        `isFree alias "${alias.slug}" must omit \`openRouterResolve\` — free Zen models don't exist on OpenRouter`
      ).toBeUndefined();
    });

    it(`${alias.slug} fallback chain terminates at an isFree alias`, () => {
      const terminal = resolveDisplayAlias(alias.slug);
      expect(terminal, `fallback chain for "${alias.slug}" is broken`).toBeDefined();
      expect(
        terminal?.isFree,
        `isFree alias "${alias.slug}" walks to "${terminal?.slug}" which is NOT isFree — users would silently start paying`
      ).toBe(true);
    });
  }
});