From de2c59addc67fdd626428cd47ee7aca72be61ef8 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Thu, 13 Aug 2026 12:26:57 -0700 Subject: [PATCH] fix(UI): probe the pinned default model in the auto router connection test The connection test built its targets from the tiers and the embedding model only, so a Default Model pin outside every tier was never reached and a green result could hide an unreachable default. model_info_view had already hand rolled the dedupe and append locally, so the rule moved into buildAutoRouterTestTargets and both call sites now share it. --- .../add_model/add_auto_router_tab.tsx | 6 ++- .../build_auto_router_test_targets.test.ts | 42 +++++++++++++++++++ .../build_auto_router_test_targets.ts | 15 ++++++- .../src/components/model_info_view.tsx | 35 ++++++++-------- 4 files changed, 77 insertions(+), 21 deletions(-) diff --git a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx index 8aabfa57ab6..78019b8a547 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx @@ -395,11 +395,13 @@ const AddAutoRouterTab: React.FC = ({ }; const handleTestConnection = () => { - const targets = buildAutoRouterTestTargets({ + const testTargetParams = { tiers: complexityRouterConfig.tiers, semanticMatchingEnabled, embeddingModel, - }); + defaultModel: resolveComplexityDefaultModel(complexityRouterConfig.tiers, complexityRouterConfig.default_model), + }; + const targets = buildAutoRouterTestTargets(testTargetParams); if (targets.length === 0) { NotificationManager.fromBackend("Please select at least one model for a complexity tier"); diff --git a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts index f8ab4bab903..85fd846ddbc 100644 --- a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts +++ b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts @@ -77,4 +77,46 @@ describe("buildAutoRouterTestTargets", () => { }); expect(targets).toEqual([{ labels: ["SIMPLE"], modelGroup: "gpt-4o-mini", mode: "chat" }]); }); + + // A pin outside every tier is still a live fallback destination (empty-tier landings, and a + // failed LLM classifier routing to it), so a passing test must reach it too or it is only + // proving the tiers are reachable, not the router. + it("appends the default model as its own target when it is not already in a tier", () => { + const targets = buildAutoRouterTestTargets({ + tiers, + semanticMatchingEnabled: false, + embeddingModel: undefined, + defaultModel: "claude-3-opus", + }); + expect(targets).toEqual([ + { labels: ["SIMPLE"], modelGroup: "gpt-4o-mini", mode: "chat" }, + { labels: ["MEDIUM", "COMPLEX"], modelGroup: "claude-sonnet-4", mode: "chat" }, + { labels: ["REASONING"], modelGroup: "o3", mode: "chat" }, + { labels: ["Default"], modelGroup: "claude-3-opus", mode: "chat" }, + ]); + }); + + it("does not duplicate a default model that a tier already covers", () => { + const targets = buildAutoRouterTestTargets({ + tiers, + semanticMatchingEnabled: false, + embeddingModel: undefined, + defaultModel: "claude-sonnet-4", + }); + expect(targets).toEqual([ + { labels: ["SIMPLE"], modelGroup: "gpt-4o-mini", mode: "chat" }, + { labels: ["MEDIUM", "COMPLEX"], modelGroup: "claude-sonnet-4", mode: "chat" }, + { labels: ["REASONING"], modelGroup: "o3", mode: "chat" }, + ]); + }); + + it.each([[undefined], [""], [" "]])("adds no default target for %o", (defaultModel) => { + const targets = buildAutoRouterTestTargets({ + tiers: { SIMPLE: ["gpt-4o-mini"], MEDIUM: [], COMPLEX: [], REASONING: [] }, + semanticMatchingEnabled: false, + embeddingModel: undefined, + defaultModel, + }); + expect(targets).toEqual([{ labels: ["SIMPLE"], modelGroup: "gpt-4o-mini", mode: "chat" }]); + }); }); diff --git a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts index 708a25c16f0..471552fc84f 100644 --- a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts +++ b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts @@ -12,6 +12,9 @@ export interface BuildAutoRouterTestTargetsParams { tiers: ComplexityTiers; semanticMatchingEnabled: boolean; embeddingModel: string | undefined; + /** The resolved default model - see resolveComplexityDefaultModel. A live fallback destination, + * so it is probed even when no tier lists it. */ + defaultModel?: string; } // Keys drive iteration order; `satisfies Record` makes it a @@ -27,8 +30,9 @@ export const buildAutoRouterTestTargets = ({ tiers, semanticMatchingEnabled, embeddingModel, + defaultModel, }: BuildAutoRouterTestTargetsParams): AutoRouterTestTarget[] => { - const groupedByModel = TIER_ORDER.reduce>((acc, tier) => { + const tieredByModel = TIER_ORDER.reduce>((acc, tier) => { return (tiers[tier] ?? []).reduce((tierAcc, rawModel) => { const modelGroup = rawModel?.trim(); if (!modelGroup) return tierAcc; @@ -36,6 +40,15 @@ export const buildAutoRouterTestTargets = ({ }, acc); }, {}); + // The default is a live destination whenever the chosen tier has no model, and when an LLM + // classifier fails with "Route to the default model", so a green test that skipped it would be + // reporting on a router it had not fully reached. + const resolvedDefault = defaultModel?.trim(); + const groupedByModel = + resolvedDefault && !(resolvedDefault in tieredByModel) + ? { ...tieredByModel, [resolvedDefault]: ["Default"] } + : tieredByModel; + const tierTargets: AutoRouterTestTarget[] = Object.entries(groupedByModel).map(([modelGroup, labels]) => ({ labels, modelGroup, diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index e1a4d4311ab..6f29443f1c5 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -40,7 +40,7 @@ import { isMaskedSecret, stripMaskedSecrets } from "../utils/maskedSecretUtils"; import { formItemValidateJSON, truncateString } from "../utils/textUtils"; import AutoRouterConnectionTest from "./add_model/auto_router_connection_test"; import { AutoRouterTestTarget, buildAutoRouterTestTargets } from "./add_model/build_auto_router_test_targets"; -import { normalizeTierModels } from "./add_model/complexity_router_tiers"; +import { normalizeTierModels, resolveComplexityDefaultModel } from "./add_model/complexity_router_tiers"; import { hasAutoRouterEditor, isAutoRouterDeployment, @@ -146,6 +146,7 @@ interface ComplexityRouterTierConfig { }; semantic_keyword_matching?: boolean; embedding_model?: string; + default_model?: string; } interface ComplexityRouterModelData { @@ -170,25 +171,23 @@ const buildComplexityRouterTestTargets = ( config = rawConfig; } - const tierTargets = buildAutoRouterTestTargets({ - tiers: { - SIMPLE: normalizeTierModels(config.tiers?.SIMPLE), - MEDIUM: normalizeTierModels(config.tiers?.MEDIUM), - COMPLEX: normalizeTierModels(config.tiers?.COMPLEX), - REASONING: normalizeTierModels(config.tiers?.REASONING), - }, + const tiers = { + SIMPLE: normalizeTierModels(config.tiers?.SIMPLE), + MEDIUM: normalizeTierModels(config.tiers?.MEDIUM), + COMPLEX: normalizeTierModels(config.tiers?.COMPLEX), + REASONING: normalizeTierModels(config.tiers?.REASONING), + }; + + // litellm_params overrides the config blob at init (complexity_router.py), so it wins here too. + const pinned = modelData?.litellm_params?.complexity_router_default_model || config.default_model; + + const testTargetParams = { + tiers, semanticMatchingEnabled: Boolean(config.semantic_keyword_matching), embeddingModel: config.embedding_model, - }); - - const defaultModel = modelData?.litellm_params?.complexity_router_default_model?.trim(); - if (!defaultModel || tierTargets.some((target) => target.modelGroup === defaultModel)) { - return tierTargets; - } - return [ - ...tierTargets, - { labels: ["Default (unconfigured tiers)"], modelGroup: defaultModel, mode: "chat" as const }, - ]; + defaultModel: resolveComplexityDefaultModel(tiers, pinned), + }; + return buildAutoRouterTestTargets(testTargetParams); }; export default function ModelInfoView({