From 8a30af69d046602b6baffd0bef3c17632d80c7e3 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Mon, 3 Aug 2026 18:48:23 -0700 Subject: [PATCH] fix(ui): verify and create must use the same token, not just the freshest each The ref fix made router creation use whichever token is current when the create call fires, but the preceding availability check still went through the query's own refetch, which stays bound to whatever token was current when that render's useQuery was set up. If the token rotated in between, that split verification from a stale caller's model list against creation under a different one, meaning the caller actually creating the router never had its own access checked. Fetch directly against accessTokenRef in the verification step too, so both calls agree on the same live identity instead of each independently chasing "freshest." --- .../add_model/add_auto_router_tab.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 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 3a78f19e8a1..c622b815dfa 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 @@ -7,7 +7,7 @@ import { all_admin_roles } from "@/utils/roles"; import { type ModelWriteScope } from "@/utils/modelPermissions"; import TeamDropdown from "../common_components/team_dropdown"; import { handleAddAutoRouterSubmit } from "./handle_add_auto_router_submit"; -import { fetchAvailableModels } from "@/components/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; import ComplexityRouterConfig, { ComplexityRouterConfigValue, DEFAULT_ADAPTIVE_WEIGHTS, @@ -204,13 +204,20 @@ const AddAutoRouterTab: React.FC = ({ // background refetch failure keeps trusting the stale cache by design - see modelsUnverifiable // above). The backend does not re-check a router's referenced model names against the caller's // access either, so this is the only place that can catch it: force a fresh fetch right before - // creating the router, rather than trusting whatever's cached. + // creating the router, rather than trusting whatever's cached. Fetch directly against + // accessTokenRef instead of the query's own refetch, which stays bound to whichever token was + // current when this render's useQuery was set up - using it here could verify one caller's + // models and create the router under another if the token rotates mid-check. const verifyPresetStillAvailable = async (presetKey: string): Promise => { const preset = getPresetByKey(presetKey); if (!preset) return false; - const { data: freshModels, isError: freshError } = await refetchModels(); - if (freshError) return false; - const freshSet = new Set((freshModels ?? []).map((m) => m.model_group)); + let freshModels: ModelGroup[]; + try { + freshModels = await fetchAvailableModels(accessTokenRef.current); + } catch { + return false; + } + const freshSet = new Set(freshModels.map((m) => m.model_group)); return getMissingModelsInPreset(preset, freshSet).length === 0; };