import { compareModelIds, compareUnrankedModelIds, familyOf, namesThinking, releaseOf, tierRankOf } from "./model-order.js"; // The order every provider's catalog is served and browsed in; only Anthropic's catalog arrives ranked, the rest hand // back sets, so their registry order is not a preference. // A Codex catalog exactly as an OpenAI-compatible /v1/models hands it over: alphabetical, i.e. meaningless. const CODEX = ["gpt-5.1-codex", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]; test("ranks the frontier line above the cheap one and the newest release above its predecessors", () => { // The base (no-tier) line leads, newest first; mini sinks below all of it regardless of recency. expect(CODEX.toSorted(compareModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.1-codex", "gpt-5.4-mini"]); }); test("orders a release's named tiers strongest-first whichever order the endpoint listed them in", () => { for (const arrival of [CODEX.toSorted(), CODEX.toReversed()]) { const ordered = arrival.toSorted(compareModelIds); expect(ordered.slice(0, 3)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]); expect(ordered.at(-1)).toBe("gpt-5.4-mini"); } }); test("keeps release-local tiers below the next generation and above the previous one", () => { expect(["gpt-5.6-luna", "gpt-5.5", "gpt-5.7", "gpt-5.6-sol"].toSorted(compareModelIds)).toEqual([ "gpt-5.7", "gpt-5.6-sol", "gpt-5.6-luna", "gpt-5.5", ]); }); test("the Codex release-tier order is stable across catalog refreshes", () => { const arrivals = [ ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"], ["gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.6-sol"], ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"], ]; for (const arrival of arrivals) { expect(arrival.toSorted(compareUnrankedModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]); } // Ranking still outranks the tiebreak: mini stays at the tail under every sibling. expect(["gpt-5.4-mini", ...arrivals[0]!].toSorted(compareUnrankedModelIds).at(-1)).toBe("gpt-5.4-mini"); }); test("leaves a RANKED catalog's ties alone: the id tiebreak is for sets, and Anthropic publishes an opinion", () => { // compareUnrankedModelIds would rank fable ahead of opus by id; ranked catalogs keep the provider's own order. expect(["claude-opus-5", "claude-fable-5"].toSorted(compareModelIds)).toEqual(["claude-opus-5", "claude-fable-5"]); expect(["claude-opus-5", "claude-fable-5"].toSorted(compareUnrankedModelIds)).toEqual(["claude-fable-5", "claude-opus-5"]); }); test("reads each vendor's tier vocabulary, not just Claude's", () => { expect(["gemini-3-flash", "gemini-3-flash-lite", "gemini-3-pro"].toSorted(compareModelIds)).toEqual([ "gemini-3-pro", "gemini-3-flash", "gemini-3-flash-lite", ]); expect(["grok-4-fast", "grok-code-fast-1", "grok-4", "grok-3"].toSorted(compareModelIds)).toEqual([ "grok-4", "grok-3", "grok-4-fast", "grok-code-fast-1", ]); expect(["kimi-k2-turbo-preview", "kimi-k2-0711-preview", "kimi-k2-mini"].toSorted(compareModelIds)).toEqual([ "kimi-k2-0711-preview", "kimi-k2-turbo-preview", "kimi-k2-mini", ]); }); test("reads Kimi's k-prefixed generation so K3 leads the K2.x catalog", () => { const catalog = ["kimi-k2.6", "kimi-k2.7-code-highspeed", "kimi-k3", "kimi-k2.7-code"]; expect(releaseOf("kimi-k3")).toEqual({ version: [3], date: 0 }); expect(familyOf("kimi-k3")).toBe(familyOf("kimi-k2.6")); expect(catalog.toSorted(compareUnrankedModelIds)).toEqual(["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"]); }); test("the rightmost tier word wins, because tier words compose", () => { // flash-lite is Flash's cheap end, codex-max is Codex's frontier end; the leftmost word would misfile both. expect(tierRankOf(familyOf("gemini-3-flash-lite"))).toBe(tierRankOf("lite")); expect(tierRankOf(familyOf("gpt-5.1-codex-max"))).toBe(tierRankOf("max")); }); test("leads with a family carrying no tier word at all, so a brand-new flagship is never buried by its novelty", () => { expect(["claude-sonnet-5", "claude-mythos-1", "claude-opus-5"].toSorted(compareModelIds)[0]).toBe("claude-mythos-1"); }); test("files a re-served open-weights model on the cheap rung, not at the head of the catalog it visits", () => { // gpt-oss also ships on Google's channel; with no tier word, lead-unknown would wrongly rank it above Opus. expect(["gpt-oss-120b-medium", "claude-opus-4-6-thinking", "gemini-pro-agent"].toSorted(compareModelIds)).toEqual([ "claude-opus-4-6-thinking", "gemini-pro-agent", "gpt-oss-120b-medium", ]); }); // The Google channel as CLIProxyAPI publishes it: one row per thinking level, ids that agree on nothing. The catalog's // head is the default a turn with no pin receives and the row an open chat is repointed to when its own pin goes // missing, so which of these leads is a routing decision, not a cosmetic one. const ANTIGRAVITY = [ "gemini-3.8-flash-high", "gemini-3.6-flash-high", "gemini-3.1-pro-low", "gemini-3-flash", "claude-sonnet-4-6", "gpt-oss-120b-medium", "gemini-3.1-flash-lite", "claude-opus-4-6-thinking", "gemini-3.7-flash-high", "gemini-pro-agent", ]; test("seats a quiet rung under its own tier's ordinary rows, whatever version its id claims", () => { // Both are Gemini 3.1 Pro; only the quiet one says which level, and only the loud one omits the version. On release // alone the Low row leads the tier and every unpinned Google turn opens on it. expect(["gemini-3.1-pro-low", "gemini-pro-agent"].toSorted(compareUnrankedModelIds)).toEqual(["gemini-pro-agent", "gemini-3.1-pro-low"]); expect(["claude-sonnet-4-6-minimal", "claude-sonnet-4-6"].toSorted(compareModelIds)).toEqual(["claude-sonnet-4-6", "claude-sonnet-4-6-minimal"]); // Only BELOW the default is quiet: a loud row keeps its release, so the newer model still leads its tier. expect(["claude-opus-4-6-thinking", "claude-opus-5"].toSorted(compareModelIds)).toEqual(["claude-opus-5", "claude-opus-4-6-thinking"]); expect(["gpt-oss-120b-medium", "gpt-oss-120b-low"].toSorted(compareModelIds)).toEqual(["gpt-oss-120b-medium", "gpt-oss-120b-low"]); }); test("hands the Google channel a default that is a frontier row, before and after its Opus row goes missing", () => { expect(ANTIGRAVITY.toSorted(compareUnrankedModelIds)[0]).toBe("claude-opus-4-6-thinking"); // What the picker and every unpinned turn fall to while the channel is out of Opus capacity: Pro at its own level, // not the same model's Low row. expect(ANTIGRAVITY.filter((id) => id !== "claude-opus-4-6-thinking").toSorted(compareUnrankedModelIds)[0]).toBe("gemini-pro-agent"); // The quiet row still sorts above every lesser tier: demoted within Pro, not out of it. expect(ANTIGRAVITY.toSorted(compareUnrankedModelIds).slice(0, 3)).toEqual(["claude-opus-4-6-thinking", "gemini-pro-agent", "gemini-3.1-pro-low"]); }); test("keeps the arrival order between ids the rule cannot separate: Anthropic's catalog IS ranked", () => { expect(["claude-opus-5", "claude-fable-5"].toSorted(compareModelIds)).toEqual(["claude-opus-5", "claude-fable-5"]); expect(["claude-fable-5", "claude-opus-5"].toSorted(compareModelIds)).toEqual(["claude-fable-5", "claude-opus-5"]); }); test("groups every version of a family under one key, whatever shape its id takes", () => { expect(familyOf("claude-opus-4-8")).toBe(familyOf("claude-opus-5")); expect(familyOf("claude-haiku-4-5-20251001")).toBe(familyOf("claude-haiku-4-5")); expect(familyOf("gpt-5.1")).toBe(familyOf("gpt-5")); // Distinct lines stay distinct: a variant word is part of the family, not a version of the base one. expect(familyOf("gpt-5.1-codex")).not.toBe(familyOf("gpt-5.1")); }); test("stands an id with nothing but numbers (and an ACP row's empty one) as its own family", () => { expect(familyOf("4-5")).toBe("4-5"); expect(familyOf("")).toBe(""); }); test("holds date stamps apart from version components, or a dated build outranks the point release after it", () => { expect(releaseOf("claude-opus-4-1-20250805")).toEqual({ version: [4, 1], date: 20250805 }); expect(["claude-opus-4-20250514", "claude-opus-4-1-20250805"].toSorted(compareModelIds)).toEqual([ "claude-opus-4-1-20250805", "claude-opus-4-20250514", ]); }); test("breaks a version tie by date stamp, so two builds of one release still order", () => { expect(["claude-haiku-4-5-20251001", "claude-haiku-4-5-20260210"].toSorted(compareModelIds)).toEqual([ "claude-haiku-4-5-20260210", "claude-haiku-4-5-20251001", ]); }); test("reads a longer version as the newer one, so 5.1 outranks 5", () => { expect(["gpt-5", "gpt-5.1"].toSorted(compareModelIds)).toEqual(["gpt-5.1", "gpt-5"]); }); test("sorts an unversioned rolling alias under the releases that name their version", () => { // `kimi-latest` claims no release; guessing one would seat it above models that do say what they are. expect(releaseOf("kimi-latest").version).toEqual([]); expect(["kimi-latest", "kimi-k2-0711-preview"].toSorted(compareModelIds)).toEqual(["kimi-k2-0711-preview", "kimi-latest"]); }); test("names the thinking rows, and only those", () => { // What a settings row shows beside a pin. expect(namesThinking("gemini-3.6-flash-high")).toBe(true); expect(namesThinking("gemini-3.1-pro-low")).toBe(false); expect(namesThinking("gemini-3.5-flash-extra-low")).toBe(false); expect(namesThinking("claude-haiku-4-5-20251001")).toBe(false); expect(namesThinking("gpt-5.6-luna")).toBe(false); // An effort word other than `high` still names one; so does the on/off form beside its quiet row. expect(namesThinking("gpt-oss-120b-medium")).toBe(true); expect(namesThinking("kimi-k2-thinking")).toBe(true); expect(namesThinking("kimi-k2")).toBe(false); });