diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 5e322598a10..927d0d2b08f 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1405,9 +1405,6 @@ "no-nested-ternary": { "count": 1 }, - "no-restricted-imports": { - "count": 2 - }, "prefer-const": { "count": 2 } @@ -1761,11 +1758,6 @@ "count": 1 } }, - "src/components/BetaBadge.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/CloudZeroCostTracking/CloudZeroCreateModal.tsx": { "no-restricted-imports": { "count": 1 @@ -1789,11 +1781,6 @@ "count": 1 } }, - "src/components/DeprecationBanner.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/EntityUsageExport/ExportSummary.tsx": { "no-restricted-imports": { "count": 1 @@ -1995,11 +1982,6 @@ "count": 1 } }, - "src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx": { "local/no-complex-jsx-arrow": { "count": 1 @@ -2017,9 +1999,6 @@ } }, "src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx": { - "no-restricted-imports": { - "count": 2 - }, "prefer-const": { "count": 2 } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.test.tsx index 9ffcbfc9975..0f3ba6c3471 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.test.tsx @@ -62,6 +62,8 @@ const settingsRow = async (fieldName: string) => { return row as HTMLElement; }; +const numericValueIn = (row: HTMLElement) => Number((within(row).getByRole("spinbutton") as HTMLInputElement).value); + describe("GeneralSettings General tab", () => { beforeEach(() => { vi.mocked(getGeneralSettingsCall).mockResolvedValue([...SETTINGS_FIXTURE.map((s) => ({ ...s }))]); @@ -87,7 +89,7 @@ describe("GeneralSettings General tab", () => { await user.click(screen.getByText("General")); const row = await settingsRow("max_ui_session_budget"); - expect(within(row).getByRole("spinbutton")).toHaveValue("7.50"); + expect(numericValueIn(row)).toBe(7.5); const actionCell = row.querySelectorAll("td")[3]; const resetIcon = actionCell.querySelector("svg"); @@ -95,7 +97,7 @@ describe("GeneralSettings General tab", () => { await user.click(resetIcon as unknown as Element); expect(deleteConfigFieldSetting).toHaveBeenCalledWith("token", "max_ui_session_budget"); - expect(within(row).getByRole("spinbutton")).toHaveValue("1.00"); + expect(numericValueIn(row)).toBe(1); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx index ed7b17067d5..2a5b94b2fd7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx @@ -1,22 +1,14 @@ import React, { useState, useEffect } from "react"; -import { - Card, - Table, - TableHead, - TableRow, - TableHeaderCell, - TableCell, - TableBody, - Title, - Text, - Button, - Icon, - Switch, -} from "@tremor/react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "@/components/networking"; -import { InputNumber, Select as AntdSelect } from "antd"; -import { TrashIcon } from "@heroicons/react/outline"; +import { Trash2 } from "lucide-react"; import { StatusBadge } from "@/components/shared/table_cells"; import RouterSettings from "@/components/router_settings"; @@ -44,16 +36,22 @@ export interface generalSettingsItem { field_default_value?: any; } +const NUMERIC_INPUT_WIDTH = "w-36"; + +const toNumericValue = (raw: string): number | null => (raw === "" ? null : Number(raw)); + const SettingValueEditor: React.FC<{ setting: generalSettingsItem; onChange: (fieldName: string, newValue: any) => void; }> = ({ setting, onChange }) => { if (setting.field_type === "Integer") { return ( - onChange(setting.field_name, newValue)} + className={NUMERIC_INPUT_WIDTH} + value={setting.field_value ?? ""} + onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))} /> ); } @@ -61,42 +59,55 @@ const SettingValueEditor: React.FC<{ return ( onChange(setting.field_name, checked)} + onCheckedChange={(checked) => onChange(setting.field_name, checked)} /> ); } if (setting.field_type === "Float") { return ( - onChange(setting.field_name, newValue)} + className={NUMERIC_INPUT_WIDTH} + value={setting.field_value ?? ""} + onChange={(event) => onChange(setting.field_name, toNumericValue(event.target.value))} /> ); } if (setting.field_type === "Dollar") { return ( - onChange(setting.field_name, newValue)} - /> + + $ + onChange(setting.field_name, toNumericValue(event.target.value))} + /> + ); } if (setting.field_type === "Select") { return ( - ({ label: option, value: option }))} - onChange={(newValue) => onChange(setting.field_name, newValue ?? "")} - /> + ); } return null; @@ -131,33 +142,43 @@ export const PromptCachingPanel: React.FC<{ return ( - Prompt Caching + + Prompt Caching -
-
- Automatic Anthropic prompt caching -

{enableSetting.field_description}

-
- persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} /> -
- - {ttlSetting && (
-
- Cache lifetime (TTL) -

{ttlSetting.field_description}

+
+

Automatic Anthropic prompt caching

+

{enableSetting.field_description}

- ({ label: option, value: option }))} - onChange={(newValue) => persist(ANTHROPIC_PROMPT_CACHING_TTL, newValue ?? "")} - /> + persist(ENABLE_ANTHROPIC_PROMPT_CACHING, checked)} />
- )} + + {ttlSetting && ( +
+
+

Cache lifetime (TTL)

+

{ttlSetting.field_description}

+
+ +
+ )} + ); }; @@ -254,55 +275,60 @@ const GeneralSettings: React.FC = ({ accessToken, user - - - - Setting - Value - Status - Action - - - - {generalSettings - .filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB) - .map((value, index) => ( - - - {value.field_name} -

- {value.field_description} -

-
- - - - - {value.stored_in_db == true ? ( - - ) : value.stored_in_db == false ? ( - - ) : ( - - )} - - - - handleResetField(value.field_name)}> - Reset - - -
- ))} -
-
+ + + + + Setting + Value + Status + Action + + + + {generalSettings + .filter((value) => value.field_type !== "TypedDictionary" && value.field_tab !== PROMPT_CACHING_TAB) + .map((value, index) => ( + + +

{value.field_name}

+

+ {value.field_description} +

+
+ + + + + {value.stored_in_db == true ? ( + + ) : value.stored_in_db == false ? ( + + ) : ( + + )} + + + + handleResetField(value.field_name)} + className="inline-flex shrink-0 cursor-pointer items-center justify-center px-1.5 py-1.5 text-red-500" + > + + + +
+ ))} +
+
+
diff --git a/ui/litellm-dashboard/src/components/BetaBadge.tsx b/ui/litellm-dashboard/src/components/BetaBadge.tsx index 7c4ef04417e..4e2195d1c36 100644 --- a/ui/litellm-dashboard/src/components/BetaBadge.tsx +++ b/ui/litellm-dashboard/src/components/BetaBadge.tsx @@ -1,4 +1,4 @@ -import { Badge } from "antd"; +import { Badge } from "@/components/ui/badge"; import { useDisableShowNewBadge } from "@/app/(dashboard)/hooks/useDisableShowNewBadge"; export default function BetaBadge({ children, dot = false }: { children?: React.ReactNode; dot?: boolean }) { @@ -8,11 +8,14 @@ export default function BetaBadge({ children, dot = false }: { children?: React. return children ? <>{children} : null; } + const badge = dot ? : Beta; + return children ? ( - + {children} - + {badge} + ) : ( - + badge ); } diff --git a/ui/litellm-dashboard/src/components/DeprecationBanner.test.tsx b/ui/litellm-dashboard/src/components/DeprecationBanner.test.tsx new file mode 100644 index 00000000000..596ad2a1626 --- /dev/null +++ b/ui/litellm-dashboard/src/components/DeprecationBanner.test.tsx @@ -0,0 +1,44 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; +import { DeprecationBanner } from "./DeprecationBanner"; + +describe("DeprecationBanner", () => { + it("names the deprecated feature in the heading and the body", () => { + render(); + + expect(screen.getByText("Memory is on a draft deprecation list")).toBeInTheDocument(); + expect(screen.getByText(/Memory is one of several experimental features/)).toBeInTheDocument(); + }); + + it("states the target removal date and that the list is not final", () => { + render(); + + expect(screen.getByText(/as early as September 1, 2026/)).toBeInTheDocument(); + expect(screen.getByText(/This list is a draft and is not final/)).toBeInTheDocument(); + }); + + it("links to the deprecation discussion in a new tab without leaking the opener", () => { + render(); + + const link = screen.getByRole("link", { name: "deprecation discussion" }); + expect(link).toHaveAttribute("href", "https://github.com/BerriAI/litellm/discussions/32090"); + expect(link).toHaveAttribute("target", "_blank"); + expect(link).toHaveAttribute("rel", "noopener noreferrer"); + }); + + it("exposes a named close control", () => { + render(); + + expect(screen.getByRole("button", { name: /close/i })).toBeInTheDocument(); + }); + + it("hides the banner once the close control is used", async () => { + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole("button", { name: /close/i })); + + expect(screen.queryByText("Memory is on a draft deprecation list")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/DeprecationBanner.tsx b/ui/litellm-dashboard/src/components/DeprecationBanner.tsx index 33c75ec22e3..9df34636f82 100644 --- a/ui/litellm-dashboard/src/components/DeprecationBanner.tsx +++ b/ui/litellm-dashboard/src/components/DeprecationBanner.tsx @@ -1,8 +1,8 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import Link from "next/link"; -import { Alert } from "antd"; +import { Info, X } from "lucide-react"; const DEPRECATION_DISCUSSION_URL = "https://github.com/BerriAI/litellm/discussions/32090"; const DEPRECATION_TARGET_DATE = "September 1, 2026"; @@ -11,21 +11,42 @@ interface DeprecationBannerProps { featureName: string; } -export const DeprecationBanner: React.FC = ({ featureName }) => ( - - {`${featureName} is one of several experimental features we're considering removing, potentially as early as ${DEPRECATION_TARGET_DATE}. This list is a draft and is not final. If you rely on this feature, please share feedback on the `} - - deprecation discussion - - . - - } - type="info" - showIcon - closable - style={{ marginBottom: 16 }} - /> -); +export const DeprecationBanner: React.FC = ({ featureName }) => { + const [isClosed, setIsClosed] = useState(false); + + if (isClosed) { + return null; + } + + return ( +
+ +
+

{`${featureName} is on a draft deprecation list`}

+

+ {`${featureName} is one of several experimental features we're considering removing, potentially as early as ${DEPRECATION_TARGET_DATE}. This list is a draft and is not final. If you rely on this feature, please share feedback on the `} + + deprecation discussion + + . +

+
+ +
+ ); +}; diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx index 938e1104301..3efdcfd6b51 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/EditFallbacks.tsx @@ -4,9 +4,9 @@ * Reuses FallbackGroupConfig with the primary model locked */ -import { Button } from "antd"; +import { Button } from "@/components/ui/button"; import { useQuery } from "@tanstack/react-query"; -import { Pencil } from "lucide-react"; +import { LoaderCircle, Pencil } from "lucide-react"; import React, { useMemo, useState } from "react"; import { fetchAvailableModels } from "@/components/llm_calls/fetch_models"; import NotificationManager from "../../../molecules/notifications_manager"; @@ -88,16 +88,11 @@ export default function EditFallbacks({ disablePrimaryModel />
- -
diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx index 4aa9fb15705..f82780f0c73 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.tsx @@ -1,7 +1,7 @@ import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap"; -import { ArrowRightIcon, PencilAltIcon, PlayIcon, TrashIcon } from "@heroicons/react/outline"; -import { Icon, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "@tremor/react"; -import { Tooltip, Typography } from "antd"; +import { ArrowRight, Pencil, Play, Trash2 } from "lucide-react"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import openai from "openai"; import React, { useEffect, useState } from "react"; import DeleteResourceModal from "../../../common_components/DeleteResourceModal"; @@ -18,12 +18,14 @@ type Fallbacks = FallbackEntry[]; const modelCardClass = "inline-flex items-center gap-2 px-2.5 py-1 rounded-md border border-gray-200 bg-gray-50 text-sm font-medium text-gray-800 shrink-0"; +const iconWrapperClass = "inline-flex shrink-0 items-center justify-center px-1.5 py-1.5"; + function renderModelNameCell(modelName: string, getProviderFromModel?: (modelName: string) => string): React.ReactNode { const provider = getProviderFromModel?.(modelName) ?? modelName; return ( - {modelName} + {modelName} ); } @@ -41,19 +43,23 @@ function renderFallbacksChain( return ( - {modelName} + {modelName} ); }; return ( - + {list.map((model, i) => ( - {i > 0 && } + {i > 0 && ( + + + + )} ))} @@ -248,7 +254,7 @@ const Fallbacks: React.FC = ({ accessToken, userRole, userID }) const canModify = isProxyAdminRole(userRole ?? ""); return ( - <> + {canModify && ( = ({ accessToken, userRole, userID }) )} {!hasFallbacks ? (
- + No fallbacks configured. Add fallbacks to automatically try another model when the primary fails. - +
) : ( - + - Model Name - Fallbacks - Actions + Model Name + Fallbacks + Actions - + {routerSettings["fallbacks"].map((item: FallbackEntry, index: number) => Object.entries(item).map(([key, value]) => ( - {renderModelNameCell(key, getProviderFromModel)} - + + {renderModelNameCell(key, getProviderFromModel)} + + {renderFallbacksChain(key, Array.isArray(value) ? value : [], getProviderFromModel)} {canModify && ( <> - - testFallbackModelResponse(Object.keys(item)[0], accessToken || "")} - className="cursor-pointer hover:text-blue-600" - /> - - - handleEditClick(item)} - onKeyDown={(e) => e.key === "Enter" && handleEditClick(item)} - className="cursor-pointer inline-flex" + + testFallbackModelResponse(Object.keys(item)[0], accessToken || "")} + className={`${iconWrapperClass} cursor-pointer hover:text-blue-600`} + /> + } > - - + + + Test fallback - - handleDeleteClick(item)} - onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)} - className="cursor-pointer inline-flex" + + handleEditClick(item)} + onKeyDown={(e) => e.key === "Enter" && handleEditClick(item)} + className={`${iconWrapperClass} cursor-pointer hover:text-blue-600`} + /> + } > - - + + + Edit fallback + + + handleDeleteClick(item)} + onKeyDown={(e) => e.key === "Enter" && handleDeleteClick(item)} + className={`${iconWrapperClass} cursor-pointer hover:text-red-600`} + /> + } + > + + + Delete fallback )} @@ -350,7 +373,7 @@ const Fallbacks: React.FC = ({ accessToken, userRole, userID }) onOk={handleDeleteConfirm} confirmLoading={isDeleting} /> - + ); };