From b140c02a913aa363100a740da423d739e91f9b9d Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Mon, 3 Aug 2026 17:48:41 -0700 Subject: [PATCH] fix(ui): stop treating a failed background model refetch as unverifiable react-query keeps the last successful list around when a later refetch fails: data stays populated, but isError flips true. presetAvailability read isError alone, so once a caller had a good cached list, any subsequent refetch hiccup (window refocus, a manual retry that itself fails, etc.) made every preset unverifiable again, wrongly blocked an already-selected preset at the new submit-time check, and showed a "models are no longer available" toast for models that were, per the cache, still there. Only treat the state as unverifiable when there has never been a successful fetch (data is still undefined); otherwise keep trusting the cached list, matching how react-query itself treats stale-but-valid data. --- .../add_model/add_auto_router_tab.test.tsx | 27 ++++++++++++++++++- .../add_model/add_auto_router_tab.tsx | 11 +++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx index f0115bf57aa..892950600a5 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx @@ -1,5 +1,5 @@ import { renderWithProviders, screen, waitFor, testQueryClient } from "../../../tests/test-utils"; -import { fireEvent } from "@testing-library/react"; +import { act, fireEvent } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { vi } from "vitest"; import AddAutoRouterTab from "./add_auto_router_tab"; @@ -200,6 +200,31 @@ describe("AddAutoRouterTab", () => { expect(mockFetchAvailableModels).toHaveBeenCalledTimes(2); }); + // react-query keeps the last successful list when a later refetch fails, so a background + // refetch error must not treat an already-verified, still-cached preset as unverifiable: the + // caller never sees a stale error message, and a preset they already picked stays submittable. + it("keeps a selected preset submit-reachable when a background refetch fails but cached models remain valid", async () => { + const user = userEvent.setup(); + mockFetchAvailableModels.mockResolvedValueOnce(ALL_FAMILY_MODELS).mockRejectedValue(new Error("boom")); + + renderWithProviders(); + openTemplateDropdown(); + await waitFor(() => expect(isOptionDisabled(optionByLabel("Anthropic Family")!)).toBe(false)); + fireEvent.click(optionByLabel("Anthropic Family")!); + + await act(async () => { + await testQueryClient.refetchQueries({ queryKey: ["availableModels", "autoRouter", "token"] }); + }); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "resilient-router"); + await user.click(screen.getByRole("button", { name: /add auto router/i })); + + await waitFor(() => expect(mockHandleAddAutoRouterSubmit).toHaveBeenCalled()); + expect(NotificationManager.fromBackend).not.toHaveBeenCalledWith( + "This template's models are no longer available. Please reselect a template or switch to Custom.", + ); + }); + // The headline behavior: selecting a preset must pre-fill the tier config so the created // router carries the preset's models. Real tier validation runs here (getMissingTiersError is // not stubbed), so if selection stopped pre-filling, the empty tiers would either block the 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 0c37a195e6d..58718d8e8da 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 @@ -106,7 +106,7 @@ const AddAutoRouterTab: React.FC = ({ }, [accessToken]); const { - data: modelInfo = [], + data, isLoading: modelsLoading, isError: modelsError, refetch: refetchModels, @@ -115,6 +115,11 @@ const AddAutoRouterTab: React.FC = ({ queryFn: () => fetchAvailableModels(accessToken), enabled: Boolean(accessToken), }); + const modelInfo = React.useMemo(() => data ?? [], [data]); + // react-query keeps the last successful list around when a later refetch fails, so isError + // alone can't tell "never loaded" apart from "loaded, then a background refetch errored" - only + // the former leaves us with nothing trustworthy to verify a preset's models against. + const modelsUnverifiable = modelsError && data === undefined; const isAdmin = all_admin_roles.includes(userRole); @@ -133,7 +138,7 @@ const AddAutoRouterTab: React.FC = ({ // makes the load-race (pick during loading, then discover a missing model) unrepresentable. const presetAvailability = (preset: AutoRouterPreset): PresetAvailability => { if (modelsLoading) return { kind: "loading" }; - if (modelsError) return { kind: "unverifiable" }; + if (modelsUnverifiable) return { kind: "unverifiable" }; const missing = getMissingModelsInPreset(preset, availableModelSet); return missing.length > 0 ? { kind: "missing_models", models: missing } : { kind: "available" }; }; @@ -372,7 +377,7 @@ const AddAutoRouterTab: React.FC = ({ {showValidationErrors && !selectedPreset && (
Please select a template
)} - {modelsError && ( + {modelsUnverifiable && (
Could not load available models.{" "}