diff --git a/ui/litellm-dashboard/src/autorouter_presets.json b/ui/litellm-dashboard/src/autorouter_presets.json
index b1c0235eb13..ec9cfe1eac4 100644
--- a/ui/litellm-dashboard/src/autorouter_presets.json
+++ b/ui/litellm-dashboard/src/autorouter_presets.json
@@ -1,18 +1,4 @@
{
- "sample_spec": {
- "label": "Anthropic Family (Example)",
- "description": "Complexity-tiered routing using only Anthropic models",
- "complexity_router_config": {
- "tiers": {
- "SIMPLE": ["claude-haiku-4-5"],
- "MEDIUM": ["claude-sonnet-4-5"],
- "COMPLEX": ["claude-opus-5"],
- "REASONING": ["claude-opus-5"]
- },
- "classifier_type": "heuristic",
- "escalation_keywords": ["LITELLM ESCALATE"]
- }
- },
"anthropic_family": {
"label": "Anthropic Family",
"description": "Routes across the Claude model family: Haiku for simple queries, Sonnet for medium, Opus for complex and reasoning-heavy requests.",
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 5fc39bbc45a..30153b6f87a 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
@@ -246,7 +246,26 @@ describe("AddAutoRouterTab", () => {
await user.type(screen.getByPlaceholderText(/smart_router/i), "raced-router");
await user.click(screen.getByRole("button", { name: /add auto router/i }));
- // No preset was applied, so the tiers are empty and tier validation blocks the submit.
+ // No preset was applied and none was picked, so the template check blocks the submit.
+ expect(mockHandleAddAutoRouterSubmit).not.toHaveBeenCalled();
+ });
+
+ // The Template field carries a required marker but, until now, nothing actually validated it:
+ // submit fell through to the unrelated missing-tiers error instead. This pins a Template-specific
+ // block so a future regression (e.g. dropping this check) surfaces as a wrong error message, not
+ // silence.
+ it("blocks the submit and shows an inline error when no template is chosen", async () => {
+ const user = userEvent.setup();
+ mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS);
+
+ renderWithProviders();
+ await user.type(screen.getByPlaceholderText(/smart_router/i), "no-template-router");
+ await user.click(screen.getByRole("button", { name: /add auto router/i }));
+
+ expect(await screen.findByText("Please select a template")).toBeInTheDocument();
+ expect(NotificationManager.fromBackend).toHaveBeenCalledWith(
+ "Please select a template, or choose Custom Configuration",
+ );
expect(mockHandleAddAutoRouterSubmit).not.toHaveBeenCalled();
});
@@ -319,6 +338,8 @@ describe("AddAutoRouterTab", () => {
,
);
+ openTemplateDropdown();
+ fireEvent.click(optionByLabel("Custom Configuration")!);
await user.type(screen.getByPlaceholderText(/smart_router/i), "team-scoped-router");
await user.selectOptions(screen.getByTestId("team-dropdown"), "team-1");
await user.click(screen.getByRole("button", { name: /add auto router/i }));
@@ -335,6 +356,8 @@ describe("AddAutoRouterTab", () => {
,
);
+ openTemplateDropdown();
+ fireEvent.click(optionByLabel("Custom Configuration")!);
await user.type(screen.getByPlaceholderText(/smart_router/i), "team-scoped-router");
await user.click(screen.getByRole("button", { name: /add auto router/i }));
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 9abc1c17ca1..f73a06860c3 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
@@ -122,7 +122,7 @@ const AddAutoRouterTab: React.FC = ({
label: model_group,
}));
- const availableModelSet = new Set(modelInfo.map((m) => m.model_group));
+ const availableModelSet = React.useMemo(() => new Set(modelInfo.map((m) => m.model_group)), [modelInfo]);
const presets = React.useMemo(() => getAllPresets(), []);
// A preset's models can only be trusted against a successfully loaded list. Selection and the
@@ -182,6 +182,12 @@ const AddAutoRouterTab: React.FC = ({
};
const submitRecommendedRouter = (name: string) => {
+ if (!selectedPreset) {
+ setShowValidationErrors(true);
+ NotificationManager.fromBackend("Please select a template, or choose Custom Configuration");
+ return;
+ }
+
const {
tiers,
classifier_type: classifierType,
@@ -348,6 +354,9 @@ const AddAutoRouterTab: React.FC = ({
);
})}
+ {showValidationErrors && !selectedPreset && (
+ Please select a template
+ )}
{
- it("loads exactly the two model-family presets (sample_spec excluded)", () => {
+ it("loads exactly the two model-family presets", () => {
const presets = getAllPresets();
expect(presets.map((p) => p.label).sort()).toEqual(["Anthropic Family", "OpenAI Family"]);
// Every preset carries all four fields the UI relies on; a JSON typo dropping one fails here.
@@ -20,8 +20,6 @@ describe("autorouter_presets", () => {
it("resolves a preset by its stable JSON key, not its display label", () => {
expect(getPresetByKey("anthropic_family")?.label).toBe("Anthropic Family");
expect(getPresetByKey("does_not_exist")).toBeUndefined();
- // sample_spec is filtered out, so it is not resolvable by key either.
- expect(getPresetByKey("sample_spec")).toBeUndefined();
});
it("keeps every preset a plain heuristic complexity router (no adaptive/quality settings)", () => {
diff --git a/ui/litellm-dashboard/src/lib/autorouter_presets.ts b/ui/litellm-dashboard/src/lib/autorouter_presets.ts
index 46aec8685df..4183f90bbd4 100644
--- a/ui/litellm-dashboard/src/lib/autorouter_presets.ts
+++ b/ui/litellm-dashboard/src/lib/autorouter_presets.ts
@@ -13,12 +13,10 @@ export interface AutoRouterPreset {
// The bundled JSON is a developer-authored, build-time asset, so it is trusted at the import
// boundary rather than re-validated at runtime (resolveJsonModule widens its string literals,
// hence this one cast). autorouter_presets.test.ts pins the parsed shape, so a JSON typo fails
-// CI. "sample_spec" documents the shape and is not a selectable preset.
+// CI.
const RAW = presetsRaw as Record>;
-const PRESETS: AutoRouterPreset[] = Object.entries(RAW)
- .filter(([key]) => key !== "sample_spec")
- .map(([key, preset]) => ({ key, ...preset }));
+const PRESETS: AutoRouterPreset[] = Object.entries(RAW).map(([key, preset]) => ({ key, ...preset }));
export const getAllPresets = (): AutoRouterPreset[] => PRESETS;