From b4af02063248c1a06481e37a936174a26271a112 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Fri, 28 Aug 2026 09:54:22 -0700 Subject: [PATCH] fix(ui): name the orphaned keyword rules inside the tier editor before Done --- .../add_model/ComplexityRouterConfig.test.tsx | 19 +++++++++++++++++++ .../add_model/ComplexityRouterConfig.tsx | 12 +++++++++++- .../add_model/add_auto_router_tab.test.tsx | 19 +++++++++++++++++++ .../add_model/add_auto_router_tab.tsx | 4 ++++ .../edit_auto_router_modal.tsx | 4 ++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx index e30604419fe..91201b51663 100644 --- a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx @@ -1094,6 +1094,25 @@ describe("ComplexityRouterConfig tier editing", () => { expect(screen.queryByRole("button", { name: "Edit tiers" })).not.toBeInTheDocument(); }); + it("surfaces the caller's orphaned-rule verdict while editing, so Done is not a silent exit", () => { + renderEditor(customValue, { keywordRulesError: "Keyword rule(s) 1 route to a tier this router no longer has" }); + expect( + screen.getByText("Keyword rule(s) 1 route to a tier this router no longer has", { exact: false }), + ).toBeInTheDocument(); + }); + + it("keeps the orphaned-rule verdict out of the collapsed view, where the submit tooltip owns it", () => { + renderWithProviders( + , + ); + expect(screen.queryByText("route to a tier this router no longer has", { exact: false })).not.toBeInTheDocument(); + }); + it("renders the four built-in tiers before any edit, unchanged", () => { renderWithProviders(); expect(screen.getByRole("button", { name: "Edit tiers" })).toBeInTheDocument(); diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx index 8dc4898f1ed..153afa0b586 100644 --- a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx @@ -197,10 +197,11 @@ const TierSetToolbar: React.FC<{ isCustomSet: boolean; rowCount: number; rowsError: string | null; + keywordRulesError: string | null | undefined; onEditingChange: ((editing: boolean) => void) | undefined; onAdd: () => void; onRestore: () => void; -}> = ({ editing, isCustomSet, rowCount, rowsError, onEditingChange, onAdd, onRestore }) => ( +}> = ({ editing, isCustomSet, rowCount, rowsError, keywordRulesError, onEditingChange, onAdd, onRestore }) => ( <>
{editing ? ( @@ -234,6 +235,11 @@ const TierSetToolbar: React.FC<{ and an edited set requires the LLM classification method )} + {editing && keywordRulesError && ( + + {keywordRulesError}. Edit the rules under Advanced: Keyword/Semantic Matching, or bring the tier back + + )} ); @@ -419,6 +425,8 @@ interface ComplexityRouterConfigProps { // rules or semantic matching, so it renders this component without them. keywordTierRules?: KeywordTierRule[]; onKeywordTierRulesChange?: (rules: KeywordTierRule[]) => void; + /** getKeywordTierRulesError's verdict, owned by the caller: importing it here would be an import cycle. */ + keywordRulesError?: string | null; semanticMatchingEnabled?: boolean; onSemanticMatchingEnabledChange?: (enabled: boolean) => void; embeddingModel?: string; @@ -569,6 +577,7 @@ const ComplexityRouterConfig: React.FC = ({ onCustomTechnicalKeywordsChange, keywordTierRules = [], onKeywordTierRulesChange, + keywordRulesError, semanticMatchingEnabled = false, onSemanticMatchingEnabledChange, embeddingModel, @@ -739,6 +748,7 @@ const ComplexityRouterConfig: React.FC = ({ isCustomSet={Boolean(customTierSet)} rowCount={tierRows.length} rowsError={tierRowsError} + keywordRulesError={keywordRulesError} onEditingChange={onEditingTiersChange} onAdd={addCustomTier} onRestore={exitToBuiltInTiers} 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 79fa2dab4f8..01cb41bcb95 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 @@ -283,6 +283,25 @@ describe("AddAutoRouterTab", () => { expect(screen.queryByText("At least one keyword is required")).not.toBeInTheDocument(); }); + it("shows the orphaned-rule reason in the tier editor when a rule's tier is removed", async () => { + const user = userEvent.setup(); + vi.mocked(getMissingTiersError).mockReturnValue(null); + + renderWithProviders(); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "orphan-rule-router"); + expandDetailedConfiguration(); + await user.click(screen.getByText("Advanced: Keyword/Semantic Matching")); + await user.click(screen.getByRole("button", { name: /add keyword rule/i })); + await addKeyword(user, screen.getByText("Keywords 1").closest("div") as HTMLElement, "invoice"); + + await user.click(screen.getByRole("button", { name: "Edit tiers" })); + await user.click(screen.getByRole("button", { name: "Remove the COMPLEX tier" })); + + expect(await screen.findByText(/route to a tier this router no longer has/)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /add auto router/i })).toBeDisabled(); + }); + it("marks only the offending keyword row, leaving a filled one alone", async () => { const user = userEvent.setup(); vi.mocked(getMissingTiersError).mockReturnValue(null); 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 0249b21762a..4e5e5e8d460 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 @@ -581,6 +581,10 @@ const AddAutoRouterTab: React.FC = ({ onCustomTechnicalKeywordsChange={setCustomTechnicalKeywords} keywordTierRules={keywordTierRules} onKeywordTierRulesChange={setKeywordTierRules} + keywordRulesError={getKeywordTierRulesError( + keywordTierRules, + activeTierRows(complexityRouterConfig), + )} semanticMatchingEnabled={semanticMatchingEnabled} onSemanticMatchingEnabledChange={setSemanticMatchingEnabled} embeddingModel={embeddingModel} diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx index 184a4795b12..425d5d51f06 100644 --- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx +++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx @@ -687,6 +687,10 @@ const EditAutoRouterModal: React.FC = ({ onCustomTechnicalKeywordsChange={setCustomTechnicalKeywords} keywordTierRules={keywordTierRules} onKeywordTierRulesChange={setKeywordTierRules} + keywordRulesError={getKeywordTierRulesError( + keywordTierRules, + activeTierRows(complexityRouterConfig), + )} semanticMatchingEnabled={semanticMatchingEnabled} onSemanticMatchingEnabledChange={setSemanticMatchingEnabled} embeddingModel={embeddingModel}