From 1ccb970d520aa24a078d5b2a40ccc7f5249a770c Mon Sep 17 00:00:00 2001 From: Kerry Lu Date: Fri, 30 Oct 2026 11:43:13 -0700 Subject: [PATCH] refactor(ui): remove the deprecated Old Usage page (#49284) Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/e2e/ui/fixtures/menuMappings.ts | 1 - tests/e2e/ui/fixtures/migratedPages.ts | 6 - tests/e2e/ui/fixtures/pages.ts | 1 - .../e2e/ui/tests/logs/logsPagination.spec.ts | 2 +- tests/e2e/ui/tests/usage/usagePage.spec.ts | 2 +- ui/litellm-dashboard/eslint-suppressions.json | 28 - .../app/(dashboard)/legacyPageRoutes.test.ts | 2 +- .../src/app/(dashboard)/legacyPageRoutes.ts | 2 +- .../old-usage/_components/usage.test.tsx | 257 ----- .../old-usage/_components/usage.tsx | 950 ------------------ .../src/app/(dashboard)/old-usage/page.tsx | 22 - .../components/UsagePageView.test.tsx | 4 - .../src/components/leftnav.test.tsx | 26 - .../src/components/leftnav.tsx | 8 - .../src/components/page_metadata.ts | 1 - .../src/components/view_user_spend.tsx | 144 --- .../tests/CreateKeyPage.expiredToken.test.tsx | 1 - 17 files changed, 4 insertions(+), 1453 deletions(-) delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.test.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx delete mode 100644 ui/litellm-dashboard/src/app/(dashboard)/old-usage/page.tsx delete mode 100644 ui/litellm-dashboard/src/components/view_user_spend.tsx diff --git a/tests/e2e/ui/fixtures/menuMappings.ts b/tests/e2e/ui/fixtures/menuMappings.ts index d6e7ea86982..4cf90c00cd0 100644 --- a/tests/e2e/ui/fixtures/menuMappings.ts +++ b/tests/e2e/ui/fixtures/menuMappings.ts @@ -32,7 +32,6 @@ export const menuLabelToPage: Record = { Budgets: Page.Budgets, "API Playground": Page.TransformRequest, "Tag Management": Page.TagManagement, - "Old Usage": Page.Usage, // Tools submenu items "MCP Servers": Page.McpServers, "Vector Stores": Page.VectorStores, diff --git a/tests/e2e/ui/fixtures/migratedPages.ts b/tests/e2e/ui/fixtures/migratedPages.ts index 5fa1f9d2444..6fa8d0c2587 100644 --- a/tests/e2e/ui/fixtures/migratedPages.ts +++ b/tests/e2e/ui/fixtures/migratedPages.ts @@ -118,12 +118,6 @@ export const MIGRATED_E2E_PAGES: Readonly> = { content: { role: "heading", name: "AI Hub" }, }, new_usage: { segment: "usage", linkName: "Usage", content: { role: "heading", name: "Usage" } }, - usage: { - segment: "old-usage", - linkName: "Old Usage", - group: "Experimental", - content: { role: "tab", name: "All Up" }, - }, agents: { segment: "agents", linkName: "Agents", group: "Agentic", content: { role: "heading", name: "Agents" } }, "router-settings": { segment: "router-settings", diff --git a/tests/e2e/ui/fixtures/pages.ts b/tests/e2e/ui/fixtures/pages.ts index 8210334c166..a850dcb4755 100644 --- a/tests/e2e/ui/fixtures/pages.ts +++ b/tests/e2e/ui/fixtures/pages.ts @@ -30,5 +30,4 @@ export enum Page { TagManagement = "tag-management", VectorStores = "vector-stores", NewUsage = "new_usage", - Usage = "usage", } diff --git a/tests/e2e/ui/tests/logs/logsPagination.spec.ts b/tests/e2e/ui/tests/logs/logsPagination.spec.ts index 585eb31c37c..a611f760e8b 100644 --- a/tests/e2e/ui/tests/logs/logsPagination.spec.ts +++ b/tests/e2e/ui/tests/logs/logsPagination.spec.ts @@ -84,7 +84,7 @@ test.describe("Logs pagination with Aria in the header", () => { await expect(aria).toBeFocused(); await visibleTestId(page, "pagination-first").click(); await expect(visibleTestId(page, "pagination-range")).toHaveText("Showing 1-25 of 26"); - for (const destination of [Page.Usage, Page.LlmPlayground, Page.ApiKeys]) { + for (const destination of [Page.NewUsage, Page.LlmPlayground, Page.ApiKeys]) { await navigateToPage(page, destination); await aria.click(); await expect(page.getByRole("dialog", { name: "Aria Agent", exact: true })).toBeVisible(); diff --git a/tests/e2e/ui/tests/usage/usagePage.spec.ts b/tests/e2e/ui/tests/usage/usagePage.spec.ts index d532042c7af..2a58a1ef59d 100644 --- a/tests/e2e/ui/tests/usage/usagePage.spec.ts +++ b/tests/e2e/ui/tests/usage/usagePage.spec.ts @@ -11,7 +11,7 @@ import { waitForSpendLog, } from "../../helpers/traffic"; -/** Covers /ui/usage. The legacy /ui/old-usage view is deprecated and deliberately not covered. */ +/** Covers /ui/usage. */ const topKeysCard = (page: PlaywrightPage): Locator => page.locator("section").filter({ has: page.getByRole("heading", { name: "Top Virtual Keys", exact: true }) }); diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 9fce93782b2..c14f3e6f036 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -625,23 +625,6 @@ "count": 1 } }, - "src/app/(dashboard)/old-usage/_components/usage.tsx": { - "local/filename-pascal-case": { - "count": 1 - }, - "max-lines": { - "count": 1 - }, - "prefer-const": { - "count": 6 - }, - "react-hooks/immutability": { - "count": 1 - }, - "react-hooks/purity": { - "count": 1 - } - }, "src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.tsx": { "react-hooks/set-state-in-effect": { "count": 2 @@ -2090,17 +2073,6 @@ "count": 1 } }, - "src/components/view_user_spend.tsx": { - "local/filename-pascal-case": { - "count": 1 - }, - "prefer-const": { - "count": 3 - }, - "react-hooks/set-state-in-effect": { - "count": 2 - } - }, "src/contexts/AuthContext.tsx": { "react-hooks/set-state-in-effect": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.test.ts index 6b02b79b04c..315d988db54 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.test.ts @@ -10,7 +10,7 @@ describe("legacyPageRedirectHref", () => { expect(redirect("page=models")).toBe("/ui/models-and-endpoints"); expect(redirect("page=llm-playground")).toBe("/ui/playground"); expect(redirect("page=new_usage")).toBe("/ui/usage"); - expect(redirect("page=usage")).toBe("/ui/old-usage"); + expect(redirect("page=usage")).toBe("/ui/usage"); }); it("keeps the older aliases for renamed pages", () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.ts b/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.ts index e3025266ddb..fe054c7b39a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.ts @@ -36,7 +36,7 @@ const LEGACY_PAGE_ROUTES: ReadonlyMap = new Map( "logging-and-alerts": "logging-and-alerts", "model-hub-table": "model-hub-table", new_usage: "usage", - usage: "old-usage", + usage: "usage", "cost-optimization": "cost-optimization", "model-insights": "model-insights", agents: "agents", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.test.tsx deleted file mode 100644 index 0e4455ee912..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.test.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import React from "react"; -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { act, screen, waitFor, within } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import { renderWithProviders } from "../../../../../tests/test-utils"; -import UsagePage from "./usage"; - -const networking = vi.hoisted(() => ({ - adminSpendLogsCall: vi.fn(), - adminTopKeysCall: vi.fn(), - adminTopModelsCall: vi.fn(), - adminTopEndUsersCall: vi.fn(), - teamSpendLogsCall: vi.fn(), - tagsSpendLogsCall: vi.fn(), - allTagNamesCall: vi.fn(), - adminspendByProvider: vi.fn(), - adminGlobalActivity: vi.fn(), - adminGlobalActivityPerModel: vi.fn(), - getProxyUISettings: vi.fn(), - modelAvailableCall: vi.fn(), - keyInfoV1Call: vi.fn(), -})); - -vi.mock("@/components/networking", () => networking); -vi.mock("../../../../components/networking", () => networking); - -vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ - default: () => ({ - accessToken: "sk-test", - token: "tok", - userRole: "Admin", - userId: "u1", - premiumUser: true, - }), -})); - -const UNLIMITED_SETTINGS = { DISABLE_EXPENSIVE_DB_QUERIES: false, NUM_SPEND_LOGS_ROWS: 10 }; - -const renderUsage = (overrides: Partial> = {}) => - renderWithProviders( - , - ); - -// Width of this window is guarded by "proves the flush window is wide enough". -const flushPendingRequests = async () => { - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - await new Promise((resolve) => setTimeout(resolve, 0)); - }); -}; - -beforeEach(() => { - vi.clearAllMocks(); - networking.getProxyUISettings.mockResolvedValue(UNLIMITED_SETTINGS); - networking.adminSpendLogsCall.mockResolvedValue([{ date: "2026-07-01", spend: 12.5 }]); - networking.adminTopKeysCall.mockResolvedValue([ - { api_key: "sk-abcdefghijk", key_alias: "prod-key", total_spend: 9.5 }, - ]); - networking.adminTopModelsCall.mockResolvedValue([{ model: "gpt-5.1", total_spend: 7.25 }]); - networking.adminTopEndUsersCall.mockResolvedValue([ - { end_user: "customer-alpha", total_spend: 3.5, total_count: 42 }, - ]); - networking.teamSpendLogsCall.mockResolvedValue({ - daily_spend: [{ date: "2026-07-01", "team-a": 5 }], - teams: ["team-a"], - total_spend_per_team: [{ team_id: "team-a", total_spend: 5 }], - }); - networking.tagsSpendLogsCall.mockResolvedValue({ spend_per_tag: [{ name: "prod", spend: 4 }] }); - networking.allTagNamesCall.mockResolvedValue({ tag_names: ["prod", "staging"] }); - networking.adminspendByProvider.mockResolvedValue([{ provider: "openai", spend: 6.75 }]); - networking.adminGlobalActivity.mockResolvedValue({ - sum_api_requests: 120, - sum_total_tokens: 4500, - daily_data: [{ date: "2026-07-01", api_requests: 120, total_tokens: 4500 }], - }); - networking.adminGlobalActivityPerModel.mockResolvedValue([]); - networking.modelAvailableCall.mockResolvedValue({ data: [] }); - networking.keyInfoV1Call.mockResolvedValue({ info: {} }); -}); - -describe("old usage page", () => { - describe("when the proxy has disabled expensive DB queries", () => { - beforeEach(() => { - networking.getProxyUISettings.mockResolvedValue({ - DISABLE_EXPENSIVE_DB_QUERIES: true, - NUM_SPEND_LOGS_ROWS: 2500000, - }); - }); - - it("shows the database query limit warning instead of the usage dashboard", async () => { - renderUsage(); - - expect(await screen.findByText("Database Query Limit Reached")).toBeInTheDocument(); - expect(screen.getByText(/SpendLogs in DB has/)).toHaveTextContent("2500000"); - expect(screen.getByText(/Please follow our guide to view usage when SpendLogs has more than 1M rows/i)); - expect(screen.queryByRole("tab", { name: "All Up" })).not.toBeInTheDocument(); - }); - - it("links to the cost tracking guide in a new tab", async () => { - renderUsage(); - - const link = await screen.findByRole("link", { name: "View Usage Guide" }); - expect(link).toHaveAttribute("href", "https://docs.litellm.ai/docs/proxy/cost_tracking"); - expect(link).toHaveAttribute("target", "_blank"); - }); - - it("skips every expensive usage query", async () => { - renderUsage(); - - await screen.findByText("Database Query Limit Reached"); - await waitFor(() => expect(networking.getProxyUISettings).toHaveBeenCalled()); - - expect(networking.adminSpendLogsCall).not.toHaveBeenCalled(); - expect(networking.adminspendByProvider).not.toHaveBeenCalled(); - expect(networking.adminTopKeysCall).not.toHaveBeenCalled(); - expect(networking.adminTopModelsCall).not.toHaveBeenCalled(); - expect(networking.adminGlobalActivity).not.toHaveBeenCalled(); - expect(networking.adminGlobalActivityPerModel).not.toHaveBeenCalled(); - expect(networking.teamSpendLogsCall).not.toHaveBeenCalled(); - expect(networking.adminTopEndUsersCall).not.toHaveBeenCalled(); - expect(networking.tagsSpendLogsCall).not.toHaveBeenCalled(); - }); - }); - - describe("as an admin", () => { - it("renders the admin tabs", async () => { - renderUsage(); - - expect(await screen.findByRole("tab", { name: "All Up" })).toBeInTheDocument(); - expect(screen.getByRole("tab", { name: "Team Based Usage" })).toBeInTheDocument(); - expect(screen.getByRole("tab", { name: "Customer Usage" })).toBeInTheDocument(); - expect(screen.getByRole("tab", { name: "Tag Based Usage" })).toBeInTheDocument(); - }); - - it("renders the cost panel cards", async () => { - renderUsage(); - - expect(await screen.findByText("Monthly Spend")).toBeInTheDocument(); - expect(screen.getByText("Top Virtual Keys")).toBeInTheDocument(); - expect(screen.getByText("Top Models")).toBeInTheDocument(); - expect(screen.getByText("Spend by Provider")).toBeInTheDocument(); - }); - - it("lists spend by provider in a table", async () => { - renderUsage(); - - const providerCell = await screen.findByText("openai"); - const row = providerCell.closest("tr"); - expect(row).not.toBeNull(); - expect(within(row as HTMLElement).getByText("$6.75")).toBeInTheDocument(); - expect(screen.getByRole("columnheader", { name: "Provider" })).toBeInTheDocument(); - }); - - it("shows the customer usage table when its tab is selected", async () => { - const user = userEvent.setup(); - renderUsage(); - - await user.click(await screen.findByRole("tab", { name: "Customer Usage" })); - - const customerCell = await screen.findByText("customer-alpha"); - const row = customerCell.closest("tr"); - expect(row).not.toBeNull(); - expect(within(row as HTMLElement).getByText("$3.50")).toBeInTheDocument(); - expect(within(row as HTMLElement).getByText("42")).toBeInTheDocument(); - expect(screen.getByRole("columnheader", { name: "Total Events" })).toBeInTheDocument(); - }); - - it("shows the tag spend panel when its tab is selected", async () => { - const user = userEvent.setup(); - renderUsage(); - - await user.click(await screen.findByRole("tab", { name: "Tag Based Usage" })); - - expect(await screen.findByText("Spend Per Tag")).toBeInTheDocument(); - }); - - it("shows the team spend panel when its tab is selected", async () => { - const user = userEvent.setup(); - renderUsage(); - - await user.click(await screen.findByRole("tab", { name: "Team Based Usage" })); - - expect(await screen.findByText("Total Spend Per Team")).toBeInTheDocument(); - expect(screen.getByText("Daily Spend Per Team")).toBeInTheDocument(); - }); - }); - - // org_admin is an organization membership role; those users reach the UI as "Internal User". - describe.each(["Internal User", "Internal Viewer", "internal_user", "internal_user_viewer", "Org Admin"])( - "as %s", - (userRole) => { - it("shows the admin-only notice instead of the usage dashboard", async () => { - renderUsage({ userRole }); - - expect(await screen.findByText(/Proxy-wide usage is only available to admin users/i)).toBeInTheDocument(); - expect(screen.queryByRole("tab", { name: "All Up" })).not.toBeInTheDocument(); - }); - - it("fires no /global/spend or /global/activity request", async () => { - renderUsage({ userRole }); - - await screen.findByText(/Proxy-wide usage is only available to admin users/i); - await flushPendingRequests(); - - expect(networking.getProxyUISettings).not.toHaveBeenCalled(); - expect(networking.adminSpendLogsCall).not.toHaveBeenCalled(); - expect(networking.adminTopKeysCall).not.toHaveBeenCalled(); - expect(networking.adminTopModelsCall).not.toHaveBeenCalled(); - expect(networking.adminTopEndUsersCall).not.toHaveBeenCalled(); - expect(networking.teamSpendLogsCall).not.toHaveBeenCalled(); - expect(networking.tagsSpendLogsCall).not.toHaveBeenCalled(); - expect(networking.allTagNamesCall).not.toHaveBeenCalled(); - expect(networking.adminspendByProvider).not.toHaveBeenCalled(); - expect(networking.adminGlobalActivity).not.toHaveBeenCalled(); - expect(networking.adminGlobalActivityPerModel).not.toHaveBeenCalled(); - }); - }, - ); - - describe("the admin-only gate", () => { - it("proves the flush window is wide enough to catch a leaked request", async () => { - renderUsage({ userRole: "Admin" }); - - await flushPendingRequests(); - - expect(networking.getProxyUISettings).toHaveBeenCalled(); - expect(networking.adminSpendLogsCall).toHaveBeenCalled(); - expect(networking.tagsSpendLogsCall).toHaveBeenCalled(); - expect(networking.adminGlobalActivity).toHaveBeenCalled(); - }); - - it("still lets an admin through, so the notice is a real gate and not a dead branch", async () => { - renderUsage({ userRole: "Admin" }); - - expect(await screen.findByRole("tab", { name: "All Up" })).toBeInTheDocument(); - expect(screen.queryByText(/Proxy-wide usage is only available to admin users/i)).not.toBeInTheDocument(); - await waitFor(() => expect(networking.adminSpendLogsCall).toHaveBeenCalled()); - }); - - it("does not put the session token in the provider spend query", async () => { - renderUsage({ userRole: "Admin", token: "session-jwt-value" }); - - await waitFor(() => expect(networking.adminspendByProvider).toHaveBeenCalled()); - const callArgs = networking.adminspendByProvider.mock.calls[0]; - expect(callArgs).not.toContain("session-jwt-value"); - expect(callArgs[0]).toBe("sk-test"); - }); - }); -}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx deleted file mode 100644 index 15b2a30b50c..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/_components/usage.tsx +++ /dev/null @@ -1,950 +0,0 @@ -import React, { useState, useEffect } from "react"; - -import ViewUserSpend from "@/components/view_user_spend"; -import { ProxySettings } from "@/app/(dashboard)/hooks/proxySettings/useProxySettings"; -import AdvancedDatePicker from "@/components/shared/advanced_date_picker"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Combobox, - ComboboxChip, - ComboboxChips, - ComboboxChipsInput, - ComboboxContent, - ComboboxEmpty, - ComboboxItem, - ComboboxList, - ComboboxValue, - useComboboxAnchor, -} from "@/components/ui/combobox"; -import { Meter, MeterIndicator, MeterTrack } from "@/components/shared/Meter"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { - NUMERIC_CELL_CLASS, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { AreaChart, BarChart, DonutChart } from "@/components/shared/charts"; - -import { - adminSpendLogsCall, - adminTopKeysCall, - adminTopModelsCall, - adminTopEndUsersCall, - teamSpendLogsCall, - tagsSpendLogsCall, - allTagNamesCall, - adminspendByProvider, - adminGlobalActivity, - adminGlobalActivityPerModel, - getProxyUISettings, -} from "@/components/networking"; -import TopKeyView from "@/components/UsagePage/components/EntityUsage/TopKeyView"; -import { MoneyCell } from "@/components/shared/table_cells"; -import { hasCapability } from "@/utils/capabilities"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; - -interface UsagePageProps { - accessToken: string | null; - token: string | null; - userRole: string | null; - userID: string | null; - keys: any[] | null; - premiumUser: boolean; -} - -interface GlobalActivityData { - sum_api_requests: number; - sum_total_tokens: number; - daily_data: { date: string; api_requests: number; total_tokens: number }[]; -} - -const EMPTY_GLOBAL_ACTIVITY: GlobalActivityData = { sum_api_requests: 0, sum_total_tokens: 0, daily_data: [] }; - -type UsageDateRange = { from?: Date; to?: Date }; - -type TeamSpendTotal = { name: string; value: number }; - -type TagOption = { value: string; label: string; disabled: boolean }; - -const ALL_TAGS = "all-tags"; - -const isAdminOrAdminViewer = (role: string | null): boolean => { - if (role === null) return false; - return role === "Admin" || role === "Admin Viewer"; -}; - -const TeamSpendBarList: React.FC<{ data: TeamSpendTotal[] }> = ({ data }) => { - const max = Math.max(0, ...data.map((team) => team.value)); - - return ( -
- {data.map((team) => ( -
-

{team.name}

- - - - - -

- {formatNumberWithCommas(team.value, 2)} -

-
- ))} -
- ); -}; - -const UsagePage: React.FC = ({ accessToken, token, userRole, userID, keys, premiumUser }) => { - const anchor = useComboboxAnchor(); - const canViewGlobalSpend = hasCapability(userRole, "viewGlobalSpend"); - const currentDate = new Date(); - const [keySpendData, setKeySpendData] = useState([]); - const [topKeys, setTopKeys] = useState([]); - const [topModels, setTopModels] = useState([]); - const [topUsers, setTopUsers] = useState([]); - const [teamSpendData, setTeamSpendData] = useState([]); - const [topTagsData, setTopTagsData] = useState([]); - const [allTagNames, setAllTagNames] = useState([]); - const [uniqueTeamIds, setUniqueTeamIds] = useState([]); - const [totalSpendPerTeam, setTotalSpendPerTeam] = useState([]); - const [spendByProvider, setSpendByProvider] = useState([]); - const [globalActivity, setGlobalActivity] = useState(EMPTY_GLOBAL_ACTIVITY); - const [globalActivityPerModel, setGlobalActivityPerModel] = useState([]); - const [selectedKeyToken, setSelectedKeyToken] = useState(null); - const [selectedTags, setSelectedTags] = useState([ALL_TAGS]); - const [dateValue, setDateValue] = useState({ - from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), - to: new Date(), - }); - const [proxySettings, setProxySettings] = useState(null); - const [totalMonthlySpend, setTotalMonthlySpend] = useState(0); - - const firstDay = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1); - const lastDay = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); - - let startTime = formatDate(firstDay); - let endTime = formatDate(lastDay); - - const selectableKeys: { token: string; alias: string }[] = (keys ?? []) - .filter((key: any) => key && typeof key["key_alias"] === "string" && key["key_alias"].length > 0) - .map((key: any) => ({ token: String(key["token"]), alias: String(key["key_alias"]) })); - - const tagOptions: TagOption[] = [ - { value: ALL_TAGS, label: "All Tags", disabled: false }, - ...allTagNames - .filter((tag) => tag !== ALL_TAGS) - .map((tag) => ({ - value: tag, - label: premiumUser ? tag : `✨ ${tag} (Enterprise only Feature)`, - disabled: !premiumUser, - })), - ]; - - function valueFormatterNumbers(number: number) { - const formatter = new Intl.NumberFormat("en-US", { - maximumFractionDigits: 0, - notation: "compact", - compactDisplay: "short", - }); - - return formatter.format(number); - } - - const fetchProxySettings = async () => { - if (accessToken) { - try { - const proxy_settings: ProxySettings = await getProxyUISettings(accessToken); - return proxy_settings; - } catch (error) { - console.error("Error fetching proxy settings:", error); - } - } - }; - - useEffect(() => { - if (!canViewGlobalSpend) { - return; - } - updateTagSpendData(dateValue.from, dateValue.to); - }, [canViewGlobalSpend, dateValue, selectedTags]); - - const updateEndUserData = async ( - startTime: Date | undefined, - endTime: Date | undefined, - uiSelectedKey: string | null, - ) => { - if (!startTime || !endTime || !accessToken) { - return; - } - - let newTopUserData = await adminTopEndUsersCall( - accessToken, - uiSelectedKey, - startTime.toISOString(), - endTime.toISOString(), - ); - setTopUsers(newTopUserData); - }; - - const updateTagSpendData = async (startTime: Date | undefined, endTime: Date | undefined) => { - if (!startTime || !endTime || !accessToken) { - return; - } - - // we refetch because the state variable can be None when the user refreshes the page - const proxy_settings: ProxySettings | undefined = await fetchProxySettings(); - - if (proxy_settings?.DISABLE_EXPENSIVE_DB_QUERIES) { - return; // Don't run expensive DB queries - return out when SpendLogs has more than 1M rows - } - - let top_tags = await tagsSpendLogsCall( - accessToken, - startTime.toISOString(), - endTime.toISOString(), - selectedTags.length === 0 ? undefined : selectedTags, - ); - setTopTagsData(top_tags.spend_per_tag); - }; - - function formatDate(date: Date) { - const year = date.getFullYear(); - let month = date.getMonth() + 1; // JS month index starts from 0 - let day = date.getDate(); - - // Pad with 0 if month or day is less than 10 - const monthStr = month < 10 ? "0" + month : month; - const dayStr = day < 10 ? "0" + day : day; - - return `${year}-${monthStr}-${dayStr}`; - } - - const valueFormatter = (number: number) => `$ ${formatNumberWithCommas(number, 2)}`; - - const fetchAndSetData = async ( - fetchFunction: () => Promise, - setStateFunction: React.Dispatch>, - errorMessage: string, - ) => { - try { - const data = await fetchFunction(); - setStateFunction(data); - } catch (error) { - console.error(errorMessage, error); - // Optionally, update UI to reflect error state for this specific data - } - }; - - // Update the fillMissingDates function to handle different date formats - const fillMissingDates = (data: any[], startDate: Date, endDate: Date, categories: string[]) => { - const filledData = []; - const currentDate = new Date(startDate); - - // Helper function to standardize date format - const standardizeDate = (dateStr: string) => { - if (dateStr.includes("-")) { - // Already in YYYY-MM-DD format - return dateStr; - } else { - // Convert "Jan 06" format - const [month, day] = dateStr.split(" "); - const year = new Date().getFullYear(); - const monthIndex = new Date(`${month} 01 2024`).getMonth(); - const fullDate = new Date(year, monthIndex, parseInt(day)); - return fullDate.toISOString().split("T")[0]; - } - }; - - // Create a map of existing dates for quick lookup - const existingDates = new Map( - data.map((item) => { - const standardizedDate = standardizeDate(item.date); - return [ - standardizedDate, - { - ...item, - date: standardizedDate, // Store standardized date format - }, - ]; - }), - ); - - // Iterate through each date in the range - while (currentDate <= endDate) { - const dateStr = currentDate.toISOString().split("T")[0]; - - if (existingDates.has(dateStr)) { - // Use existing data if we have it - filledData.push(existingDates.get(dateStr)); - } else { - // Create an entry with zero values - const emptyEntry: any = { - date: dateStr, - api_requests: 0, - total_tokens: 0, - }; - - // Add zero values for each model/team if needed - categories.forEach((category) => { - if (!emptyEntry[category]) { - emptyEntry[category] = 0; - } - }); - - filledData.push(emptyEntry); - } - - // Move to next day - currentDate.setDate(currentDate.getDate() + 1); - } - - return filledData; - }; - - // Update the fetchOverallSpend function - const fetchOverallSpend = async () => { - if (!accessToken) { - return; - } - try { - const data = await adminSpendLogsCall(accessToken); - - // Get the first and last day of the current month - const now = new Date(); - const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); - const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0); - - // Fill in missing dates - const filledData = fillMissingDates(data, firstDay, lastDay, []); - - // Calculate total spend for the month and round to 2 decimal places - const monthlyTotal = Number(filledData.reduce((sum, day) => sum + (day.spend || 0), 0).toFixed(2)); - setTotalMonthlySpend(monthlyTotal); - - setKeySpendData(filledData); - } catch (error) { - console.error("Error fetching overall spend:", error); - } - }; - - const fetchProviderSpend = () => - fetchAndSetData( - () => (accessToken ? adminspendByProvider(accessToken, startTime, endTime) : Promise.reject("No access token")), - setSpendByProvider, - "Error fetching provider spend", - ); - - const fetchTopKeys = async () => { - if (!accessToken) return; - await fetchAndSetData( - async () => { - const top_keys = await adminTopKeysCall(accessToken); - return top_keys.map((k: any) => ({ - key: k["api_key"].substring(0, 10), - api_key: k["api_key"], - key_alias: k["key_alias"], - spend: Number(k["total_spend"].toFixed(2)), - })); - }, - setTopKeys, - "Error fetching top keys", - ); - }; - - const fetchTopModels = async () => { - if (!accessToken) return; - await fetchAndSetData( - async () => { - const top_models = await adminTopModelsCall(accessToken); - return top_models.map((k: any) => ({ - key: k["model"], - spend: formatNumberWithCommas(k["total_spend"], 2), - })); - }, - setTopModels, - "Error fetching top models", - ); - }; - - // Update the fetchTeamSpend function - const fetchTeamSpend = async () => { - if (!accessToken) return; - await fetchAndSetData( - async () => { - const teamSpend = await teamSpendLogsCall(accessToken); - - // Get the first and last day of the current month - const now = new Date(); - const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); - const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0); - - // Fill in missing dates with zero values for all teams - const filledData = fillMissingDates(teamSpend.daily_spend, firstDay, lastDay, teamSpend.teams); - - setTeamSpendData(filledData); - setUniqueTeamIds(teamSpend.teams); - return teamSpend.total_spend_per_team.map((tspt: any) => ({ - name: tspt["team_id"] || "", - value: Number(tspt["total_spend"] || 0), - })); - }, - setTotalSpendPerTeam, - "Error fetching team spend", - ); - }; - - const fetchTagNames = () => { - if (!accessToken) return; - fetchAndSetData( - async () => { - const all_tag_names = await allTagNamesCall(accessToken); - return all_tag_names.tag_names; - }, - setAllTagNames, - "Error fetching tag names", - ); - }; - - const fetchTopTags = () => { - if (!accessToken) return; - fetchAndSetData( - () => tagsSpendLogsCall(accessToken, dateValue.from?.toISOString(), dateValue.to?.toISOString(), undefined), - (data) => setTopTagsData(data.spend_per_tag), - "Error fetching top tags", - ); - }; - - const fetchTopEndUsers = () => { - if (!accessToken) return; - fetchAndSetData( - () => adminTopEndUsersCall(accessToken, null, undefined, undefined), - setTopUsers, - "Error fetching top end users", - ); - }; - - // Update the fetchGlobalActivity function - const fetchGlobalActivity = async () => { - if (!accessToken) return; - try { - const data = await adminGlobalActivity(accessToken, startTime, endTime); - - // Get the date range from the current month - const now = new Date(); - const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); - const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0); - - // Fill in missing dates for daily_data - const filledDailyData = fillMissingDates(data.daily_data || [], firstDay, lastDay, [ - "api_requests", - "total_tokens", - ]); - - setGlobalActivity({ - ...data, - daily_data: filledDailyData, - }); - } catch (error) { - console.error("Error fetching global activity:", error); - } - }; - - // Update the fetchGlobalActivityPerModel function - const fetchGlobalActivityPerModel = async () => { - if (!accessToken) return; - try { - const data = await adminGlobalActivityPerModel(accessToken, startTime, endTime); - - // Get the date range from the current month - const now = new Date(); - const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); - const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0); - - // Fill in missing dates for each model's daily data - const filledModelData = data.map((modelData: any) => ({ - ...modelData, - daily_data: fillMissingDates(modelData.daily_data || [], firstDay, lastDay, ["api_requests", "total_tokens"]), - })); - - setGlobalActivityPerModel(filledModelData); - } catch (error) { - console.error("Error fetching global activity per model:", error); - } - }; - - useEffect(() => { - const initlizeUsageData = async () => { - if (!canViewGlobalSpend) { - return; - } - if (accessToken && token && userRole && userID) { - const proxy_settings: ProxySettings | undefined = await fetchProxySettings(); - if (proxy_settings) { - setProxySettings(proxy_settings); // saved in state so it can be used when rendering UI - if (proxy_settings?.DISABLE_EXPENSIVE_DB_QUERIES) { - return; // Don't run expensive UI queries - return out of initlizeUsageData at this point - } - } - - fetchOverallSpend(); - fetchProviderSpend(); - fetchTopKeys(); - fetchTopModels(); - fetchGlobalActivity(); - fetchGlobalActivityPerModel(); - - if (isAdminOrAdminViewer(userRole)) { - fetchTeamSpend(); - fetchTagNames(); - fetchTopTags(); - fetchTopEndUsers(); - } - } - }; - - initlizeUsageData(); - }, [canViewGlobalSpend, accessToken, token, userRole, userID, startTime, endTime]); - - if (!canViewGlobalSpend) { - return ( -
- - - Usage - - -

- Proxy-wide usage is only available to admin users. Your own usage is on the Usage page. -

-
-
-
- ); - } - - if (proxySettings?.DISABLE_EXPENSIVE_DB_QUERIES) { - return ( -
- - - Database Query Limit Reached - - -

- SpendLogs in DB has {proxySettings.NUM_SPEND_LOGS_ROWS} rows. -

- Please follow our guide to view usage when SpendLogs has more than 1M rows. -

-
- ); - } - - return ( -
- - - All Up - - {isAdminOrAdminViewer(userRole) && ( - <> - Team Based Usage - Customer Usage - Tag Based Usage - - )} - - - - - - Cost - Activity - - - -
-
-

- Project Spend {new Date().toLocaleString("default", { month: "long" })} 1 -{" "} - {new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate()} -

- -
-
- - - Monthly Spend - - - - - -
-
- - - Top Virtual Keys - - - {}} /> - - -
-
- - - Top Models - - - `$${formatNumberWithCommas(value, 2)}`} - /> - - -
-
-
- - - Spend by Provider - - -
-
- `$${formatNumberWithCommas(value, 2)}`} - /> -
-
- - - - Provider - Spend - - - - {spendByProvider.map((provider) => ( - - {provider.provider} - - - - - ))} - -
-
-
-
-
-
-
-
- - -
- - - All Up - - -
-
-

- API Requests {valueFormatterNumbers(globalActivity.sum_api_requests)} -

- -
-
-

- Tokens {valueFormatterNumbers(globalActivity.sum_total_tokens)} -

- -
-
-
-
- - {globalActivityPerModel.map((globalActivity, index) => ( - - - {globalActivity.model} - - -
-
-

- API Requests {valueFormatterNumbers(globalActivity.sum_api_requests)} -

- -
-
-

- Tokens {valueFormatterNumbers(globalActivity.sum_total_tokens)} -

- -
-
-
-
- ))} -
-
-
-
- - -
-
- - - Total Spend Per Team - - - - - - - - Daily Spend Per Team - - - - - -
-
-
- - -

- Customers of your LLM API calls. Tracked when a `user` param is passed in your LLM calls{" "} - - docs here - -

-
-
- { - setDateValue(value); - updateEndUserData(value.from, value.to, null); - }} - /> -
-
-

Select Key

- -
-
- - - -
- - - - Customer - Spend - Total Events - - - - - {topUsers?.map((user: any, index: number) => ( - - {user.end_user} - - - - {user.total_count} - - ))} - -
-
-
-
-
- - -
-
- { - setDateValue(value); - updateTagSpendData(value.from, value.to); - }} - /> -
- -
- selectedTags.includes(option.value))} - onValueChange={(options: TagOption[]) => setSelectedTags(options.map((option) => option.value))} - isItemEqualToValue={(a: TagOption, b: TagOption) => a.value === b.value} - itemToStringLabel={(option: TagOption) => option.label} - > - }> - - {(options: TagOption[]) => - options.map((option) => ( - - {option.label} - - )) - } - - - - - No tags found - - {(option: TagOption) => ( - - {option.label} - - )} - - - -
-
-
-
- - - Spend Per Tag - - -

- Get Started by Tracking cost per tag{" "} - - here - -

- -
-
-
-
-
-
-
- ); -}; - -export default UsagePage; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/old-usage/page.tsx deleted file mode 100644 index 138dd97e5e8..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/old-usage/page.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client"; - -import Usage from "./_components/usage"; -import { DeprecationBanner } from "@/components/DeprecationBanner"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -export default function OldUsagePage() { - const { accessToken, token, userRole, userId: userID, premiumUser } = useAuthorized(); - return ( - <> - - - - ); -} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx index 9d39e9c080c..f7ffd6d52cf 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx @@ -102,10 +102,6 @@ vi.mock("@/components/activity_metrics", () => ({ processActivityData: vi.fn(), })); -vi.mock("@/components/view_user_spend", () => ({ - default: () =>
View User Spend
, -})); - vi.mock("@/components/UsagePage/components/EntityUsage/TopKeyView", () => ({ default: () =>
Top Keys
, })); diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx index 6cf05af1ed1..f3bff85e2d5 100644 --- a/ui/litellm-dashboard/src/components/leftnav.test.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx @@ -444,30 +444,6 @@ describe("Sidebar (leftnav)", () => { }); expect(screen.queryByText("Prompts")).not.toBeInTheDocument(); }); - - it("should hide Old Usage from internal users while keeping other Experimental children", async () => { - mockUseAuthorized.mockReturnValue(internalAuth); - renderWithProviders(); - - act(() => { - fireEvent.click(screen.getByText("Experimental")); - }); - await waitFor(() => { - expect(screen.getByText("API Playground")).toBeInTheDocument(); - }); - expect(screen.queryByText("Old Usage")).not.toBeInTheDocument(); - }); - - it("should show Old Usage to admins", async () => { - renderWithProviders(); - - act(() => { - fireEvent.click(screen.getByText("Experimental")); - }); - await waitFor(() => { - expect(screen.getByText("Old Usage")).toBeInTheDocument(); - }); - }); }); // Workflow Runs, Memory and Guardrails Monitor render a shell and then 401 @@ -704,7 +680,6 @@ describe("Sidebar (leftnav)", () => { expectHref("Models + Endpoints", "/ui/models-and-endpoints"); expectHref("Usage", "/ui/usage"); expectHref("API Reference", "/ui/api-reference"); - expectHref("Old Usage", "/ui/old-usage"); }); it("never links a leaf to the legacy ?page= switch", () => { @@ -760,7 +735,6 @@ describe("getBreadcrumb", () => { it("resolves routes whose segment differs from the sidebar page id", () => { expect(getBreadcrumb("/ui/models-and-endpoints")).toEqual({ section: "AI Gateway", title: "Models + Endpoints" }); expect(getBreadcrumb("/ui/usage")).toEqual({ section: "Observability", title: "Usage" }); - expect(getBreadcrumb("/ui/old-usage")).toEqual({ section: "Developer Tools", title: "Old Usage" }); }); it("titles the dashboard root as Home", () => { diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 6795cf1c7d1..2cf6f8219db 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -414,14 +414,6 @@ const menuGroups: MenuGroup[] = [ icon: , roles: all_admin_roles, }, - { - key: "4", - page: "usage", - route: "old-usage", - label: "Old Usage", - icon: , - roles: rolesWithCapability("viewGlobalSpend"), - }, ], }, ], diff --git a/ui/litellm-dashboard/src/components/page_metadata.ts b/ui/litellm-dashboard/src/components/page_metadata.ts index 18f136c3b28..97220a0ef76 100644 --- a/ui/litellm-dashboard/src/components/page_metadata.ts +++ b/ui/litellm-dashboard/src/components/page_metadata.ts @@ -41,7 +41,6 @@ export const pageDescriptions: Record = { "tag-management": "Organize resources with tags", prompts: "Manage and version prompt templates", skills: "Browse and manage Claude Code skills", - usage: "View legacy usage dashboard", "router-settings": "Configure routing and load balancing settings", telemetry: "Choose which anonymous usage telemetry this proxy collects and see the last report", "logging-and-alerts": "Set up logging and alert configurations", diff --git a/ui/litellm-dashboard/src/components/view_user_spend.tsx b/ui/litellm-dashboard/src/components/view_user_spend.tsx deleted file mode 100644 index 655fc092673..00000000000 --- a/ui/litellm-dashboard/src/components/view_user_spend.tsx +++ /dev/null @@ -1,144 +0,0 @@ -"use client"; -import React, { useEffect, useState } from "react"; -import { modelAvailableCall } from "./networking"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; - -// Define the props type -interface ViewUserSpendProps { - userSpend: number | null; - userMaxBudget: number | null; - selectedTeam: any | null; -} -const ViewUserSpend: React.FC = ({ userSpend, userMaxBudget, selectedTeam }) => { - const { accessToken, userRole, userId: userID } = useAuthorized(); - let [spend, setSpend] = useState(userSpend !== null ? userSpend : 0.0); - const [maxBudget, setMaxBudget] = useState( - selectedTeam ? Number(formatNumberWithCommas(selectedTeam.max_budget, 4)) : null, - ); - useEffect(() => { - if (selectedTeam) { - if (selectedTeam.team_alias === "Default Team") { - setMaxBudget(userMaxBudget); - } else { - let setMaxBudgetFlag = false; - if (selectedTeam.team_memberships) { - /** - * What 'team_memberships' looks like: - * "team_memberships": [ - * { - * "user_id": "2c315de3-e7ce-4269-b73e-b039a06187b1", - * "team_id": "test-team_515e6f42-ded2-4f0d-8919-0a1f43c5a45f", - * "budget_id": "0880769f-716a-4149-ab19-7f7651ad4db5", - * "litellm_budget_table": { - "soft_budget": null, - "max_budget": 20.0, - "max_parallel_requests": null, - "tpm_limit": null, - "rpm_limit": null, - "model_max_budget": null, - "budget_duration": null - } - */ - for (const member of selectedTeam.team_memberships) { - if ( - member.user_id === userID && - "max_budget" in member.litellm_budget_table && - member.litellm_budget_table.max_budget !== null - ) { - setMaxBudget(member.litellm_budget_table.max_budget); - setMaxBudgetFlag = true; - } - } - } - if (!setMaxBudgetFlag) { - setMaxBudget(selectedTeam.max_budget); - } - } - } else { - setMaxBudget(userMaxBudget); - } - }, [selectedTeam, userMaxBudget]); - const [userModels, setUserModels] = useState([]); - useEffect(() => { - const fetchData = async () => { - if (!accessToken || !userID || !userRole) { - return; - } - }; - const fetchUserModels = async () => { - try { - if (userID === null || userRole === null) { - return; - } - - if (accessToken !== null) { - const model_available = await modelAvailableCall(accessToken, userID, userRole); - let available_model_names = model_available["data"].map((element: { id: string }) => element.id); - setUserModels(available_model_names); - } - } catch (error) { - console.error("Error fetching user models:", error); - } - }; - - fetchUserModels(); - fetchData(); - }, [userRole, accessToken, userID]); - - useEffect(() => { - if (userSpend !== null) { - setSpend(userSpend); - } - }, [userSpend]); - - // logic to decide what models to display - let modelsToDisplay = []; - if (selectedTeam && selectedTeam.models) { - modelsToDisplay = selectedTeam.models; - } - - // check if "all-proxy-models" is in modelsToDisplay - if (modelsToDisplay && modelsToDisplay.includes("all-proxy-models")) { - modelsToDisplay = userModels; - } else if (modelsToDisplay && modelsToDisplay.includes("all-team-models")) { - modelsToDisplay = selectedTeam.models; - } else if (modelsToDisplay && modelsToDisplay.length === 0) { - modelsToDisplay = userModels; - } - - const displayMaxBudget = maxBudget !== null ? `$${formatNumberWithCommas(Number(maxBudget), 4)} limit` : "No limit"; - - const roundedSpend = spend !== undefined ? formatNumberWithCommas(spend, 4) : null; - - return ( -
-
-
-

Total Spend

-

${roundedSpend}

-
-
-

Max Budget

-

{displayMaxBudget}

-
-
- {/*
- - Team Models - - - {modelsToDisplay.map((model: string) => ( - - {model} - - ))} - - - -
*/} -
- ); -}; - -export default ViewUserSpend; diff --git a/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx b/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx index 58d7309c078..e5efd4c8e9f 100644 --- a/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx +++ b/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx @@ -127,7 +127,6 @@ vi.mock("@/components/new_usage", () => ({ default: stub("new-usage") })); vi.mock("@/components/api_ref", () => ({ default: stub("api-ref") })); vi.mock("@/components/chat_ui/ChatUI", () => ({ default: stub("chat-ui") })); vi.mock("@/components/leftnav", () => ({ default: stub("sidebar") })); -vi.mock("@/app/(dashboard)/old-usage/_components/usage", () => ({ default: stub("usage") })); vi.mock("@/components/cache_dashboard", () => ({ default: stub("cache-dashboard") })); vi.mock("@/app/(dashboard)/guardrails/_components", () => ({ default: stub("guardrails") })); vi.mock("@/components/prompts", () => ({ default: stub("prompts") }));