From aa4843cb020021ebb75e9098ead07de9af339a18 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Mon, 3 Aug 2026 17:31:47 -0700 Subject: [PATCH] fix(ui): revalidate the selected preset's models at submit time handlePresetChange only ever applies a preset that was verified available at selection time, but that guarantee could go stale by submit time if the caller's model access narrowed in between (a token change re-keys the model query without clearing the selection, since clearing it would erase in-progress Custom edits too). Re-run the same presetAvailability check at the submit boundary instead of trusting state gathered earlier, so a stale preset can no longer create a router referencing models the current caller doesn't have. --- .../add_model/add_auto_router_tab.test.tsx | 29 +++++++++++++++++++ .../add_model/add_auto_router_tab.tsx | 14 +++++++++ 2 files changed, 43 insertions(+) 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 efb46736ea5..f0115bf57aa 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 @@ -306,6 +306,35 @@ describe("AddAutoRouterTab", () => { expect(isOptionDisabled(optionByLabel("OpenAI Family")!)).toBe(true); }); + // handlePresetChange only ever applies an available preset, but that guarantee can go stale by + // submit time: select under a caller with the full family, then switch to a caller missing one + // of its models. Nothing clears the selection (that would erase in-progress Custom edits too), + // so submit itself must re-verify against the current caller's list before creating the router. + it("blocks submit when the selected preset's models are no longer available for the current caller", async () => { + const user = userEvent.setup(); + mockFetchAvailableModels + .mockResolvedValueOnce(ALL_FAMILY_MODELS) + .mockResolvedValueOnce(ALL_FAMILY_MODELS.filter((m) => m.model_group !== "o3")); + + const { rerender } = renderWithProviders( + , + ); + openTemplateDropdown(); + await waitFor(() => expect(isOptionDisabled(optionByLabel("OpenAI Family")!)).toBe(false)); + fireEvent.click(optionByLabel("OpenAI Family")!); + + rerender(); + await waitFor(() => expect(mockFetchAvailableModels).toHaveBeenCalledTimes(2)); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "stale-preset-router"); + await user.click(screen.getByRole("button", { name: /add auto router/i })); + + expect(NotificationManager.fromBackend).toHaveBeenCalledWith( + "This template's models are no longer available. Please reselect a template or switch to Custom.", + ); + expect(mockHandleAddAutoRouterSubmit).not.toHaveBeenCalled(); + }); + // Prefill must preserve a preset's deliberately-falsy fields (a 0 match threshold, an empty // escalation list). Using `||` instead of `??` would swap the 0 for the create-form default and // re-enable escalation the preset meant to turn off, so this asserts the exact submitted values. 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 9d1ed00d31c..0c37a195e6d 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 @@ -189,6 +189,20 @@ const AddAutoRouterTab: React.FC = ({ return; } + // handlePresetChange only ever applies a preset that was available at selection time; that + // guarantee can go stale by submit time (e.g. the caller's model access narrowed since), so + // re-verify here rather than trust state gathered earlier. + if (selectedPreset !== "custom") { + const preset = getPresetByKey(selectedPreset); + if (!preset || presetAvailability(preset).kind !== "available") { + setShowValidationErrors(true); + NotificationManager.fromBackend( + "This template's models are no longer available. Please reselect a template or switch to Custom.", + ); + return; + } + } + const { tiers, classifier_type: classifierType,