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,