diff --git a/ui/litellm-dashboard/src/app/(dashboard)/fallbacks/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/fallbacks/page.tsx index 6ec2a98aa79..b93091d1986 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/fallbacks/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/fallbacks/page.tsx @@ -6,12 +6,10 @@ import Fallbacks from "@/components/Settings/RouterSettings/Fallbacks/Fallbacks" export default function FallbacksPage() { const { accessToken, userRole, userId } = useAuthorized(); return ( -
+

Fallbacks

-

- Automatically retry a different model when the primary fails -

+

Automatically retry a different model when the primary fails

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 index 3745616c6dc..49ee62c50d9 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.test.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.test.tsx @@ -53,7 +53,11 @@ describe("FallbackSelectionForm", () => { 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(); }); @@ -61,16 +65,36 @@ describe("FallbackSelectionForm", () => { 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 display friendly label for provider wildcard primary", () => { + const groups: FallbackGroup[] = [{ id: "1", primaryModel: "openai/*", fallbackModels: [] }]; + render( + , + ); + expect(screen.getByRole("tab", { name: "All OpenAI Models" })).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 fallback group/i }); @@ -125,7 +149,11 @@ describe("FallbackSelectionForm", () => { { id: "2", primaryModel: "gpt-3.5-turbo", fallbackModels: [] }, ]; render( - , + , ); const removeButtons = screen.getAllByRole("button", { name: /^remove /i }); @@ -142,7 +170,11 @@ describe("FallbackSelectionForm", () => { it("should render FallbackGroupConfig for each group", () => { const groups: FallbackGroup[] = [{ id: "1", primaryModel: null, fallbackModels: [] }]; render( - , + , ); expect(screen.getByRole("combobox", { name: /primary model/i })).toHaveValue(""); expect(screen.getByText("Primary Model")).toBeInTheDocument(); @@ -151,7 +183,11 @@ describe("FallbackSelectionForm", () => { 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); 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 fdeb886ef04..17036c6c770 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/FallbackSelectionForm.tsx @@ -10,7 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Plus, X } from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import { toast } from "@/lib/toast"; -import { fallbackChainOptions, primaryModelOptions } from "./fallbackOptions"; +import { fallbackChainOptions, fallbackPrimaryLabel, primaryModelOptions } from "./fallbackOptions"; import { FallbackGroup, FallbackGroupConfig } from "./FallbackGroupConfig"; interface FallbackSelectionFormProps { @@ -81,7 +81,7 @@ export function FallbackSelectionForm({ }; const groupLabel = (group: FallbackGroup, index: number) => - group.primaryModel ? group.primaryModel : `Group ${index + 1}`; + group.primaryModel ? fallbackPrimaryLabel(group.primaryModel) : `Group ${index + 1}`; if (groups.length === 0) { return ( diff --git a/ui/litellm-dashboard/src/components/liteadmin/operations.ts b/ui/litellm-dashboard/src/components/liteadmin/operations.ts index dcc57dc72b1..fbb9571c696 100644 --- a/ui/litellm-dashboard/src/components/liteadmin/operations.ts +++ b/ui/litellm-dashboard/src/components/liteadmin/operations.ts @@ -109,7 +109,11 @@ const fallbackSetFields = { model: text.describe( "The primary model group, or a provider wildcard like openai/* meaning all models from that provider.", ), - fallbacks: z.array(fallbackTarget).min(1).max(10), + fallbacks: z + .array(fallbackTarget) + .min(1) + .max(10) + .describe("Concrete model groups to try in order. Never a wildcard."), }; const fallbackDeleteFields = { model: text }; @@ -359,15 +363,12 @@ export function createLiteAdminOperations(context: OperationContext) { query: { ...a, start_date: `${a.start_date} 00:00:00`, end_date: `${a.end_date} 23:59:59` }, }), ), - operation("read", "fallback")( - "fallbacks_list", - "List configured router fallbacks", - object({}), - async () => ({ fallbacks: fallbackEntries(await readRouterSettings()) }), - ), + operation("read", "fallback")("fallbacks_list", "List configured router fallbacks", object({}), async () => ({ + fallbacks: fallbackEntries(await readRouterSettings()), + })), operation("write", "fallback")( "fallback_set", - "Set the fallback chain for a primary model. The model may be a provider wildcard like openai/* meaning all models from that provider; wildcards are allowed as model but never inside fallbacks", + "Set the fallback chain for a model", object(fallbackSetFields), async (a) => { const settings = await readRouterSettings();