From ac24a2b4580eb61a04fffcd1048b0f4cb17714c6 Mon Sep 17 00:00:00 2001 From: MumuTW <42820974+MumuTW@users.noreply.github.com> Date: Tue, 25 Aug 2026 17:59:17 +0800 Subject: [PATCH 1/5] fix(autoCombo): inherit task fitness from base model for effort/alias variants (#11489) Auto-combo task fitness scored every catalog id by exact string match, but dispatch already resolves - ids to a base model. A variant like gpt-5.6-sol-xhigh missed both DB layers and fell to the wildcard 0.5 while its base model was scored properly. Adds resolveScoresAs(), a catalog-anchored resolver with three tiers: an explicit scoresAs declared on the registry entry, a reasoning-effort suffix stripped by one of the EXISTING dispatch splitters, and a -free tier marker. Tiers 2-3 accept a stripped base only when it is itself a routable catalog id, which keeps qwen3.7-max (where -max is the model) and grok-4.6-fast-high (whose base does not exist) unresolved. No new suffix regex is introduced. getTaskFitnessWithSource retries user_override and arena_elo against the resolved base on a literal miss and reports :inherited. The former -free arena_elo special case (#4517) is folded into the same path and extended to user_override. Populates scoresAs for the relations stripping cannot express: the forward vendor alias gpt-5.6 -> gpt-5.6-sol, and cursor/agy's - spelling of the Claude ids. Fixes #11489 --- open-sse/config/providerModels.ts | 42 +++++++++ .../config/providers/registry/cursor/index.ts | 64 ++++++++++--- .../config/providers/registry/openai/index.ts | 5 +- open-sse/config/providers/shared.ts | 11 +++ .../autoCombo/__tests__/autoCombo.test.ts | 17 ++-- .../__tests__/scoresAs-11489.test.ts | 92 +++++++++++++++++++ open-sse/services/autoCombo/scoresAs.ts | 85 +++++++++++++++++ open-sse/services/autoCombo/taskFitness.ts | 62 ++++++++----- .../unit/task-fitness-scores-as-11489.test.ts | 53 +++++++++++ 9 files changed, 385 insertions(+), 46 deletions(-) create mode 100644 open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts create mode 100644 open-sse/services/autoCombo/scoresAs.ts create mode 100644 tests/unit/task-fitness-scores-as-11489.test.ts diff --git a/open-sse/config/providerModels.ts b/open-sse/config/providerModels.ts index 5cc983387114..3e6dffbf99e8 100644 --- a/open-sse/config/providerModels.ts +++ b/open-sse/config/providerModels.ts @@ -145,6 +145,48 @@ function getGlobalModel(modelId: string): RegistryModel | undefined { return bestMatch; } +/** + * Exact-id catalog lookup: the registry entry for `modelId` across every + * provider, or for its basename once a `vendor/` prefix is stripped. + * + * This is `getGlobalModel`'s steps 1–2 only. Step 3 (a `startsWith` substring + * scan) deliberately guesses at a base model, which is exactly what a + * catalog-anchor check must not do — `resolveScoresAs` (#11489) uses this to + * verify that a suffix-stripped base is a REAL routable id before inheriting + * its quality scores. + */ +export function findRegistryModelById(modelId: string): RegistryModel | undefined { + if (typeof modelId !== "string" || modelId.length === 0) return undefined; + for (const models of Object.values(PROVIDER_MODELS)) { + const found = models.find((m) => m.id === modelId); + if (found) return found; + } + const basename = modelId.split("/").pop() || modelId; + if (basename === modelId) return undefined; + for (const models of Object.values(PROVIDER_MODELS)) { + const found = models.find((m) => m.id === basename); + if (found) return found; + } + return undefined; +} + +/** + * The `scoresAs` target declared for `modelId`, if any (#11489). Scans every + * provider that ships the id rather than stopping at the first hit, so an + * unannotated duplicate of the same id in another provider's catalog cannot + * shadow the entry that actually declares the relation. + */ +export function findRegistryScoresAs(modelId: string): string | undefined { + if (typeof modelId !== "string" || modelId.length === 0) return undefined; + const basename = modelId.split("/").pop() || modelId; + for (const models of Object.values(PROVIDER_MODELS)) { + for (const m of models) { + if ((m.id === modelId || m.id === basename) && m.scoresAs) return m.scoresAs; + } + } + return undefined; +} + export function getProviderModel(aliasOrId: string, modelId: string): RegistryModel | undefined { const models = PROVIDER_MODELS[aliasOrId]; if (!models) return getGlobalModel(modelId); diff --git a/open-sse/config/providers/registry/cursor/index.ts b/open-sse/config/providers/registry/cursor/index.ts index 7d54a2252f2a..cc5a78f8ced0 100644 --- a/open-sse/config/providers/registry/cursor/index.ts +++ b/open-sse/config/providers/registry/cursor/index.ts @@ -26,10 +26,30 @@ export const cursorProvider: RegistryEntry = { { id: "gpt-5.3-codex-spark-preview", name: "GPT 5.3 Codex Spark Preview" }, { id: "gpt-5.3-codex-spark-preview-high", name: "GPT 5.3 Codex Spark Preview High" }, { id: "gpt-5.3-codex-spark-preview-xhigh", name: "GPT 5.3 Codex Spark Preview XHigh" }, - { id: "claude-4.6-opus-high-thinking-fast", name: "Claude 4.6 Opus High Thinking Fast" }, - { id: "claude-4.6-opus-max-thinking-fast", name: "Claude 4.6 Opus Max Thinking Fast" }, - { id: "claude-4.6-sonnet-medium", name: "Claude 4.6 Sonnet Medium" }, - { id: "claude-4.6-sonnet-medium-thinking", name: "Claude 4.6 Sonnet Medium Thinking" }, + // #11489: cursor/agy spell Claude ids - ("claude-4.6-opus-high"); + // the effort splitter strips those to "claude-4.6-opus", which is not a catalog id. + // `scoresAs` points each at the canonical - spelling so quality + // scores are inherited. Operational metadata stays on these entries. + { + id: "claude-4.6-opus-high-thinking-fast", + name: "Claude 4.6 Opus High Thinking Fast", + scoresAs: "claude-opus-4-6", + }, + { + id: "claude-4.6-opus-max-thinking-fast", + name: "Claude 4.6 Opus Max Thinking Fast", + scoresAs: "claude-opus-4-6", + }, + { + id: "claude-4.6-sonnet-medium", + name: "Claude 4.6 Sonnet Medium", + scoresAs: "claude-sonnet-4-6", + }, + { + id: "claude-4.6-sonnet-medium-thinking", + name: "Claude 4.6 Sonnet Medium Thinking", + scoresAs: "claude-sonnet-4-6", + }, { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" }, { id: "gemini-3.7-flash", name: "Gemini 3.7 Flash" }, { id: "gemini-3-flash", name: "Gemini 3 Flash" }, @@ -184,12 +204,24 @@ export const cursorProvider: RegistryEntry = { { id: "gpt-5.4-high-fast", name: "GPT-5.4 High Fast" }, { id: "gpt-5.4-xhigh", name: "GPT-5.4 1M Extra High" }, { id: "gpt-5.4-xhigh-fast", name: "GPT-5.4 Extra High Fast" }, - { id: "claude-4.6-opus-high", name: "Opus 4.6 1M" }, - { id: "claude-4.6-opus-max", name: "Opus 4.6 1M Max" }, - { id: "claude-4.6-opus-high-thinking", name: "Opus 4.6 1M Thinking" }, - { id: "claude-4.6-opus-max-thinking", name: "Opus 4.6 1M Max Thinking" }, - { id: "claude-4.5-opus-high", name: "Opus 4.5" }, - { id: "claude-4.5-opus-high-thinking", name: "Opus 4.5 Thinking" }, + // #11489: cursor/agy spell Claude ids - ("claude-4.6-opus-high"); + // the effort splitter strips those to "claude-4.6-opus", which is not a catalog id. + // `scoresAs` points each at the canonical - spelling so quality + // scores are inherited. Operational metadata stays on these entries. + { id: "claude-4.6-opus-high", name: "Opus 4.6 1M", scoresAs: "claude-opus-4-6" }, + { id: "claude-4.6-opus-max", name: "Opus 4.6 1M Max", scoresAs: "claude-opus-4-6" }, + { + id: "claude-4.6-opus-high-thinking", + name: "Opus 4.6 1M Thinking", + scoresAs: "claude-opus-4-6", + }, + { + id: "claude-4.6-opus-max-thinking", + name: "Opus 4.6 1M Max Thinking", + scoresAs: "claude-opus-4-6", + }, + { id: "claude-4.5-opus-high", name: "Opus 4.5", scoresAs: "claude-opus-4-5" }, + { id: "claude-4.5-opus-high-thinking", name: "Opus 4.5 Thinking", scoresAs: "claude-opus-4-5" }, { id: "gpt-5.2-low", name: "GPT-5.2 Low" }, { id: "gpt-5.2-low-fast", name: "GPT-5.2 Low Fast" }, { id: "gpt-5.2-fast", name: "GPT-5.2 Fast" }, @@ -223,13 +255,17 @@ export const cursorProvider: RegistryEntry = { { id: "gpt-5.4-nano-medium", name: "GPT-5.4 Nano" }, { id: "gpt-5.4-nano-high", name: "GPT-5.4 Nano High" }, { id: "gpt-5.4-nano-xhigh", name: "GPT-5.4 Nano Extra High" }, - { id: "claude-4.5-sonnet", name: "Sonnet 4.5" }, - { id: "claude-4.5-sonnet-thinking", name: "Sonnet 4.5 Thinking" }, + { id: "claude-4.5-sonnet", name: "Sonnet 4.5", scoresAs: "claude-sonnet-4-5" }, + { + id: "claude-4.5-sonnet-thinking", + name: "Sonnet 4.5 Thinking", + scoresAs: "claude-sonnet-4-5", + }, { id: "gpt-5.1-low", name: "GPT-5.1 Low" }, { id: "gpt-5.1", name: "GPT-5.1" }, { id: "gpt-5.1-high", name: "GPT-5.1 High" }, - { id: "claude-4-sonnet", name: "Sonnet 4" }, - { id: "claude-4-sonnet-thinking", name: "Sonnet 4 Thinking" }, + { id: "claude-4-sonnet", name: "Sonnet 4", scoresAs: "claude-sonnet-4" }, + { id: "claude-4-sonnet-thinking", name: "Sonnet 4 Thinking", scoresAs: "claude-sonnet-4" }, { id: "gpt-5-mini", name: "GPT-5 Mini" }, { id: "kimi-k3-low", name: "Kimi K3 Low" }, { id: "kimi-k3-max", name: "Kimi K3" }, diff --git a/open-sse/config/providers/registry/openai/index.ts b/open-sse/config/providers/registry/openai/index.ts index 60a276a948b9..f2cd5dc2d574 100644 --- a/open-sse/config/providers/registry/openai/index.ts +++ b/open-sse/config/providers/registry/openai/index.ts @@ -12,7 +12,10 @@ export const openaiProvider: RegistryEntry = { authHeader: "bearer", defaultContextLength: 128000, models: [ - { id: "gpt-5.6", name: "GPT-5.6", ...GPT_5_6_API_CAPABILITIES }, + // #11489: per OpenAI's model reference `gpt-5.6` is an ALIAS of `gpt-5.6-sol`, + // not a distinct model — quality scores point forward, which no suffix + // stripper can express. Siblings `-terra`/`-luna` are their own models. + { id: "gpt-5.6", name: "GPT-5.6", scoresAs: "gpt-5.6-sol", ...GPT_5_6_API_CAPABILITIES }, { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", ...GPT_5_6_API_CAPABILITIES }, { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", ...GPT_5_6_API_CAPABILITIES }, { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", ...GPT_5_6_API_CAPABILITIES }, diff --git a/open-sse/config/providers/shared.ts b/open-sse/config/providers/shared.ts index 16c0c09b4156..6c67c4eb5fb2 100644 --- a/open-sse/config/providers/shared.ts +++ b/open-sse/config/providers/shared.ts @@ -83,6 +83,17 @@ export interface RegistryModel { /** Per-model upstream header-response timeout override — precedes * `RegistryEntry.timeoutMs` and the global `FETCH_TIMEOUT_MS` (#6354). */ timeoutMs?: number; + /** + * Id whose QUALITY scores (task fitness / arena / user overrides) this id + * inherits (#11489). Operational fields (timeoutMs, cost, context) stay on + * this entry. One hop only; the target must itself be a catalog id. + * + * Only for relations suffix-stripping cannot express: forward vendor aliases + * (`gpt-5.6` → `gpt-5.6-sol`) and cross-provider spellings of the same model + * (cursor's `claude-4.6-opus-high` → `claude-opus-4-6`). Plain effort/`-free` + * variants are derived by `resolveScoresAs` and need no entry here. + */ + scoresAs?: string; } // Reasoning models reject temperature, top_p, penalties, logprobs, n. diff --git a/open-sse/services/autoCombo/__tests__/autoCombo.test.ts b/open-sse/services/autoCombo/__tests__/autoCombo.test.ts index efeae64bcc18..96c2dbc42e52 100644 --- a/open-sse/services/autoCombo/__tests__/autoCombo.test.ts +++ b/open-sse/services/autoCombo/__tests__/autoCombo.test.ts @@ -101,10 +101,15 @@ describe("Task Fitness", () => { // The fix: getTaskFitnessWithSource strips a trailing "-free" suffix // and re-queries arena_elo with the base id. We seed an arena_elo // row directly via the DB module, look up the free variant, and - // assert the alias path returns the base score with source - // "arena_elo_free_alias". - const baseId = "alias-base-test-4517"; - const freeId = "alias-base-test-4517-free"; + // assert the alias path returns the base score, now tagged + // "arena_elo:inherited" by the shared resolver (#11489). + // + // #11489 also made the base-id lookup CATALOG-ANCHORED, so this uses a + // real catalog pair instead of the synthetic ids it was written with: + // an id that resolves to a base no provider actually ships is a ghost, + // and inheriting a score for it was never meaningful. + const baseId = "mimo-v2.5"; + const freeId = "mimo-v2.5-free"; const { upsertModelIntelligence, deleteModelIntelligence } = await import("../../../../src/lib/db/modelIntelligence.ts"); // Seed arena_elo on the base id only — no row exists for the free id. @@ -121,9 +126,9 @@ describe("Task Fitness", () => { try { const result = getTaskFitnessWithSource(freeId, "coding"); // Without the fix: result.source would be "wildcard_boost" (0.5 default). - // With the fix: result.source is "arena_elo_free_alias" with score 0.42. + // With the fix: result.source is "arena_elo:inherited" with score 0.42. expect(result.score).toBeCloseTo(0.42, 5); - expect(result.source).toBe("arena_elo_free_alias"); + expect(result.source).toBe("arena_elo:inherited"); } finally { deleteModelIntelligence(baseId, "arena_elo", "coding"); invalidateFitnessCache(); diff --git a/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts b/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts new file mode 100644 index 000000000000..b1d8d31b36ed --- /dev/null +++ b/open-sse/services/autoCombo/__tests__/scoresAs-11489.test.ts @@ -0,0 +1,92 @@ +/** + * TDD regression for #11489: auto-combo task fitness scored every catalog id by + * exact string match, while dispatch already resolves `-` ids back + * to a base model. A variant like `gpt-5.6-sol-xhigh` missed every DB layer and + * landed on the wildcard 0.5, while its base model was scored properly. + * + * `resolveScoresAs` is the shared, catalog-anchored resolver the fitness chain + * consults on a miss. It resolves in three tiers and NEVER guesses: + * 1. an explicit `scoresAs` declared on the registry entry (one hop only), + * 2. a trailing reasoning-effort suffix stripped by an EXISTING splitter, + * 3. a trailing `-free` tier marker, + * and tiers 2–3 only accept a base that is itself a routable catalog id — which + * is what keeps `qwen3.7-max` (where `-max` is the model, not an effort) and + * `grok-4.6-fast-high` (whose stripped base is not in the catalog) unresolved. + */ +import { describe, it, expect } from "vitest"; +import { resolveScoresAs } from "../scoresAs"; +import { findRegistryModelById } from "../../../config/providerModels"; + +describe("#11489 resolveScoresAs", () => { + it("strips a reasoning-effort suffix when the base is a catalog id", () => { + expect(resolveScoresAs("gpt-5.6-sol-xhigh")).toEqual({ + base: "gpt-5.6-sol", + via: "effort-suffix", + }); + }); + + it("follows a vendor alias that points FORWARD (gpt-5.6 is an alias of gpt-5.6-sol)", () => { + // No suffix-stripper can produce this direction; it is registry data. + expect(resolveScoresAs("gpt-5.6")).toEqual({ base: "gpt-5.6-sol", via: "explicit" }); + }); + + it("leaves sibling models unresolved (gpt-5.6-luna is its own model)", () => { + const result = resolveScoresAs("gpt-5.6-luna"); + expect(result.via).toBeNull(); + expect(result.base).toBe("gpt-5.6-luna"); + expect(result.base).not.toBe("gpt-5.6"); + expect(result.base).not.toBe("gpt-5.6-sol"); + }); + + it("rejects an effort-stripped base that is not itself a catalog id", () => { + // `grok-4.6-fast-high` strips to `grok-4.6-fast`, a ghost id on today's + // catalog. Asserted against the catalog rather than hardcoded so the test + // stays true if a provider ever ships the base as a routable id. + const result = resolveScoresAs("grok-4.6-fast-high"); + if (findRegistryModelById("grok-4.6-fast")) { + expect(result).toEqual({ base: "grok-4.6-fast", via: "effort-suffix" }); + } else { + expect(result).toEqual({ base: "grok-4.6-fast-high", via: null }); + } + }); + + it("does not treat a trailing '-max' that is part of the model name as an effort", () => { + // `qwen3.7-max` IS the model; `qwen3.7` does not exist. + expect(resolveScoresAs("qwen3.7-max")).toEqual({ base: "qwen3.7-max", via: null }); + }); + + it("never collapses a model onto its family", () => { + expect(resolveScoresAs("claude-sonnet-5")).toEqual({ base: "claude-sonnet-5", via: null }); + }); + + it("resolves the cursor/agy spelling of a Claude model to its canonical id", () => { + // `claude-4.6-opus-high` strips to `claude-4.6-opus`, which is not a catalog + // id — the canonical spelling is `claude-opus-4-6`. Explicit registry data. + expect(resolveScoresAs("claude-4.6-opus-high")).toEqual({ + base: "claude-opus-4-6", + via: "explicit", + }); + expect(resolveScoresAs("claude-4.6-sonnet-medium")).toEqual({ + base: "claude-sonnet-4-6", + via: "explicit", + }); + }); + + it("strips a '-free' tier marker when the paid base is a catalog id", () => { + expect(findRegistryModelById("mimo-v2.5")).toBeTruthy(); + expect(resolveScoresAs("mimo-v2.5-free")).toEqual({ base: "mimo-v2.5", via: "free-suffix" }); + }); + + it("leaves a '-free' id unresolved when no paid base exists in the catalog", () => { + expect(findRegistryModelById("ox-alpha")).toBeFalsy(); + expect(resolveScoresAs("ox-alpha-free")).toEqual({ base: "ox-alpha-free", via: null }); + }); + + it("returns the id unchanged for junk input", () => { + expect(resolveScoresAs("totally-unknown-model")).toEqual({ + base: "totally-unknown-model", + via: null, + }); + expect(resolveScoresAs("")).toEqual({ base: "", via: null }); + }); +}); diff --git a/open-sse/services/autoCombo/scoresAs.ts b/open-sse/services/autoCombo/scoresAs.ts new file mode 100644 index 000000000000..cca584b5efc4 --- /dev/null +++ b/open-sse/services/autoCombo/scoresAs.ts @@ -0,0 +1,85 @@ +/** + * scoresAs — resolve a catalog id to the id whose QUALITY scores it inherits (#11489). + * + * Dispatch already treats `-` ids as variants of a base model + * (`splitClaudeEffortSuffix`, `splitCodexReasoningSuffix` run on the incoming + * request model). Auto-combo's task fitness did not: it scored every catalog id + * by exact string match, so a variant like `gpt-5.6-sol-xhigh` missed every DB + * layer and landed on the wildcard 0.5 while its base model was scored properly. + * + * This module is the single seam that closes that gap. Three tiers, in order: + * + * 1. `explicit` — the registry entry declares `scoresAs`. For relations + * suffix-stripping cannot express: forward vendor aliases + * (`gpt-5.6` IS an alias of `gpt-5.6-sol`, per OpenAI's + * model reference) and cross-provider spellings of the + * same model (`claude-4.6-opus-high` → `claude-opus-4-6`). + * 2. `effort-suffix` — a trailing reasoning-effort token stripped by one of the + * EXISTING dispatch splitters. No new regex is introduced + * here; a fourth pattern would be a fourth place to get + * the same fact wrong (cf. the #8603 shadowing defect). + * 3. `free-suffix` — a trailing `-free` tier marker, so a free-tier variant + * picks up the benchmark of its paid counterpart (#4517, + * previously a standalone arena_elo-only special case in + * `taskFitness.ts`, now folded in and extended to + * `user_override` too). + * + * Tiers 2 and 3 are CATALOG-ANCHORED: a stripped base is accepted only when it + * is itself a routable catalog id. Without that guard, 57 of the catalog's 201 + * strippable effort ids resolve to a base that does not exist — `qwen3.7-max` + * would inherit from a phantom `qwen3.7` (`-max` is part of the model name + * here, not an effort), `grok-4.6-fast-high` from a phantom `grok-4.6-fast`, + * and `extra-high` from `extra`. Resolution never guesses: anything the three + * tiers cannot justify comes back unresolved. + * + * One hop only — a `scoresAs` target is not itself re-resolved. + */ +import { findRegistryModelById, findRegistryScoresAs } from "../../config/providerModels.ts"; +import { splitClaudeEffortSuffix } from "../../config/providerModels.ts"; +import { splitCodexReasoningSuffix } from "../../executors/codex/reasoningSuffix.ts"; + +/** How a base id was reached; `null` means "not resolved — score the id as given". */ +export type ScoresAsVia = "explicit" | "effort-suffix" | "free-suffix" | null; + +export interface ScoresAsResolution { + /** The id whose quality scores apply. Equals the input when `via` is `null`. */ + base: string; + via: ScoresAsVia; +} + +/** Suffix marking a free-tier variant of a paid model (e.g. `mimo-v2.5-free`). */ +const FREE_SUFFIX = "-free"; + +/** Dispatch-time effort splitters, reused verbatim. Order is not significant: + * both are catalog-anchored below, so a wrong strip is rejected either way. */ +const EFFORT_SPLITTERS = [splitClaudeEffortSuffix, splitCodexReasoningSuffix] as const; + +export function resolveScoresAs(modelId: string): ScoresAsResolution { + const unresolved: ScoresAsResolution = { base: modelId, via: null }; + if (typeof modelId !== "string" || modelId.length === 0) return unresolved; + + // 1. Explicit registry declaration. One hop: the target must itself be a + // catalog id, and its own `scoresAs` (if any) is deliberately not followed. + const declared = findRegistryScoresAs(modelId); + if (declared && declared !== modelId && findRegistryModelById(declared)) { + return { base: declared, via: "explicit" }; + } + + // 2. Reasoning-effort suffix, catalog-anchored. + for (const split of EFFORT_SPLITTERS) { + const base = split(modelId).baseModel; + if (base && base !== modelId && findRegistryModelById(base)) { + return { base, via: "effort-suffix" }; + } + } + + // 3. Free-tier suffix, catalog-anchored. + if (modelId.endsWith(FREE_SUFFIX)) { + const base = modelId.slice(0, -FREE_SUFFIX.length); + if (base.length > 0 && findRegistryModelById(base)) { + return { base, via: "free-suffix" }; + } + } + + return unresolved; +} diff --git a/open-sse/services/autoCombo/taskFitness.ts b/open-sse/services/autoCombo/taskFitness.ts index 6489583ae50e..83899365e30c 100644 --- a/open-sse/services/autoCombo/taskFitness.ts +++ b/open-sse/services/autoCombo/taskFitness.ts @@ -7,6 +7,9 @@ * Resolution chain (highest → lowest priority): * 1. User override — DB `model_intelligence` where source='user_override' * 2. Arena ELO — DB `model_intelligence` where source='arena_elo' + * 2b. Layers 1-2 retried against the base model this id inherits quality scores + * from, when `resolveScoresAs` resolves one (#11489). Reported as + * `:inherited`. * 3. Models.dev tier — derived from `model_capabilities` table capability data * 4. Static FITNESS_TABLE — existing hardcoded lookup (current behavior) * 5. Wildcard boosts — existing pattern matching boosts (current behavior) @@ -20,6 +23,7 @@ import { setUserFitnessOverrideEntry, deleteUserFitnessOverrideEntry, } from "../../../src/lib/db/modelIntelligence.ts"; +import { resolveScoresAs } from "./scoresAs.ts"; const FITNESS_TABLE: Record> = { coding: { @@ -353,20 +357,23 @@ export function getTaskFitnessWithSource( return { score: userOverride, source: "user_override" }; } - // Try arena_elo with the literal model id first (e.g. "mimo-v2.5"). If that's - // a miss and the model id carries a "-free" suffix (e.g. "mimo-v2.5-free"), - // try the un-suffixed base id so free-tier variants inherit the arena_elo - // score of their paid counterpart. This is what operators expect: the - // upstream's `mimo-v2.5` is benchmarked once, and `mimo-v2.5-free` should - // pick up the same signal rather than falling through to the wildcard 0.5 - // and losing every free-vs-paid comparison. const arenaElo = queryModelIntelligence(normalizedModel, normalizedTask, "arena_elo"); if (arenaElo !== null) { return { score: arenaElo, source: "arena_elo" }; } - const arenaEloBase = lookupFreeAliasArenaElo(normalizedModel, normalizedTask); - if (arenaEloBase !== null) { - return { score: arenaEloBase, source: "arena_elo_free_alias" }; + + // Layers 1-2, retried against the base model this id inherits quality from + // (#11489). Every DB-backed source publishes scores for BASE models only, so + // a variant id — an effort suffix (`gpt-5.6-sol-xhigh`), a vendor alias + // (`gpt-5.6`), a `-free` tier marker (`mimo-v2.5-free`, #4517) — misses both + // literal lookups and used to fall all the way to the wildcard 0.5, losing + // every comparison against a base model that happens to be benchmarked. + // The score is inherited VERBATIM: the 12-factor scoring already prices cost + // and latency per variant, so there is no basis for inventing an effort + // delta. `:inherited` keeps the indirection visible to callers. + const inherited = lookupInheritedFitness(normalizedModel, normalizedTask); + if (inherited !== null) { + return inherited; } const tierScore = getModelsDevTierFitness(normalizedModel, normalizedTask); @@ -382,24 +389,29 @@ export function getTaskFitnessWithSource( return { score: lookupWildcardBoosts(normalizedModel, normalizedTask), source: "wildcard_boost" }; } -/** Suffix used to mark free-tier model variants (e.g. "mimo-v2.5-free"). */ -const FREE_SUFFIX = "-free"; - /** - * Strip a trailing "-free" suffix from the model id and re-query arena_elo. - * Returns `null` when the original id has no "-free" suffix, when the base id - * is identical to the original, or when no arena_elo row exists for the base. + * Re-run the two DB-backed layers against the base model `normalizedModel` + * inherits quality scores from (#11489). Returns `null` when the id resolves to + * itself (nothing to inherit) or when the base has no row either. * - * Examples: - * "mimo-v2.5-free" → look up "mimo-v2.5" - * "deepseek-v4-flash-free" → look up "deepseek-v4-flash" - * "big-pickle" → no "-free" suffix → return null (skip) + * This subsumes the former `-free` arena_elo special case (#4517) and extends + * it: the `-free` base is now consulted for `user_override` too, and the same + * indirection now covers effort suffixes and vendor aliases. `resolveScoresAs` + * is catalog-anchored, so a base that is not a routable id is never queried. */ -function lookupFreeAliasArenaElo(normalizedModel: string, normalizedTask: string): number | null { - if (!normalizedModel.endsWith(FREE_SUFFIX)) return null; - const baseId = normalizedModel.slice(0, -FREE_SUFFIX.length); - if (baseId.length === 0 || baseId === normalizedModel) return null; - return queryModelIntelligence(baseId, normalizedTask, "arena_elo"); +function lookupInheritedFitness( + normalizedModel: string, + normalizedTask: string +): { score: number; source: string } | null { + const { base, via } = resolveScoresAs(normalizedModel); + if (via === null || base === normalizedModel) return null; + const normalizedBase = base.toLowerCase(); + + for (const source of ["user_override", "arena_elo"] as const) { + const score = queryModelIntelligence(normalizedBase, normalizedTask, source); + if (score !== null) return { score, source: `${source}:inherited` }; + } + return null; } export function setUserFitnessOverride(model: string, category: string, score: number): void { diff --git a/tests/unit/task-fitness-scores-as-11489.test.ts b/tests/unit/task-fitness-scores-as-11489.test.ts new file mode 100644 index 000000000000..03f85f24f51b --- /dev/null +++ b/tests/unit/task-fitness-scores-as-11489.test.ts @@ -0,0 +1,53 @@ +import { describe, it, before, after } from "node:test"; +import assert from "node:assert/strict"; +import { + getTaskFitnessWithSource, + setUserFitnessOverride, + clearUserFitnessOverride, + invalidateFitnessCache, +} from "../../open-sse/services/autoCombo/taskFitness.ts"; +import { resetDbInstance } from "../../src/lib/db/core.ts"; + +/** + * TDD regression for #11489: an effort variant must inherit its base model's + * task-fitness score instead of falling through to the wildcard 0.5, and a + * sibling model must NOT inherit it. + */ +describe("taskFitness inherits from the scoresAs base (#11489)", () => { + before(() => { + setUserFitnessOverride("gpt-5.6-sol", "coding", 0.97); + invalidateFitnessCache(); + }); + + after(() => { + clearUserFitnessOverride("gpt-5.6-sol", "coding"); + invalidateFitnessCache(); + resetDbInstance(); + }); + + it("scores the base model from its own user_override row", () => { + const result = getTaskFitnessWithSource("gpt-5.6-sol", "coding"); + assert.equal(result.score, 0.97); + assert.equal(result.source, "user_override"); + }); + + it("inherits the base score for an effort variant, tagged as inherited", () => { + // Before the fix this returned the wildcard 0.5. + const result = getTaskFitnessWithSource("gpt-5.6-sol-xhigh", "coding"); + assert.equal(result.score, 0.97); + assert.equal(result.source, "user_override:inherited"); + }); + + it("inherits through an explicit forward vendor alias (gpt-5.6 -> gpt-5.6-sol)", () => { + const result = getTaskFitnessWithSource("gpt-5.6", "coding"); + assert.equal(result.score, 0.97); + assert.equal(result.source, "user_override:inherited"); + }); + + it("does NOT leak the base score to a sibling model", () => { + const result = getTaskFitnessWithSource("gpt-5.6-luna", "coding"); + assert.notEqual(result.source, "user_override"); + assert.notEqual(result.source, "user_override:inherited"); + assert.notEqual(result.score, 0.97); + }); +}); From dbe0034f1515df21c6996c075d7236f36e83115e Mon Sep 17 00:00:00 2001 From: MumuTW <42820974+MumuTW@users.noreply.github.com> Date: Tue, 25 Aug 2026 18:00:17 +0800 Subject: [PATCH 2/5] chore(changelog): add fragment for #11492 --- changelog.d/fixes/11492-taskfitness-scores-as.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/fixes/11492-taskfitness-scores-as.md diff --git a/changelog.d/fixes/11492-taskfitness-scores-as.md b/changelog.d/fixes/11492-taskfitness-scores-as.md new file mode 100644 index 000000000000..e1b02d4871a4 --- /dev/null +++ b/changelog.d/fixes/11492-taskfitness-scores-as.md @@ -0,0 +1 @@ +- **fix(autoCombo):** effort/alias model variants (`gpt-5.6-sol-xhigh`, `gpt-5.6`, cursor's `claude-4.6-opus-high`) inherit their base model's task fitness instead of falling to the wildcard 0.5 ([#11492](https://github.com/diegosouzapw/OmniRoute/pull/11492)) From 7ded2f74a790af8d0915b5986103a2dae7305eaa Mon Sep 17 00:00:00 2001 From: MumuTW <42820974+MumuTW@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:27:15 +0800 Subject: [PATCH 3/5] fix(intelligence): synthesize base-model arena rows from effort/harness variants; drop MODEL_ALIAS_MAP (#11504) --- src/lib/arenaEloSync.ts | 110 +++++++++++++------ tests/unit/arena-elo-sync.test.ts | 174 +++++++++++++++++++++--------- 2 files changed, 201 insertions(+), 83 deletions(-) diff --git a/src/lib/arenaEloSync.ts b/src/lib/arenaEloSync.ts index fa4284b2c5fb..427763b4a8e3 100644 --- a/src/lib/arenaEloSync.ts +++ b/src/lib/arenaEloSync.ts @@ -10,6 +10,8 @@ * On by default; opt out via Dashboard Feature Flags or ARENA_ELO_SYNC_ENABLED=false. */ +import { resolveScoresAs } from "@omniroute/open-sse/services/autoCombo/scoresAs.ts"; + import { isArenaEloSyncEnabled } from "@/shared/utils/featureFlags"; import { backupDbFile } from "./db/backup"; @@ -139,22 +141,6 @@ const VENDOR_PREFIXES = [ "ai21/", ] as const; -/** - * OmniRoute model aliases: canonical name → known aliases. - * Creates additional DB entries for each alias so that models - * are findable under any name OmniRoute uses internally. - */ -const MODEL_ALIAS_MAP: Record = { - "claude-opus-4-6-thinking": ["claude-opus-4", "anthropic/claude-opus-4"], - "claude-sonnet-4-5": ["claude-sonnet-4.5", "anthropic/claude-sonnet-4.5"], - "gpt-5.5": ["openai/gpt-5.5", "gpt-5"], - "gemini-3-flash": ["google/gemini-3-flash", "gemini-flash"], - "deepseek-r1": ["deepseek/deepseek-r1", "if/deepseek-r1"], - "kimi-k2-thinking": ["moonshot/kimi-k2"], - "qwen3-coder-plus": ["alibaba/qwen3-coder"], - "llama-4": ["meta/llama-4", "llama4"], -}; - /** Votes threshold for "high" confidence. */ const HIGH_CONFIDENCE_VOTES = 5000; @@ -196,17 +182,25 @@ function getEffectiveArenaEloSyncEnabled(): boolean { // ─── Model name normalization ──────────────────────────── +/** + * Trailing harness annotation the Arena leaderboard appends to some entries, + * e.g. "gpt-5.6-sol-xhigh (codex-harness)". It describes the scaffold the model + * was measured under, not the model id, so it never belongs in a stored key. + */ +const HARNESS_ANNOTATION_RE = /\s*\([^)]*\)\s*$/; + /** * Normalize a model name from the Arena leaderboard. * - * Lowercases the name and strips known vendor prefixes - * (e.g. "anthropic/claude-opus-4" → "claude-opus-4"). + * Lowercases the name, drops a trailing harness annotation + * ("gpt-5.6-sol-xhigh (codex-harness)" → "gpt-5.6-sol-xhigh") and strips known + * vendor prefixes ("anthropic/claude-opus-4" → "claude-opus-4"). * * @param rawName - The raw model name from the API response. * @returns The cleaned, lowercase model name. */ export function normalizeModelName(rawName: string): string { - let name = rawName.toLowerCase(); + let name = rawName.toLowerCase().replace(HARNESS_ANNOTATION_RE, ""); for (const prefix of VENDOR_PREFIXES) { if (name.startsWith(prefix)) { name = name.slice(prefix.length); @@ -295,7 +289,14 @@ function computeConfidence(votes: number): "high" | "medium" | "low" { * - "text" → default, review, documentation, debugging * - "code" → coding * - * Known OmniRoute model aliases are also expanded into additional entries. + * The leaderboard scores harness × effort combinations as separate entries + * ("claude-opus-5-high", "claude-opus-5-max"), so a request for the bare id + * would miss every row. Each variant that `resolveScoresAs()` resolves to a + * routable base id therefore also contributes a synthesized base row carrying + * the BEST task fit among that base's variants (the per-effort entries measure + * the same weights at different budgets — the model's ceiling is the max, and + * cost/latency are separate factors in the 12-factor score). An explicitly + * measured base row is never lowered by synthesis. * * @param data - Map of leaderboard category → Arena leaderboard data. * @returns Array of model intelligence entries ready for DB upsert. @@ -325,7 +326,7 @@ export function transformToModelIntelligence( const taskFit = 0.4 + 0.58 * ((model.score - minElo) / eloRange); for (const taskCategory of taskCategories) { - const entry: Omit = { + entries.push({ model: normalizedModel, category: taskCategory, source: "arena_elo", @@ -333,24 +334,63 @@ export function transformToModelIntelligence( eloRaw: model.score, confidence, expiresAt, - }; - entries.push(entry); - - // Expand known aliases - const aliases = MODEL_ALIAS_MAP[normalizedModel]; - if (aliases) { - for (const alias of aliases) { - entries.push({ - ...entry, - model: alias, - }); - } - } + }); } } } - return entries; + return withSynthesizedBaseRows(entries); +} + +/** + * Add one synthesized base row per (base, category) for every leaderboard entry + * that is an effort/alias variant of a routable catalog id. + * + * Runs as a pass over the finished variant rows so that all variants of a base + * are visible at once: the winner is the highest task fit, and it contributes + * its own `eloRaw` and `confidence` (no invented confidence label — the column's + * vocabulary stays high/medium/low). A base that the leaderboard measured + * directly keeps its own row unless a variant scored strictly higher. + * + * Resolution never guesses: `resolveScoresAs` returning `via: null` means the + * stripped base is not a catalog id, so nothing is synthesized for it. + * + * @param entries - Variant rows, one per (leaderboard entry, task category). + * @returns The same rows plus the synthesized base rows. + */ +function withSynthesizedBaseRows( + entries: Array> +): Array> { + const keyOf = (model: string, category: string) => `${model}|${category}`; + const indexByKey = new Map(); + entries.forEach((entry, index) => indexByKey.set(keyOf(entry.model, entry.category), index)); + + const best = new Map>(); + for (const entry of entries) { + const { base, via } = resolveScoresAs(entry.model); + if (via === null || base === entry.model) continue; + + const key = keyOf(base, entry.category); + const current = best.get(key); + if (!current || entry.score > current.score) { + best.set(key, { ...entry, model: base }); + } + } + + const result = [...entries]; + for (const [key, candidate] of best) { + const existingIndex = indexByKey.get(key); + if (existingIndex === undefined) { + result.push(candidate); + continue; + } + // The leaderboard measured the base itself — only a strictly better variant wins. + if (candidate.score > result[existingIndex].score) { + result[existingIndex] = candidate; + } + } + + return result; } // ─── Main sync function ────────────────────────────────── diff --git a/tests/unit/arena-elo-sync.test.ts b/tests/unit/arena-elo-sync.test.ts index 717963458d98..2113e282920f 100644 --- a/tests/unit/arena-elo-sync.test.ts +++ b/tests/unit/arena-elo-sync.test.ts @@ -40,6 +40,7 @@ const { } = await import("../../src/lib/arenaEloSync.ts"); const { setFeatureFlagOverride, removeFeatureFlagOverride } = await import("../../src/lib/db/featureFlags.ts"); +const { resolveScoresAs } = await import("../../open-sse/services/autoCombo/scoresAs.ts"); import type { ArenaLeaderboardData, @@ -328,26 +329,6 @@ describe("transformToModelIntelligence()", () => { } }); - it("expands model aliases for known models", () => { - const data = makeLeaderboardMap({ - text: [ - makeModelEntry({ - model: "anthropic/claude-opus-4-6-thinking", - score: 1400, - votes: 5000, - rank: 1, - }), - ], - }); - - const entries = transformToModelIntelligence(data); - const models = entries.map((e) => e.model); - - assert.ok(models.includes("claude-opus-4-6-thinking")); - assert.ok(models.includes("claude-opus-4")); - assert.ok(models.includes("anthropic/claude-opus-4")); - }); - it("empty leaderboard → no entries", () => { const data = makeLeaderboardMap({ text: [] }); const entries = transformToModelIntelligence(data); @@ -409,6 +390,131 @@ describe("transformToModelIntelligence()", () => { }); }); +// ═══════════════════════════════════════════════════════════ +// 2b. Variant → base row synthesis (#11504) +// ═══════════════════════════════════════════════════════════ + +describe("transformToModelIntelligence() — base-model synthesis", () => { + it("normalizeModelName strips a trailing harness annotation", () => { + assert.strictEqual( + normalizeModelName("gpt-5.6-sol-xhigh (codex-harness)"), + "gpt-5.6-sol-xhigh" + ); + }); + + it("normalizeModelName strips vendor prefix and harness annotation together", () => { + assert.strictEqual( + normalizeModelName("OpenAI/GPT-5.6-Sol-xhigh (Codex-Harness)"), + "gpt-5.6-sol-xhigh" + ); + }); + + it("harness-annotated variant yields both the variant row and a base row", () => { + const data = makeLeaderboardMap({ + code: [ + makeModelEntry({ + model: "gpt-5.6-sol-xhigh (codex-harness)", + score: 1700, + votes: 5000, + rank: 1, + }), + ], + }); + + const entries = transformToModelIntelligence(data); + const coding = entries.filter((e) => e.category === "coding").map((e) => e.model); + + assert.ok(coding.includes("gpt-5.6-sol-xhigh"), "variant row is kept"); + assert.ok(coding.includes("gpt-5.6-sol"), "base row is synthesized"); + // `gpt-5.6` is a vendor alias of `gpt-5.6-sol`, resolved at lookup time — never stored here. + assert.ok(!coding.includes("gpt-5.6")); + }); + + it("base row carries the best variant's score and eloRaw", () => { + const data = makeLeaderboardMap({ + code: [ + makeModelEntry({ model: "claude-opus-5-max", score: 1691, votes: 5000, rank: 1 }), + makeModelEntry({ model: "claude-opus-5-high", score: 1663, votes: 5000, rank: 2 }), + ], + }); + + const entries = transformToModelIntelligence(data); + const base = entries.find((e) => e.model === "claude-opus-5" && e.category === "coding"); + const max = entries.find((e) => e.model === "claude-opus-5-max" && e.category === "coding"); + + assert.ok(base, "synthesized base row exists"); + assert.ok(max); + assert.strictEqual(base.eloRaw, 1691); + assert.strictEqual(base.score, max.score); + }); + + it("never lowers an explicitly measured base row", () => { + const data = makeLeaderboardMap({ + code: [ + makeModelEntry({ model: "claude-opus-5", score: 1700, votes: 5000, rank: 1 }), + makeModelEntry({ model: "claude-opus-5-max", score: 1691, votes: 5000, rank: 2 }), + ], + }); + + const entries = transformToModelIntelligence(data); + const base = entries.filter((e) => e.model === "claude-opus-5" && e.category === "coding"); + + assert.strictEqual(base.length, 1); + assert.strictEqual(base[0].eloRaw, 1700); + }); + + it("does not collapse generations (regression guard for the deleted alias map)", () => { + const data = makeLeaderboardMap({ + text: [makeModelEntry({ model: "openai/gpt-5.5", score: 1600, votes: 5000, rank: 1 })], + }); + + const entries = transformToModelIntelligence(data); + const models = entries.map((e) => e.model); + + assert.ok(models.includes("gpt-5.5")); + assert.ok(!models.includes("gpt-5"), "gpt-5.5 must never emit a gpt-5 row"); + }); + + it("synthesizes nothing when the stripped base is not a routable catalog id", () => { + // Catalog-anchored by construction: assert the premise instead of a frozen catalog fact. + assert.strictEqual(resolveScoresAs("grok-4.6-fast-high").via, null); + + const data = makeLeaderboardMap({ + code: [makeModelEntry({ model: "grok-4.6-fast-high", score: 1600, votes: 5000, rank: 1 })], + }); + + const entries = transformToModelIntelligence(data); + const models = entries.map((e) => e.model); + + assert.deepStrictEqual(models, ["grok-4.6-fast-high"]); + }); + + it("is idempotent and emits no duplicate (model, category) keys", () => { + const data = makeLeaderboardMap({ + code: [ + makeModelEntry({ model: "claude-opus-5-max", score: 1691, votes: 5000, rank: 1 }), + makeModelEntry({ model: "claude-opus-5-high", score: 1663, votes: 5000, rank: 2 }), + makeModelEntry({ + model: "gpt-5.6-sol-xhigh (codex-harness)", + score: 1700, + votes: 5000, + rank: 3, + }), + ], + }); + + const first = transformToModelIntelligence(data); + const second = transformToModelIntelligence(data); + + const strip = (entries: ReturnType) => + entries.map(({ expiresAt: _expiresAt, ...rest }) => rest); + assert.deepStrictEqual(strip(second), strip(first)); + + const keys = first.map((e) => `${e.model}|${e.category}`); + assert.strictEqual(new Set(keys).size, keys.length); + }); +}); + // ═══════════════════════════════════════════════════════════ // 3. fetchArenaLeaderboards() // ═══════════════════════════════════════════════════════════ @@ -692,34 +798,6 @@ describe("syncArenaElo()", () => { assert.ok(status.lastSync); assert.ok(status.lastSyncModelCount > 0); }); - - it("model aliases are stored in DB alongside canonical names", async () => { - const textData = makeLeaderboardData( - [ - makeModelEntry({ - model: "anthropic/claude-opus-4-6-thinking", - score: 1400, - votes: 5000, - rank: 1, - }), - ], - "text" - ); - - mockFetch(async (url: string) => { - if (url.includes("name=text")) return jsonResponse(textData); - if (url.includes("name=code")) return jsonResponse(makeLeaderboardData([], "code")); - return new Response("Not found", { status: 404 }); - }); - - await syncArenaElo(); - - const entries = getAllEntries(); - const models = entries.map((e) => String(e.model)); - - assert.ok(models.includes("claude-opus-4-6-thinking")); - assert.ok(models.includes("claude-opus-4")); - }); }); // ═══════════════════════════════════════════════════════════ From ed2c4993b5053c15ee8ae1d6b7e7851a6ac7957e Mon Sep 17 00:00:00 2001 From: MumuTW <42820974+MumuTW@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:28:40 +0800 Subject: [PATCH 4/5] chore(changelog): add fragment for #11506 --- changelog.d/fixes/11506-arena-elo-base-rows.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/fixes/11506-arena-elo-base-rows.md diff --git a/changelog.d/fixes/11506-arena-elo-base-rows.md b/changelog.d/fixes/11506-arena-elo-base-rows.md new file mode 100644 index 000000000000..c3fd744fa443 --- /dev/null +++ b/changelog.d/fixes/11506-arena-elo-base-rows.md @@ -0,0 +1 @@ +- **fix(intelligence):** Arena ELO sync stores a synthesized base-model row for every effort/harness variant (`claude-opus-5-max` → `claude-opus-5`, `gpt-5.6-sol-xhigh (codex-harness)` → `gpt-5.6-sol`), so bare model requests reach the synced intelligence layer instead of the static fallback table; the hand-maintained `MODEL_ALIAS_MAP` — which collapsed generations (`gpt-5.5` → `gpt-5`) — is gone ([#11506](https://github.com/diegosouzapw/OmniRoute/pull/11506)) From a52885eeaaa3902520364eba71c0eae9762765e3 Mon Sep 17 00:00:00 2001 From: MumuTW <42820974+MumuTW@users.noreply.github.com> Date: Tue, 25 Aug 2026 19:30:09 +0800 Subject: [PATCH 5/5] test(intelligence): invert the two MODEL_ALIAS_MAP tests into anti-collapse guards (#11504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two existing tests that asserted MODEL_ALIAS_MAP expansion encoded exactly the generation-collapsing behaviour #11504 removes: a claude-opus-4-6-thinking score copied onto claude-opus-4. Rather than delete them, invert them — - "expands model aliases for known models" → "does not copy a variant's score onto a different generation": same fixture, now asserts claude-opus-4-6-thinking IS emitted and claude-opus-4 / anthropic/claude-opus-4 are NOT. - "model aliases are stored in DB alongside canonical names" → "stores no cross-generation alias rows in the DB": same assertion through syncArenaElo() against the DB rows. Every other pre-existing test is untouched. --- tests/unit/arena-elo-sync.test.ts | 50 +++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/unit/arena-elo-sync.test.ts b/tests/unit/arena-elo-sync.test.ts index 2113e282920f..0fc7b07ae9f0 100644 --- a/tests/unit/arena-elo-sync.test.ts +++ b/tests/unit/arena-elo-sync.test.ts @@ -329,6 +329,28 @@ describe("transformToModelIntelligence()", () => { } }); + it("does not copy a variant's score onto a different generation (MODEL_ALIAS_MAP removed, #11504)", () => { + const data = makeLeaderboardMap({ + text: [ + makeModelEntry({ + model: "anthropic/claude-opus-4-6-thinking", + score: 1400, + votes: 5000, + rank: 1, + }), + ], + }); + + const entries = transformToModelIntelligence(data); + const models = entries.map((e) => e.model); + + assert.ok(models.includes("claude-opus-4-6-thinking")); + // Was asserted the other way round while MODEL_ALIAS_MAP existed: it copied this + // score onto `claude-opus-4`, so a claude-opus-4 request read a 4.6-thinking ELO. + assert.ok(!models.includes("claude-opus-4")); + assert.ok(!models.includes("anthropic/claude-opus-4")); + }); + it("empty leaderboard → no entries", () => { const data = makeLeaderboardMap({ text: [] }); const entries = transformToModelIntelligence(data); @@ -798,6 +820,34 @@ describe("syncArenaElo()", () => { assert.ok(status.lastSync); assert.ok(status.lastSyncModelCount > 0); }); + + it("stores no cross-generation alias rows in the DB (MODEL_ALIAS_MAP removed, #11504)", async () => { + const textData = makeLeaderboardData( + [ + makeModelEntry({ + model: "anthropic/claude-opus-4-6-thinking", + score: 1400, + votes: 5000, + rank: 1, + }), + ], + "text" + ); + + mockFetch(async (url: string) => { + if (url.includes("name=text")) return jsonResponse(textData); + if (url.includes("name=code")) return jsonResponse(makeLeaderboardData([], "code")); + return new Response("Not found", { status: 404 }); + }); + + await syncArenaElo(); + + const entries = getAllEntries(); + const models = entries.map((e) => String(e.model)); + + assert.ok(models.includes("claude-opus-4-6-thinking")); + assert.ok(!models.includes("claude-opus-4")); + }); }); // ═══════════════════════════════════════════════════════════