From 25fccad12726157e348f00042835f70f753c5540 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Fri, 13 Feb 2026 11:50:13 -0800 Subject: [PATCH] Change default fallbacks to 10 models --- .../RouterSettings/Fallbacks/AddFallbacks.tsx | 2 +- .../Fallbacks/FallbackSelectionForm.test.tsx | 233 ++++++++++++++++++ .../Fallbacks/FallbackSelectionForm.tsx | 2 +- .../RouterSettingsAccordion.tsx | 11 +- 4 files changed, 240 insertions(+), 8 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.test.tsx diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.tsx index c0b626cbb12..34f059516a6 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.tsx @@ -140,7 +140,7 @@ export default function AddFallbacks({ groups={groups} onGroupsChange={setGroups} availableModels={availableModels} - maxFallbacks={5} + maxFallbacks={10} maxGroups={5} /> {/* Footer with Cancel and Save buttons */} diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.test.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.test.tsx new file mode 100644 index 00000000000..d9d126d9fb4 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.test.tsx @@ -0,0 +1,233 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { FallbackSelectionForm } from "./FallbackSelectionForm"; +import type { FallbackGroup } from "./FallbackGroupConfig"; + +const mockOnGroupsChange = vi.fn(); +const AVAILABLE_MODELS = ["gpt-4", "gpt-3.5-turbo", "claude-3-opus"]; + +vi.mock("antd", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + message: { + ...actual.message, + warning: vi.fn(), + }, + }; +}); + +describe("FallbackSelectionForm", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(Date, "now").mockReturnValue(1234567890); + }); + + it("should render the component", () => { + render( + , + ); + expect(screen.getByText(/no fallback groups configured/i)).toBeInTheDocument(); + }); + + it("should show Create First Group button when no groups exist", () => { + render( + , + ); + expect(screen.getByRole("button", { name: /create first group/i })).toBeInTheDocument(); + }); + + it("should call onGroupsChange when Create First Group is clicked", async () => { + const user = userEvent.setup(); + render( + , + ); + + await user.click(screen.getByRole("button", { name: /create first group/i })); + + expect(mockOnGroupsChange).toHaveBeenCalledTimes(1); + const [newGroups] = mockOnGroupsChange.mock.calls[0]; + expect(newGroups).toHaveLength(1); + expect(newGroups[0]).toEqual({ + id: "1234567890", + primaryModel: null, + fallbackModels: [], + }); + }); + + it("should display tabs when groups exist", () => { + const groups: FallbackGroup[] = [ + { id: "1", primaryModel: null, fallbackModels: [] }, + ]; + render( + , + ); + expect(screen.getByRole("tab", { name: /group 1/i })).toBeInTheDocument(); + }); + + it("should display primary model as tab label when set", () => { + const groups: FallbackGroup[] = [ + { id: "1", primaryModel: "gpt-4", fallbackModels: [] }, + ]; + render( + , + ); + expect(screen.getByRole("tab", { name: "gpt-4" })).toBeInTheDocument(); + }); + + it("should call onGroupsChange when add tab button is clicked", async () => { + const user = userEvent.setup(); + const groups: FallbackGroup[] = [ + { id: "1", primaryModel: null, fallbackModels: [] }, + ]; + render( + , + ); + + const addTabButton = screen.getByRole("button", { name: /add tab/i }); + await user.click(addTabButton); + + expect(mockOnGroupsChange).toHaveBeenCalledTimes(1); + const [newGroups] = mockOnGroupsChange.mock.calls[0]; + expect(newGroups).toHaveLength(2); + expect(newGroups[1]).toEqual({ + id: "1234567890", + primaryModel: null, + fallbackModels: [], + }); + }); + + it("should not show add tab button when maxGroups is reached", () => { + const groups: FallbackGroup[] = [ + { id: "1", primaryModel: null, fallbackModels: [] }, + { id: "2", primaryModel: null, fallbackModels: [] }, + { id: "3", primaryModel: null, fallbackModels: [] }, + { id: "4", primaryModel: null, fallbackModels: [] }, + { id: "5", primaryModel: null, fallbackModels: [] }, + ]; + render( + , + ); + expect(screen.queryByRole("button", { name: /add tab/i })).not.toBeInTheDocument(); + }); + + it("should show add tab button when below maxGroups with custom maxGroups", () => { + const groups: FallbackGroup[] = [ + { id: "1", primaryModel: null, fallbackModels: [] }, + ]; + render( + , + ); + expect(screen.getByRole("button", { name: /add tab/i })).toBeInTheDocument(); + }); + + it("should call onGroupsChange when a group is removed", async () => { + const user = userEvent.setup(); + const antd = await import("antd"); + const groups: FallbackGroup[] = [ + { id: "1", primaryModel: "gpt-4", fallbackModels: [] }, + { id: "2", primaryModel: "gpt-3.5-turbo", fallbackModels: [] }, + ]; + render( + , + ); + + const removeButtons = screen.getAllByRole("tab", { name: "remove" }); + await user.click(removeButtons[0]); + + expect(mockOnGroupsChange).toHaveBeenCalledTimes(1); + const [newGroups] = mockOnGroupsChange.mock.calls[0]; + expect(newGroups).toHaveLength(1); + expect(newGroups[0].id).toBe("2"); + expect(antd.message.warning).not.toHaveBeenCalled(); + }); + + it("should render FallbackGroupConfig for each group", () => { + const groups: FallbackGroup[] = [ + { id: "1", primaryModel: null, fallbackModels: [] }, + ]; + render( + , + ); + expect(screen.getByText("Select primary model")).toBeInTheDocument(); + expect(screen.getByText("Primary Model")).toBeInTheDocument(); + }); + + it("should display group with primary and fallback models in FallbackGroupConfig", () => { + const groups: FallbackGroup[] = [ + { id: "1", primaryModel: "gpt-4", fallbackModels: ["gpt-3.5-turbo"] }, + ]; + render( + , + ); + expect(screen.getByRole("tab", { name: "gpt-4" })).toBeInTheDocument(); + expect(screen.getAllByText("gpt-4").length).toBeGreaterThan(0); + expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument(); + }); + + it("should not add group when add button clicked at maxGroups", () => { + const groups: FallbackGroup[] = Array.from({ length: 5 }, (_, i) => ({ + id: String(i + 1), + primaryModel: null, + fallbackModels: [] as string[], + })); + render( + , + ); + + expect(screen.queryByRole("button", { name: /add tab/i })).not.toBeInTheDocument(); + expect(mockOnGroupsChange).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.tsx index bb9ccb312a6..08b031c6835 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.tsx @@ -22,7 +22,7 @@ export function FallbackSelectionForm({ groups, onGroupsChange, availableModels, - maxFallbacks = 5, + maxFallbacks = 10, maxGroups = 5, }: FallbackSelectionFormProps) { const [activeKey, setActiveKey] = useState(groups.length > 0 ? groups[0].id : "1"); diff --git a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx index 1f691715d30..ae4b87a3587 100644 --- a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx +++ b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx @@ -84,12 +84,12 @@ const RouterSettingsAccordion = forwardRef { // Create a stable key from the value to detect actual external changes - const valueKey = value?.router_settings + const valueKey = value?.router_settings ? JSON.stringify({ - routing_strategy: value.router_settings.routing_strategy, - fallbacks: value.router_settings.fallbacks, - enable_tag_filtering: value.router_settings.enable_tag_filtering, - }) + routing_strategy: value.router_settings.routing_strategy, + fallbacks: value.router_settings.fallbacks, + enable_tag_filtering: value.router_settings.enable_tag_filtering, + }) : null; // Skip if this is an internal update (from our own onChange) and the value hasn't actually changed @@ -358,7 +358,6 @@ const RouterSettingsAccordion = forwardRef