general-eval-card / tests /transformations /identity-canonicalization.test.ts
Jenny Chim
Deploy DuckDB-backed frontend to
da8db3e
Raw
History Blame
12.4 kB
import { describe, expect, it } from "vitest"
import { getCanonicalModelIdentity, getModelFamilyRouteId } from "../../lib/model-family"
// Executable spec for `notes/transformations/01-identity-canonicalization.md`.
//
// Each table here corresponds to a Group in the spec. Pipeline-side
// implementation must produce identical outputs for every row. Adding a row
// here is the way to extend the spec.
//
// When the pipeline ships matching values, run `scripts/verify-identity.mjs`
// against the full live cache to confirm cross-corpus equivalence before
// deleting the TS implementation (task #5b in the migration tasklist).
interface Case {
id: string
name?: string
developer?: string
expected: {
namespace?: string
familySlug?: string
familyId?: string
familyName?: string
variantKey?: string
variantLabel?: string
versionDate?: string | undefined
versionQualifier?: string | undefined
variantDisplayName?: string
}
why: string
}
// ---------------------------------------------------------------------------
// Group A β€” Token case map
// ---------------------------------------------------------------------------
//
// Each token in the map below must title-case to the canonical form. The full
// case map is captured in the spec doc (Group A table). Here we exercise a
// representative subset plus the "no map entry" baseline.
describe("Group A β€” token case map", () => {
const cases: Case[] = [
{ id: "anthropic/claude-opus-4.5", name: "Claude Opus 4.5", expected: { familyName: "Claude Opus 4.5" }, why: "claude β†’ Claude, opus β†’ Opus" },
{ id: "openai/gpt-5", name: "GPT 5", expected: { familyName: "GPT 5" }, why: "gpt β†’ GPT (uppercased)" },
{ id: "google/gemini-2.5-pro", name: "Gemini 2.5 Pro", expected: { familyName: "Gemini 2.5 Pro" }, why: "gemini, pro" },
{ id: "meta/llama-3", name: "Llama 3", expected: { familyName: "Llama 3" }, why: "llama" },
{ id: "01-ai/yi-large", name: "Yi Large", expected: { familyName: "Yi Large" }, why: "yi (lowercase letter, but in map)" },
{ id: "anthropic/claude-haiku", name: "Claude Haiku", expected: { familyName: "Claude Haiku" }, why: "haiku" },
{ id: "anthropic/claude-instant", name: "Claude Instant", expected: { familyName: "Claude Instant" }, why: "instant" },
{ id: "x/x-ai", name: "x AI", expected: { familyName: "X AI" }, why: "ai β†’ AI (a deliberate capitalization)" },
{ id: "test/turbo-mini", name: "Turbo Mini", expected: { familyName: "Turbo Mini" }, why: "turbo, mini" },
{ id: "qwen/qwen-coder", name: "Qwen Coder", expected: { familyName: "Qwen Coder" }, why: "qwen, coder" },
{ id: "test/foo-bar", name: "Foo Bar", expected: { familyName: "Foo Bar" }, why: "tokens not in map β†’ naive title-case" },
]
it.each(cases)("$why β†’ familyName=$expected.familyName", ({ id, name, expected }) => {
const result = getCanonicalModelIdentity({ id, name: name ?? "" })
expect(result.familyName).toBe(expected.familyName)
})
})
// ---------------------------------------------------------------------------
// Group B β€” v/V version-token rule
// ---------------------------------------------------------------------------
//
// Tokens matching /^v\d/i are lowercased. This rule is the source of the
// 1,253-card pipeline disagreement documented in the spec; pipeline must
// apply this rule before emitting model_family_name.
describe("Group B β€” v/V version-token rule", () => {
const cases: Case[] = [
{ id: "deepseek-ai/deepseek-v3", name: "Deepseek V3", expected: { familyName: "Deepseek v3" }, why: "trailing v3" },
{ id: "deepseek-ai/deepseek-v3.1", name: "Deepseek V3.1", expected: { familyName: "Deepseek v3.1" }, why: "v3.1 with patch" },
{ id: "mistralai/mistral-7b-instruct-v0.3", name: "Mistral 7B Instruct v0.3", expected: { familyName: "Mistral 7B Instruct v0.3" }, why: "v0.3 in middle, mistral & instruct in case map" },
{ id: "mistralai/mixtral-8x22b-instruct-v0.1", name: "Mixtral 8x22b Instruct v0.1", expected: { familyName: "Mixtral 8x22b Instruct v0.1" }, why: "v0.1 with size token" },
{ id: "amazon/nova-lite-v1.0", name: "Nova Lite v1.0", expected: { familyName: "Nova Lite v1.0" }, why: "v1.0" },
{ id: "test/foo-bar", name: "Foo Bar", expected: { familyName: "Foo Bar" }, why: "no v rule applies" },
]
it.each(cases)("$why β†’ familyName=$expected.familyName", ({ id, name, expected }) => {
expect(getCanonicalModelIdentity({ id, name: name ?? "" }).familyName).toBe(expected.familyName)
})
})
// ---------------------------------------------------------------------------
// Group C β€” Date and qualifier extraction
// ---------------------------------------------------------------------------
describe("Group C β€” date and qualifier extraction", () => {
const cases: Case[] = [
{
id: "anthropic/claude-3-5-sonnet",
name: "Claude 3.5 Sonnet",
expected: { familySlug: "claude-3.5-sonnet", versionDate: undefined, versionQualifier: undefined, variantKey: "base", variantLabel: "Current" },
why: "no date in handle β†’ base variant, Current label",
},
{
id: "anthropic/claude-3-5-sonnet-20240620",
name: "Claude 3.5 Sonnet 20240620",
expected: { familySlug: "claude-3.5-sonnet", versionDate: "2024-06-20", versionQualifier: undefined, variantKey: "20240620", variantLabel: "2024-06-20" },
why: "YYYYMMDD date present, no qualifier",
},
{
id: "anthropic/claude-3-5-sonnet-20240620-thinking",
name: "Claude 3.5 Sonnet 20240620 Thinking",
expected: {
familySlug: "claude-3.5-sonnet",
versionDate: "2024-06-20",
versionQualifier: "Thinking",
variantKey: "20240620-thinking",
variantLabel: "2024-06-20 Β· Thinking",
},
why: "date plus single-token qualifier",
},
{
id: "openai/gpt-5-2025-12-11-thinking-high",
name: "GPT 5 2025-12-11 thinking high",
expected: {
// The (?:19|20)\d{6} regex requires 8 contiguous digits; "2025-12-11" has dashes, so
// it doesn't match as a date. NormalizeHandle's digit-dash-digit collapse only fires when
// the right side is followed by a dash or end, so "2025-12-11" stays as is.
// β†’ no date matched β†’ base variant.
variantKey: "base",
versionDate: undefined,
},
why: "dashed date is NOT recognized β€” only YYYYMMDD form",
},
]
it.each(cases)("$why", ({ id, name, expected }) => {
const result = getCanonicalModelIdentity({ id, name: name ?? "" })
if (expected.familySlug !== undefined) expect(result.familySlug).toBe(expected.familySlug)
if (expected.versionDate !== undefined || "versionDate" in expected) expect(result.versionDate).toBe(expected.versionDate)
if (expected.versionQualifier !== undefined || "versionQualifier" in expected) expect(result.versionQualifier).toBe(expected.versionQualifier)
if (expected.variantKey !== undefined) expect(result.variantKey).toBe(expected.variantKey)
if (expected.variantLabel !== undefined) expect(result.variantLabel).toBe(expected.variantLabel)
})
})
// ---------------------------------------------------------------------------
// Group D β€” Handle normalization
// ---------------------------------------------------------------------------
describe("Group D β€” handle normalization (familyId derivation)", () => {
const cases: Case[] = [
{ id: "anthropic/Claude_Opus_4.5", name: "Claude Opus 4.5", expected: { familyId: "anthropic/claude-opus-4.5" }, why: "underscore β†’ dash, lowercase" },
{ id: "anthropic/claude opus 4.5", name: "Claude Opus 4.5", expected: { familyId: "anthropic/claude-opus-4.5" }, why: "space β†’ dash" },
{ id: "anthropic/--claude--opus--", name: "Claude Opus", expected: { familyId: "anthropic/claude-opus" }, why: "leading/trailing/repeated dashes collapsed" },
{ id: "anthropic/claude-3-5-sonnet", name: "Claude 3.5 Sonnet", expected: { familyId: "anthropic/claude-3.5-sonnet" }, why: "digit-dash-digit-dash collapses to digit-dot-digit-dash" },
{ id: "anthropic/claude-3-5", name: "Claude 3.5", expected: { familyId: "anthropic/claude-3.5" }, why: "digit-dash-digit at end collapses to digit-dot-digit" },
{ id: "google/gemini-1.5-pro", name: "Gemini 1.5 Pro", expected: { familyId: "google/gemini-1.5-pro" }, why: "existing dot is preserved" },
]
it.each(cases)("$why", ({ id, name, expected }) => {
expect(getCanonicalModelIdentity({ id, name: name ?? "" }).familyId).toBe(expected.familyId)
})
})
// ---------------------------------------------------------------------------
// Group E β€” Namespace and rawHandle extraction
// ---------------------------------------------------------------------------
describe("Group E β€” namespace + rawHandle extraction", () => {
it("id with slash β†’ first segment is namespace, rest is handle", () => {
const r = getCanonicalModelIdentity({ id: "anthropic/claude-opus-4-5", name: "Claude Opus 4.5" })
expect(r.namespace).toBe("anthropic")
expect(r.rawHandle).toBe("claude-opus-4-5")
})
it("id without slash falls back to developer field for namespace", () => {
const r = getCanonicalModelIdentity({ id: "gpt-5", name: "GPT 5", developer: "OpenAI" })
expect(r.namespace).toBe("openai")
})
it("developer with spaces is slug-cased for namespace", () => {
const r = getCanonicalModelIdentity({ id: "claude-opus-4.5", name: "Claude Opus 4.5", developer: "Anthropic AI" })
expect(r.namespace).toBe("anthropic-ai")
})
it("namespace prefix in name is stripped when computing rawHandle from name", () => {
// Falls into getRawHandle's strippedName path when id has no '/' segment
const r = getCanonicalModelIdentity({ id: "", name: "anthropic/Claude Opus 4.5", developer: "Anthropic" })
expect(r.namespace).toBe("anthropic")
// Note: with id empty, idHandle is empty, so falls back to stripNamespace(name, namespace)
expect(r.rawHandle).toBe("Claude Opus 4.5")
})
})
// ---------------------------------------------------------------------------
// Group F β€” familyId and model_route_id contract
// ---------------------------------------------------------------------------
describe("Group F β€” familyId and model_route_id", () => {
const cases = [
{ familyId: "anthropic/claude-opus-4.5", routeId: "anthropic__claude-opus-4.5" },
{ familyId: "openai/gpt-5", routeId: "openai__gpt-5" },
{ familyId: "01-ai/yi-large", routeId: "01-ai__yi-large" },
{ familyId: "google/gemini-2.5-pro", routeId: "google__gemini-2.5-pro" },
]
it.each(cases)("getModelFamilyRouteId($familyId) === $routeId", ({ familyId, routeId }) => {
expect(getModelFamilyRouteId(familyId)).toBe(routeId)
})
})
// ---------------------------------------------------------------------------
// Integration cases β€” full-tuple round-trips for fixtures we ship
// ---------------------------------------------------------------------------
//
// These mirror real model cards in tests/fixtures/model-cards/ (where the
// pipeline ALSO emits the canonical fields). When pipeline matches across the
// full corpus, this section is a sanity check that the spec stays in sync
// with the fixtures.
describe("integration β€” fixtures round-trip", () => {
const fixtures = [
{
id: "anthropic/claude-opus-4.5",
name: "Claude Opus 4.5",
expected: {
familyId: "anthropic/claude-opus-4.5",
familyName: "Claude Opus 4.5",
variantKey: "base",
},
why: "anthropic__claude-opus-4.5 fixture (dotted route_id quirk)",
},
{
id: "openai/gpt-5",
name: "GPT 5",
expected: {
familyId: "openai/gpt-5",
familyName: "GPT 5",
variantKey: "base",
},
why: "openai__gpt-5 fixture",
},
{
id: "01-ai/yi-34b",
name: "Yi 34B",
expected: {
familyId: "01-ai/yi-34b",
familyName: "Yi 34B",
variantKey: "base",
},
why: "01-ai__yi-34b fixture (dash-prefix slug)",
},
]
it.each(fixtures)("$why", ({ id, name, expected }) => {
const result = getCanonicalModelIdentity({ id, name })
expect(result.familyId).toBe(expected.familyId)
expect(result.familyName).toBe(expected.familyName)
expect(result.variantKey).toBe(expected.variantKey)
})
})