3c9799adda
every 12h, scripts/find-newer-models.ts scans models.dev for newer GA versions of every alias in action/models.ts and writes a focused per-alias diff. .github/workflows/models-bump.yml short-circuits when no candidates exist; otherwise hands the diff to pullfrog/pullfrog@main to evaluate against the policy in wiki/model-resolution.md and open a single living PR on the pullfrog/models-bump branch. drops the brittle "latest model per provider" snapshot block in action/test/models-catalog.main.test.ts (and its .snap file) — the cron keeps the registry in sync with upstreams, and the remaining validity tests act as the integrity gate on the bump PR.
115 lines
4.1 KiB
TypeScript
115 lines
4.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { modelAliases } from "../models.ts";
|
|
|
|
// ── catalog drift tests — main-only ─────────────────────────────────────────────
|
|
//
|
|
// these tests fetch models.dev and openrouter.ai to verify that every alias in
|
|
// models.ts still corresponds to a live, non-deprecated upstream model. upstream
|
|
// catalog drift (new model ships, old model deprecated, etc.) causes failures
|
|
// that are unrelated to any code change in the PR — so these run only on main.
|
|
//
|
|
// the registry is kept in sync with upstreams by the `models-bump` cron
|
|
// (`.github/workflows/models-bump.yml`), which scans models.dev every 12h and
|
|
// opens a PR bumping `resolve` / `openRouterResolve` for any alias whose
|
|
// upstream has shipped a newer GA version. these tests are the integrity gate
|
|
// for that PR — they catch typos, removed models, and openrouter mismatches.
|
|
//
|
|
// run locally with `pnpm test:catalog`.
|
|
// in CI, gated to push events on main.
|
|
|
|
type ModelsDevModel = {
|
|
name: string;
|
|
status?: string;
|
|
release_date?: string;
|
|
};
|
|
|
|
type ModelsDevProvider = {
|
|
name: string;
|
|
models: Record<string, ModelsDevModel>;
|
|
};
|
|
|
|
type ModelsDevApi = Record<string, ModelsDevProvider>;
|
|
|
|
const api = fetch("https://models.dev/api.json").then((r) => r.json() as Promise<ModelsDevApi>);
|
|
|
|
function parseResolve(resolve: string): { provider: string; modelId: string } {
|
|
const idx = resolve.indexOf("/");
|
|
return { provider: resolve.slice(0, idx), modelId: resolve.slice(idx + 1) };
|
|
}
|
|
|
|
describe("models.dev validity", async () => {
|
|
const data = await api;
|
|
|
|
for (const alias of modelAliases) {
|
|
const parsed = parseResolve(alias.resolve);
|
|
|
|
it(`${alias.resolve} exists on models.dev`, () => {
|
|
const providerData = data[parsed.provider];
|
|
expect(providerData, `provider "${parsed.provider}" not found on models.dev`).toBeDefined();
|
|
const model = providerData.models[parsed.modelId];
|
|
expect(
|
|
model,
|
|
`model "${parsed.modelId}" not found under ${parsed.provider} on models.dev`
|
|
).toBeDefined();
|
|
});
|
|
|
|
if (!alias.fallback) {
|
|
it(`${alias.resolve} is not deprecated`, () => {
|
|
const model = data[parsed.provider]?.models[parsed.modelId];
|
|
if (!model) return; // covered by existence test above
|
|
expect(model.status, `${alias.resolve} is deprecated on models.dev`).not.toBe("deprecated");
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
describe("openRouterResolve models.dev validity", async () => {
|
|
const data = await api;
|
|
const seen = new Set<string>();
|
|
|
|
for (const alias of modelAliases) {
|
|
if (!alias.openRouterResolve) continue;
|
|
if (seen.has(alias.openRouterResolve)) continue;
|
|
seen.add(alias.openRouterResolve);
|
|
|
|
const parsed = parseResolve(alias.openRouterResolve);
|
|
|
|
it(`${alias.openRouterResolve} exists on models.dev`, () => {
|
|
const providerData = data[parsed.provider];
|
|
expect(providerData, `provider "${parsed.provider}" not found on models.dev`).toBeDefined();
|
|
const model = providerData.models[parsed.modelId];
|
|
expect(
|
|
model,
|
|
`model "${parsed.modelId}" not found under ${parsed.provider} on models.dev`
|
|
).toBeDefined();
|
|
});
|
|
}
|
|
});
|
|
|
|
type OpenRouterModel = { id: string };
|
|
type OpenRouterModelsResponse = { data: OpenRouterModel[] };
|
|
|
|
const openRouterApi = fetch("https://openrouter.ai/api/v1/models").then(
|
|
(r) => r.json() as Promise<OpenRouterModelsResponse>
|
|
);
|
|
|
|
describe("openRouterResolve OpenRouter API validity", async () => {
|
|
const orData = await openRouterApi;
|
|
const orModelIds = new Set(orData.data.map((m) => m.id));
|
|
const seen = new Set<string>();
|
|
|
|
for (const alias of modelAliases) {
|
|
if (!alias.openRouterResolve) continue;
|
|
const orModelId = alias.openRouterResolve.slice("openrouter/".length);
|
|
if (seen.has(orModelId)) continue;
|
|
seen.add(orModelId);
|
|
|
|
it(`${orModelId} exists on OpenRouter`, () => {
|
|
expect(
|
|
orModelIds.has(orModelId),
|
|
`model "${orModelId}" not found in OpenRouter API (/api/v1/models)`
|
|
).toBe(true);
|
|
});
|
|
}
|
|
});
|