Merge pull request #21144 from BerriAI/litellm_ui_fallbacks_10

[Refactor] UI - Fallbacks: Default Configurable to 10 Models
This commit is contained in:
yuneng-jiang 2026-02-13 12:30:35 -08:00 committed by GitHub
commit eb144a44d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 240 additions and 8 deletions

View file

@ -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 */}

View file

@ -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<typeof import("antd")>();
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(
<FallbackSelectionForm
groups={[]}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
/>,
);
expect(screen.getByText(/no fallback groups configured/i)).toBeInTheDocument();
});
it("should show Create First Group button when no groups exist", () => {
render(
<FallbackSelectionForm
groups={[]}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
/>,
);
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(
<FallbackSelectionForm
groups={[]}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
/>,
);
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(
<FallbackSelectionForm
groups={groups}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
/>,
);
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(
<FallbackSelectionForm
groups={groups}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
/>,
);
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(
<FallbackSelectionForm
groups={groups}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
/>,
);
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(
<FallbackSelectionForm
groups={groups}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
maxGroups={5}
/>,
);
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(
<FallbackSelectionForm
groups={groups}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
maxGroups={3}
/>,
);
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(
<FallbackSelectionForm
groups={groups}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
/>,
);
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(
<FallbackSelectionForm
groups={groups}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
/>,
);
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(
<FallbackSelectionForm
groups={groups}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
/>,
);
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(
<FallbackSelectionForm
groups={groups}
onGroupsChange={mockOnGroupsChange}
availableModels={AVAILABLE_MODELS}
maxGroups={5}
/>,
);
expect(screen.queryByRole("button", { name: /add tab/i })).not.toBeInTheDocument();
expect(mockOnGroupsChange).not.toHaveBeenCalled();
});
});

View file

@ -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");

View file

@ -84,12 +84,12 @@ const RouterSettingsAccordion = forwardRef<RouterSettingsAccordionRef, RouterSet
// Initialize from value prop if provided (only when value actually changes externally)
useEffect(() => {
// 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<RouterSettingsAccordionRef, RouterSet
groups={fallbackGroups}
onGroupsChange={handleFallbackGroupsChange}
availableModels={availableModels}
maxFallbacks={5}
maxGroups={5}
/>
</TabPanel>