import { describe, it, expect } from "vitest"; import type { PolyModel } from "@/lib/ai/core/types"; import { buildBadgeContext, deriveBadges, lifecycleStatus, isCodingModel, isOpenWeightsModel, speedTier, quantile, isNewModel } from "@/lib/models/badges"; import { presetCompatibility, unsupportedSettings, pruneSettingsForModel, groupsFor } from "@/lib/models/params"; const NOW = Date.parse("2026-09-11T12:00:00Z"); const daysAgo = (d: number) => new Date(NOW - d * 86_400_000).toISOString(); const caps = (over: Partial = {}): PolyModel["capabilities"] => ({ text: true, vision: false, audioInput: false, audioOutput: false, imageGeneration: false, video: false, reasoning: false, tools: false, structuredOutput: false, streaming: true, files: false, webSearch: false, ...over }); function m(id: string, over: Partial = {}): PolyModel { return { key: `openai/${id}`, id, provider: "openai", displayName: id, capabilities: caps(), parameters: {}, status: "active", pricing: null, metadata: { firstSeenAt: daysAgo(120) }, ...over }; } const ALL: PolyModel[] = [ m("gpt-5.5", { capabilities: caps({ reasoning: true, vision: true, tools: true }), limits: { contextTokens: 1_050_000 }, pricing: { inputPerMillion: 5, outputPerMillion: 30 } }), m("gpt-5.4-mini", { capabilities: caps({ reasoning: true, vision: true }), limits: { contextTokens: 400_000 }, pricing: { inputPerMillion: 0.4, outputPerMillion: 1.6 } }), m("gpt-5.4-nano", { pricing: { inputPerMillion: 0.1, outputPerMillion: 0.4 }, limits: { contextTokens: 128_000 } }), m("gpt-5.5-pro", { pricing: { inputPerMillion: 30, outputPerMillion: 120 }, limits: { contextTokens: 400_000 } }), m("gpt-5.3-codex", { pricing: { inputPerMillion: 2, outputPerMillion: 8 }, limits: { contextTokens: 400_000 }, metadata: { firstSeenAt: daysAgo(3) } }), m("brand-new", { metadata: { firstSeenAt: daysAgo(1) } }), ]; describe("badges", () => { const ctx = buildBadgeContext(ALL, NOW); it("computes quantiles", () => { expect(quantile([1, 2, 3, 4], 0.5)).toBe(2.5); expect(quantile([1, 2, 3, 4, 5], 0.25)).toBe(2); }); it("derives CHEAP from real pricing quantiles, not fixed numbers", () => { expect(ctx.cheapThreshold).toBeGreaterThan(0); expect(deriveBadges(ALL[2], ctx).map((b) => b.kind)).toContain("cheap"); expect(deriveBadges(ALL[3], ctx).map((b) => b.kind)).not.toContain("cheap"); }); it("derives capability and size badges", () => { const k = deriveBadges(ALL[0], ctx).map((b) => b.kind); expect(k).toEqual(expect.arrayContaining(["reasoning", "vision", "long-context"])); expect(k).not.toContain("fast"); expect(deriveBadges(ALL[1], ctx).map((b) => b.kind)).toContain("fast"); expect(isCodingModel(ALL[4])).toBe(true); expect(deriveBadges(ALL[4], ctx).map((b) => b.kind)).toEqual(expect.arrayContaining(["new", "coding"])); }); it("treats a fresh registry as not-new (everything would be new otherwise)", () => { const fresh = ALL.map((x) => ({ ...x, metadata: { firstSeenAt: daysAgo(1) } })); const c = buildBadgeContext(fresh, NOW); expect(c.newViaFirstSeen).toBe(false); expect(isNewModel(fresh[0], c)).toBe(false); // provider-reported release dates still count expect(isNewModel({ ...fresh[0], metadata: { createdAt: daysAgo(2) } }, c)).toBe(true); }); it("speed tiers and open weights heuristics", () => { expect(speedTier(ALL[2], ctx)).toBe("fast"); expect(speedTier(ALL[3], ctx)).toBe("frontier"); expect(isOpenWeightsModel(m("meta-llama/llama-3.1-8b-instruct"))).toBe(true); expect(isOpenWeightsModel(ALL[0])).toBe(false); expect(isOpenWeightsModel(m("x", { metadata: { openWeights: true } }))).toBe(true); }); it("lifecycle: retiring > deprecated > unavailable > preview > new > active", () => { expect(lifecycleStatus(m("a", { metadata: { shutdownDate: daysAgo(-30) } }), { now: NOW }).status).toBe("retiring"); expect(lifecycleStatus(m("a", { metadata: { shutdownDate: daysAgo(-300) } }), { now: NOW }).status).toBe("active"); expect(lifecycleStatus(m("a", { metadata: { shutdownDate: daysAgo(5) } }), { now: NOW }).status).toBe("deprecated"); expect(lifecycleStatus(m("a", { status: "deprecated" }), { now: NOW, connected: false }).status).toBe("deprecated"); expect(lifecycleStatus(m("a"), { now: NOW, connected: false }).status).toBe("unavailable"); expect(lifecycleStatus(m("a", { status: "preview" }), { now: NOW, connected: true }).status).toBe("preview"); expect(lifecycleStatus(ALL[5], { ctx, connected: true }).status).toBe("new"); expect(lifecycleStatus(ALL[0], { ctx, connected: true }).status).toBe("active"); expect(lifecycleStatus(m("a", { status: "unknown" }), { now: NOW }).label).toBe("Unverified"); }); }); describe("parameter sheet", () => { const model: PolyModel = m("gpt-5.5", { capabilities: caps({ reasoning: true, tools: true, structuredOutput: true, webSearch: true }), parameters: { temperature: true, topP: true, maxTokens: true, reasoningEffort: true, reasoningEffortLevels: ["low", "medium", "high"], verbosity: true, stop: false, seed: false }, limits: { maxOutputTokens: 128_000 }, }); it("lists only supported groups", () => { const groups = groupsFor(model).map((g) => g.group.key); expect(groups).toEqual(["generation", "reasoning", "output", "tools"]); expect(groupsFor(model).find((g) => g.group.key === "generation")!.defs.map((d) => d.key)).toEqual(["temperature", "topP", "maxTokens"]); }); it("flags unsupported settings and bad effort levels", () => { const u = unsupportedSettings({ temperature: 0.2, seed: 42, stop: ["x"], reasoningEffort: "xhigh", includeReasoning: true, tools: [] }, model); expect(u.map((x) => x.key).sort()).toEqual(["reasoningEffort", "seed", "stop"]); expect(u.find((x) => x.key === "reasoningEffort")!.reason).toMatch(/low \/ medium \/ high/); }); it("computes preset compatibility and prunes", () => { const ok = presetCompatibility({ parameters: { temperature: 0.3, reasoningEffort: "high" }, tools: { builtin: ["calculator"] } }, model); expect(ok.compatible).toBe(true); expect(ok.total).toBe(3); const bad = presetCompatibility({ parameters: { temperature: 0.3, seed: 1 }, tools: {} }, model); expect(bad.compatible).toBe(false); expect(bad.unsupported[0].key).toBe("seed"); expect(pruneSettingsForModel({ temperature: 0.3, seed: 1, topK: 4 }, model)).toEqual({ temperature: 0.3 }); }); });