diff --git a/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts b/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts index 38280a0cdbd..6324b7fd611 100644 --- a/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts +++ b/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts @@ -11,6 +11,7 @@ import { buildPresetPrefill, buildModelAvailability, deploymentRefsFromModelInfo, + normalizeModelName, } from "./autorouter_presets"; import { DEFAULT_MATCH_THRESHOLD } from "@/components/add_model/SemanticKeywordMatching"; import { DEFAULT_ESCALATION_KEYWORDS } from "@/components/add_model/EscalationKeywords"; @@ -28,6 +29,29 @@ describe("autorouter_presets", () => { } }); + // buildPresetPrefill resolves every model reference through normalizeModelName, so two spellings + // of the same model in one tier (e.g. "claude-sonnet-4-5" and "claude-sonnet-4.5") collapse to one + // key. For tier_model_configs that silently drops one model's litellm_params; catch it in the + // bundled data itself, since nothing else validates preset authoring. + it("never spells the same model two ways within a single tier", () => { + for (const preset of getAllPresets()) { + const { tiers, tier_model_configs: configs } = preset.complexity_router_config; + for (const tier of Object.keys(tiers) as (keyof typeof tiers)[]) { + const fromTierList = tiers[tier] ?? []; + const fromConfigs = (configs?.[tier] ?? []).map((entry) => entry.model_name); + const names = new Set([...fromTierList, ...fromConfigs]); + const byNormalized = new Map(); + for (const name of names) { + const key = normalizeModelName(name); + byNormalized.set(key, [...(byNormalized.get(key) ?? []), name]); + } + for (const spellings of byNormalized.values()) { + expect(new Set(spellings).size, `${preset.key}.${tier}: ${spellings.join(", ")}`).toBe(1); + } + } + } + }); + it("resolves a preset by its stable JSON key, not its display label", () => { expect(getPresetByKey("anthropic_family")?.label).toBe("Anthropic Family"); expect(getPresetByKey("does_not_exist")).toBeUndefined(); diff --git a/ui/litellm-dashboard/src/lib/autorouter_presets.ts b/ui/litellm-dashboard/src/lib/autorouter_presets.ts index 68c4e8df79a..f6e7abee2f6 100644 --- a/ui/litellm-dashboard/src/lib/autorouter_presets.ts +++ b/ui/litellm-dashboard/src/lib/autorouter_presets.ts @@ -55,7 +55,7 @@ export const getRequiredModels = ( // differing only in that separator. Canonicalizing on "-" (the presets' own convention) lets both // spellings match without doing anything looser - two DIFFERENT model names never collide here, // only the punctuation within one version number does. -const normalizeModelName = (model: string): string => model.replace(/(\d)\.(\d)/g, "$1-$2"); +export const normalizeModelName = (model: string): string => model.replace(/(\d)\.(\d)/g, "$1-$2"); export interface DeploymentModelRef { modelGroup: string;