From 8b89dcf9c097e2d024db118d964a38c43008b2ed Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Fri, 31 Jul 2026 00:10:19 -0700 Subject: [PATCH] perf(ui): memoize getAllPresets to prevent re-parsing on every render --- .../add_model/add_auto_router_tab.test.tsx | 110 ++++++++++++++++++ .../add_model/add_auto_router_tab.tsx | 54 +++++---- 2 files changed, 143 insertions(+), 21 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 c05f533297f..751baaec4ee 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 @@ -62,6 +62,37 @@ vi.mock("./build_complexity_router_config", async (importOriginal) => { return { ...actual, getMissingTiersError: vi.fn(actual.getMissingTiersError) }; }); +// Real bundled presets carry no deliberately-falsy fields, so a synthetic preset is appended to +// prove prefill preserves a 0 match threshold and an empty escalation list (a preset switching +// escalation off) instead of overwriting them with the create-form defaults. Defined via +// vi.hoisted so it exists when the hoisted vi.mock factory below references it. +const { FALSY_PRESET } = vi.hoisted(() => ({ + FALSY_PRESET: { + key: "falsy_family", + label: "Falsy Family", + description: "Preset that deliberately disables escalation and pins a zero match threshold", + complexity_router_config: { + tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: ["gpt-5-mini"], COMPLEX: ["gpt-5"], REASONING: ["o3"] }, + classifier_type: "heuristic" as const, + semantic_keyword_matching: true, + embedding_model: "gpt-5-nano", + match_threshold: 0, + keyword_tier_rules: [{ keywords: ["foo"], tier: "SIMPLE" as const }], + escalation_keywords: [], + }, + }, +})); + +vi.mock("@/lib/autorouter_presets", async (importOriginal) => { + const actual = await importOriginal(); + const withSynthetic = [...actual.getAllPresets(), FALSY_PRESET]; + return { + ...actual, + getAllPresets: () => withSynthetic, + getPresetByKey: (key: string) => withSynthetic.find((preset) => preset.key === key), + }; +}); + // A real TeamDropdown fetches teams and renders an antd Select; the wiring under test is // whether team_id is registered, validated and forwarded, so a plain control stands in. vi.mock("../common_components/team_dropdown", () => ({ @@ -213,6 +244,85 @@ describe("AddAutoRouterTab", () => { expect(mockHandleAddAutoRouterSubmit).not.toHaveBeenCalled(); }); + // A caller switch must reset the form and re-enter the loading gate until the NEW caller's models + // arrive, or the previous caller's verified list keeps a preset selectable and submittable for a + // caller who may lack those models. Caller A selects OpenAI (config filled); caller B's fetch is + // held open, so during that window the option must be disabled again (loading gate) AND the filled + // config must be gone, so B cannot submit A's preset. Dropping the reset re-enables the option and + // carries A's tiers into B's submit. + it("resets the form and re-enters the loading gate while the new caller's models are fetching", async () => { + const user = userEvent.setup(); + let resolveB: (models: ModelGroup[]) => void = () => undefined; + mockFetchAvailableModels + .mockResolvedValueOnce(ALL_FAMILY_MODELS) + .mockReturnValueOnce(new Promise((resolve) => (resolveB = resolve))); + + 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)); + + openTemplateDropdown(); + await waitFor(() => expect(isOptionDisabled(optionByLabel("OpenAI Family")!)).toBe(true)); + expect(optionByLabel("OpenAI Family")).toHaveTextContent(/Checking model availability/); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "caller-b-router"); + await user.click(screen.getByRole("button", { name: /add auto router/i })); + expect(mockHandleAddAutoRouterSubmit).not.toHaveBeenCalled(); + + resolveB([]); + }); + + // A late-resolving fetch from the previous caller must not overwrite the current caller's list. + // Caller A's request is held open, caller B's resolves empty; when A finally resolves with the + // full family, the ignore guard drops it so the preset stays greyed out for B. Without the guard, + // A's response would land after B's and wrongly re-enable the preset. + it("ignores a stale in-flight model fetch that resolves after the token changed", async () => { + let resolveA: (models: ModelGroup[]) => void = () => undefined; + mockFetchAvailableModels + .mockReturnValueOnce(new Promise((resolve) => (resolveA = resolve))) + .mockResolvedValueOnce([]); + + const { rerender } = renderWithProviders( + , + ); + rerender(); + await waitFor(() => expect(mockFetchAvailableModels).toHaveBeenCalledTimes(2)); + + resolveA(ALL_FAMILY_MODELS); + + openTemplateDropdown(); + await waitFor(() => expect(optionByLabel("OpenAI Family")).toBeTruthy()); + expect(isOptionDisabled(optionByLabel("OpenAI Family")!)).toBe(true); + }); + + // 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. + it("preserves a preset's zero match threshold and empty escalation list through submit", async () => { + const user = userEvent.setup(); + mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS); + + renderWithProviders(); + openTemplateDropdown(); + + await waitFor(() => expect(isOptionDisabled(optionByLabel("Falsy Family")!)).toBe(false)); + fireEvent.click(optionByLabel("Falsy Family")!); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "falsy-router"); + await user.click(screen.getByRole("button", { name: /add auto router/i })); + + await waitFor(() => expect(mockHandleAddAutoRouterSubmit).toHaveBeenCalled()); + const payload = mockHandleAddAutoRouterSubmit.mock.calls.at(-1)?.[0]; + expect(payload.complexity_router_config.match_threshold).toBe(0); + expect(payload.complexity_router_config.escalation_keywords).toEqual([]); + }); + it("offers no team selector to a proxy admin, who may create an unscoped router", () => { renderWithProviders(); 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 5c31b229fc9..90edf920a63 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 @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { Card, Form, Button, Tooltip, Typography, Select as AntdSelect, Modal } from "antd"; import { TextInput } from "@tremor/react"; import { modelAvailableCall } from "../networking"; @@ -106,19 +106,44 @@ const AddAutoRouterTab: React.FC = ({ fetchModelAccessGroups(); }, [accessToken]); + const resetToCustom = useCallback(() => { + setComplexityRouterConfig({ + tiers: { SIMPLE: [], MEDIUM: [], COMPLEX: [], REASONING: [] }, + classifier_type: "heuristic", + }); + setCustomTechnicalKeywords([]); + setKeywordTierRules([]); + setSemanticMatchingEnabled(false); + setEmbeddingModel(undefined); + setMatchThreshold(DEFAULT_MATCH_THRESHOLD); + setEscalationKeywords(DEFAULT_ESCALATION_KEYWORDS); + }, []); + useEffect(() => { + let ignore = false; + const loadModels = async () => { + setModelsLoadState("loading"); + setModelInfo([]); + setSelectedPreset(undefined); + resetToCustom(); try { const uniqueModels = await fetchAvailableModels(accessToken); + if (ignore) return; setModelInfo(uniqueModels); setModelsLoadState("loaded"); } catch (error) { console.error("Error fetching model info for auto router:", error); + if (ignore) return; setModelsLoadState("error"); } }; loadModels(); - }, [accessToken]); + + return () => { + ignore = true; + }; + }, [accessToken, resetToCustom]); const isAdmin = all_admin_roles.includes(userRole); @@ -128,7 +153,7 @@ const AddAutoRouterTab: React.FC = ({ })); const availableModelSet = new Set(modelInfo.map((m) => m.model_group)); - const presets = getAllPresets(); + const presets = React.useMemo(() => getAllPresets(), []); // A preset's models can only be trusted against a successfully loaded list. Selection and the // greyed-out state derive from this one function, so a preset that cannot be selected can never @@ -142,19 +167,6 @@ const AddAutoRouterTab: React.FC = ({ return missing.length > 0 ? { kind: "missing_models", models: missing } : { kind: "available" }; }; - const resetToCustom = () => { - setComplexityRouterConfig({ - tiers: { SIMPLE: [], MEDIUM: [], COMPLEX: [], REASONING: [] }, - classifier_type: "heuristic", - }); - setCustomTechnicalKeywords([]); - setKeywordTierRules([]); - setSemanticMatchingEnabled(false); - setEmbeddingModel(undefined); - setMatchThreshold(DEFAULT_MATCH_THRESHOLD); - setEscalationKeywords(DEFAULT_ESCALATION_KEYWORDS); - }; - const handlePresetChange = (presetKey: string | undefined) => { if (!presetKey || presetKey === "custom") { setSelectedPreset(presetKey); @@ -182,12 +194,12 @@ const AddAutoRouterTab: React.FC = ({ }; setComplexityRouterConfig(presetComplexityRouterConfig); - setCustomTechnicalKeywords(config.custom_technical_keywords || []); - setKeywordTierRules(hydrateKeywordTierRules(config.keyword_tier_rules || [])); - setSemanticMatchingEnabled(config.semantic_keyword_matching || false); + setCustomTechnicalKeywords(config.custom_technical_keywords ?? []); + setKeywordTierRules(hydrateKeywordTierRules(config.keyword_tier_rules ?? [])); + setSemanticMatchingEnabled(config.semantic_keyword_matching ?? false); setEmbeddingModel(config.embedding_model); - setMatchThreshold(config.match_threshold || DEFAULT_MATCH_THRESHOLD); - setEscalationKeywords(config.escalation_keywords || DEFAULT_ESCALATION_KEYWORDS); + setMatchThreshold(config.match_threshold ?? DEFAULT_MATCH_THRESHOLD); + setEscalationKeywords(config.escalation_keywords ?? DEFAULT_ESCALATION_KEYWORDS); }; const submitRecommendedRouter = (name: string) => {