From 4ee55a28f75ac7e5eee80361e50be8a40d6b3293 Mon Sep 17 00:00:00 2001 From: moe-berri Date: Fri, 13 Nov 2026 02:03:44 +0800 Subject: [PATCH] refactor(ui): consolidate model leaderboard into Usage (#51426) --- .../src/app/(dashboard)/legacyPageRoutes.ts | 2 +- .../_components/ModelInsightsView.test.tsx | 125 --------- .../_components/ModelInsightsView.tsx | 247 ------------------ .../_components/modelInsightsData.test.ts | 169 ------------ .../_components/modelInsightsData.ts | 146 ----------- .../app/(dashboard)/model-insights/page.tsx | 15 +- .../components/overview/UsageOverview.tsx | 6 - .../src/components/leftnav.tsx | 12 - .../src/components/page_metadata.ts | 1 - .../shared/charts/StackedUsageChart.tsx | 1 - .../src/i18n/locales/en.json | 34 --- 11 files changed, 12 insertions(+), 746 deletions(-) delete mode 100644 litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.test.tsx delete mode 100644 litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx delete mode 100644 litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.test.ts delete mode 100644 litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.ts diff --git a/litellm/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.ts b/litellm/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.ts index 98cdd1e79bb..74c29fc31b9 100644 --- a/litellm/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.ts +++ b/litellm/ui/litellm-dashboard/src/app/(dashboard)/legacyPageRoutes.ts @@ -39,7 +39,7 @@ const LEGACY_PAGE_ROUTES: ReadonlyMap = new Map( new_usage: "usage", usage: "usage", "cost-optimization": "cost-optimization", - "model-insights": "model-insights", + "model-insights": "usage", agents: "agents", "router-settings": "router-settings", users: "users", diff --git a/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.test.tsx b/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.test.tsx deleted file mode 100644 index 7aad92f9814..00000000000 --- a/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.test.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { render, screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import type React from "react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import ModelInsightsView from "./ModelInsightsView"; -import { apiClient } from "@/components/networking"; - -vi.mock("@/components/networking", () => ({ apiClient: { get: vi.fn() } })); -vi.mock("@/components/ui/chart", () => ({ - ChartContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, - ChartTooltip: () => null, - ChartTooltipContent: () => null, -})); -vi.mock("recharts", () => ({ - Bar: () => null, - BarChart: ({ children, data }: { children: React.ReactNode; data: { date: string }[] }) => ( -
- {children} -
- ), - CartesianGrid: () => null, - XAxis: () => null, - YAxis: () => null, -})); - -const metrics = { - model_group: "fast-chat", - model: "openai/gpt-5.4-mini", - provider: "openai", - spend: 2.5, - prompt_tokens: 1000, - completion_tokens: 2000, - requests: 12, - successful_requests: 12, - failed_requests: 0, -}; - -const response = { - start_date: "2025-09-29", - end_date: "2026-09-28", - top_models: [metrics], - daily: [{ ...metrics, date: "2026-09-28" }], - daily_totals: [{ date: "2026-09-28", spend: 2.5, prompt_tokens: 1000, completion_tokens: 2000, requests: 12 }], -}; - -describe("ModelInsightsView", () => { - beforeEach(() => { - vi.mocked(apiClient.get).mockReset(); - vi.mocked(apiClient.get).mockResolvedValue(response); - }); - - it("shows the ranking with share from the API response", async () => { - render(); - - expect(await screen.findByText("fast-chat")).toBeInTheDocument(); - expect(screen.getByText("by openai")).toBeInTheDocument(); - expect(screen.getAllByText("100.0%")).toHaveLength(1); - expect(screen.queryByText("Top models by task")).not.toBeInTheDocument(); - expect(screen.getByRole("tab", { name: "tokens" })).toHaveAttribute("aria-selected", "true"); - expect(screen.getByRole("tab", { name: "log" })).toBeInTheDocument(); - expect(apiClient.get).toHaveBeenCalledWith("/model-insights", { - accessToken: "token", - query: { metric: "tokens" }, - }); - expect(apiClient.get).not.toHaveBeenCalledWith("/model-insights/tasks", expect.anything()); - }); - - it("refetches with the selected metric so top models are ranked by it", async () => { - render(); - await screen.findByText("fast-chat"); - - await userEvent.click(screen.getByRole("tab", { name: "requests" })); - - await waitFor(() => - expect(apiClient.get).toHaveBeenCalledWith("/model-insights", { - accessToken: "token", - query: { metric: "requests" }, - }), - ); - }); - - it("shows the API error instead of loading forever", async () => { - vi.mocked(apiClient.get).mockRejectedValue(new Error("Only proxy admins can view deployment-wide model insights")); - render(); - - expect(await screen.findByText("Could not load model insights")).toBeInTheDocument(); - expect(screen.getByText("Only proxy admins can view deployment-wide model insights")).toBeInTheDocument(); - }); - - it("keeps the previous ranking, dimmed, until the new metric's data arrives", async () => { - render(); - await screen.findByText("fast-chat"); - let resolve: (value: typeof response) => void = () => {}; - vi.mocked(apiClient.get).mockImplementation(() => new Promise((done) => (resolve = done as typeof resolve))); - - await userEvent.click(screen.getByRole("tab", { name: "spend" })); - - expect( - screen.getByText("Share of tokens, with the change between the first and second half of the period"), - ).toBeInTheDocument(); - - resolve(response); - expect( - await screen.findByText("Share of spend, with the change between the first and second half of the period"), - ).toBeInTheDocument(); - }); - - it("charts one bar per day by default and switches to weekly bars", async () => { - render(); - await screen.findByText("fast-chat"); - const chart = screen.getByTestId("usage-chart"); - - expect(screen.getByRole("tab", { name: "Daily" })).toHaveAttribute("aria-selected", "true"); - expect(chart).toHaveAttribute("data-buckets", "30"); - expect(chart).toHaveAttribute("data-first", "2026-08-30"); - expect(screen.getByText("Daily tokens across your gateway")).toBeInTheDocument(); - - await userEvent.click(screen.getByRole("tab", { name: "Weekly" })); - - expect(chart).toHaveAttribute("data-buckets", "12"); - expect(chart).toHaveAttribute("data-first", "2026-07-13"); - expect(screen.getByText("Weekly tokens across your gateway")).toBeInTheDocument(); - }); -}); diff --git a/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx b/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx deleted file mode 100644 index e80d5cf1709..00000000000 --- a/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx +++ /dev/null @@ -1,247 +0,0 @@ -"use client"; - -import { Page } from "@/components/shared/Page"; -import type { ParseKeys } from "i18next"; -import React from "react"; -import { useTranslation } from "react-i18next"; -import { ArrowDownRight, ArrowUpRight, BarChart3, Minus } from "lucide-react"; -import { StackedUsageChart } from "@/components/shared/charts"; - -import { apiClient } from "@/components/networking"; -import { extractErrorMessage } from "@/utils/errorUtils"; -import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; -import { PageHeader, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - buildBucketTotals, - buildSeries, - formatMetric, - Granularity, - Metric, - ModelInsightsResponse, - modelOrder, - rankModels, - RankedModel, -} from "./modelInsightsData"; - -const SCALES = ["linear", "log"] as const; -const GRANULARITIES = ["day", "week"] as const; -const METRICS = ["requests", "spend", "tokens"] as const; -const GRANULARITY_LABELS: Record = { - day: "modelInsights.modelInsightsView.granularity.day", - week: "modelInsights.modelInsightsView.granularity.week", -}; -const METRIC_LABELS: Record = { - requests: "modelInsights.modelInsightsView.metrics.requests", - spend: "modelInsights.modelInsightsView.metrics.spend", - tokens: "modelInsights.modelInsightsView.metrics.tokens", -}; -const RANKING_ROWS = 5; - -type Scale = (typeof SCALES)[number]; - -const formatDelta = (value: number) => `${value > 0 ? "+" : ""}${value.toFixed(1)}`; - -const DeltaBadge = ({ value }: { value: number }) => { - if (Math.abs(value) < 0.05) { - return ( - - 0.0 - - ); - } - const up = value > 0; - const Icon = up ? ArrowUpRight : ArrowDownRight; - return ( - - {formatDelta(value)} - - ); -}; - -const RankingRow = ({ model, rank }: { model: RankedModel; rank: number }) => { - const { t } = useTranslation(); - return ( -
  • - {rank} - -
    -

    {model.model_group}

    -

    - {t("modelInsights.modelInsightsView.byProvider", { provider: model.provider })} -

    -
    -
    -

    {model.share.toFixed(1)}%

    - -
    -
  • - ); -}; - -export default function ModelInsightsView({ accessToken }: { accessToken: string | null }) { - const { t } = useTranslation(); - const [loaded, setLoaded] = React.useState<{ metric: Metric; response: ModelInsightsResponse } | null>(null); - const [metric, setMetric] = React.useState("tokens"); - const [scale, setScale] = React.useState("linear"); - const [granularity, setGranularity] = React.useState("day"); - const [error, setError] = React.useState(null); - - React.useEffect(() => { - if (!accessToken) return; - let cancelled = false; - apiClient - .get("/model-insights", { accessToken, query: { metric } }) - .then((response) => { - if (cancelled) return; - setError(null); - setLoaded({ metric, response }); - }) - .catch((err: unknown) => { - if (!cancelled) setError(extractErrorMessage(err)); - }); - return () => { - cancelled = true; - }; - }, [accessToken, metric]); - - const data = loaded?.response ?? null; - const shown = loaded?.metric ?? metric; - const isStale = loaded !== null && loaded.metric !== metric; - const range = React.useMemo(() => ({ start: data?.start_date ?? "", end: data?.end_date ?? "" }), [data]); - const models = React.useMemo(() => (data ? modelOrder(data.daily, shown) : []), [data, shown]); - const series = React.useMemo( - () => (data ? buildSeries(data.daily, models, shown, { ...range, granularity }) : []), - [data, models, shown, range, granularity], - ); - const bucketTotals = React.useMemo( - () => (data ? buildBucketTotals(data.daily_totals, shown, { ...range, granularity }) : new Map()), - [data, shown, range, granularity], - ); - const ranking = React.useMemo( - () => (data ? rankModels(data.top_models, data.daily, shown, range) : []), - [data, shown, range], - ); - - if (error) { - return ( -
    - - {t("modelInsights.modelInsightsView.loadError")} - {error} - -
    - ); - } - - if (!data) { - return ( -
    - - -
    - ); - } - - return ( - - - - - {t("modelInsights.modelInsightsView.title")} - - - {t("modelInsights.modelInsightsView.description", { start: data.start_date, end: data.end_date })} - - - - - -
    - {t("modelInsights.modelInsightsView.topModels.title")} - - {t("modelInsights.modelInsightsView.topModels.description", { - granularity: t(GRANULARITY_LABELS[granularity]), - metric: t(METRIC_LABELS[shown]), - })} - -
    -
    - setMetric(value as Metric)}> - - {METRICS.map((value) => ( - - {t(METRIC_LABELS[value])} - - ))} - - - setGranularity(value as Granularity)}> - - {GRANULARITIES.map((value) => ( - - {t(GRANULARITY_LABELS[value])} - - ))} - - - setScale(value as Scale)}> - - {SCALES.map((value) => ( - - {value} - - ))} - - -
    -
    - - formatMetric(value, shown)} - totalLabel={t("modelInsights.modelInsightsView.gatewayTotal")} - totalFor={(label) => bucketTotals.get(label) ?? 0} - /> - -
    - - - - {t("modelInsights.modelInsightsView.leaderboard.title")} - - {t("modelInsights.modelInsightsView.leaderboard.description", { metric: t(METRIC_LABELS[shown]) })} - - - -
      - {ranking.slice(0, RANKING_ROWS).map((model, index) => ( - - ))} -
    -
      - {ranking.slice(RANKING_ROWS).map((model, index) => ( - - ))} -
    -
    -
    - - - - {t("modelInsights.modelInsightsView.costPerSession.title")} - {t("modelInsights.modelInsightsView.costPerSession.description")} - - -

    {t("modelInsights.modelInsightsView.costPerSession.hint")}

    -
    -
    -
    - ); -} diff --git a/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.test.ts b/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.test.ts deleted file mode 100644 index fe164fb1b5e..00000000000 --- a/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { buildBucketTotals, buildSeries, DailyMetric, formatMetric, modelOrder, rankModels } from "./modelInsightsData"; - -const row = (over: Partial): DailyMetric => ({ - model_group: "a", - model: "a", - provider: "openai", - date: "2026-01-01", - spend: 0, - prompt_tokens: 0, - completion_tokens: 0, - requests: 0, - successful_requests: 0, - failed_requests: 0, - ...over, -}); - -describe("buildSeries", () => { - const range = { start: "2026-01-01", end: "2026-01-15" }; - - it("sums days into 7-day buckets per model", () => { - const rows = [ - row({ date: "2026-01-01", requests: 1 }), - row({ date: "2026-01-07", requests: 2 }), - row({ date: "2026-01-08", requests: 4 }), - row({ date: "2026-01-02", model_group: "b", requests: 8 }), - ]; - expect(buildSeries(rows, ["a", "b"], "requests", { ...range, granularity: "week" })).toEqual([ - { date: "2026-01-01", a: 3, b: 8 }, - { date: "2026-01-08", a: 4, b: 0 }, - { date: "2026-01-15", a: 0, b: 0 }, - ]); - }); - - it("keeps weeks with no usage as zero instead of dropping them", () => { - const rows = [row({ date: "2026-01-01", requests: 1 }), row({ date: "2026-01-15", requests: 2 })]; - expect( - buildSeries(rows, ["a"], "requests", { ...range, granularity: "week" }).map((week) => [week.date, week.a]), - ).toEqual([ - ["2026-01-01", 1], - ["2026-01-08", 0], - ["2026-01-15", 2], - ]); - }); - - it("gives every day its own bucket with that day's token total", () => { - const rows = [ - row({ date: "2026-01-01", prompt_tokens: 100, completion_tokens: 50 }), - row({ date: "2026-01-01", prompt_tokens: 10, completion_tokens: 5 }), - row({ date: "2026-01-03", prompt_tokens: 7, completion_tokens: 3 }), - ]; - const daily = buildSeries(rows, ["a"], "tokens", { start: "2026-01-01", end: "2026-01-03", granularity: "day" }); - expect(daily).toEqual([ - { date: "2026-01-01", a: 165 }, - { date: "2026-01-02", a: 0 }, - { date: "2026-01-03", a: 10 }, - ]); - }); - - it("starts the chart at the first bucket with usage so bars stay wide on a long range", () => { - const rows = [row({ date: "2026-03-10", requests: 1 }), row({ date: "2026-03-20", requests: 2 })]; - const daily = buildSeries(rows, ["a"], "requests", { start: "2025-03-21", end: "2026-03-20", granularity: "day" }); - expect(daily[0]).toEqual({ date: "2026-02-19", a: 0 }); - expect(daily).toHaveLength(30); - expect(daily.at(-1)).toEqual({ date: "2026-03-20", a: 2 }); - - const early = [row({ date: "2025-12-01", requests: 1 }), ...rows]; - const fromFirstUse = buildSeries(early, ["a"], "requests", { - start: "2025-03-21", - end: "2026-03-20", - granularity: "day", - }); - expect(fromFirstUse[0]).toEqual({ date: "2025-12-01", a: 1 }); - expect(fromFirstUse.at(-1)?.date).toBe("2026-03-20"); - }); - - it("keeps weekly buckets on the original grid when trimming idle weeks", () => { - const rows = [row({ date: "2026-03-20", requests: 3 })]; - const weekly = buildSeries(rows, ["a"], "requests", { - start: "2025-03-21", - end: "2026-03-20", - granularity: "week", - }); - expect(weekly).toHaveLength(12); - expect(weekly.map((week) => (Date.parse(String(week.date)) - Date.parse("2025-03-21")) % (7 * 86_400_000))).toEqual( - Array(12).fill(0), - ); - expect(weekly.at(-1)).toEqual({ date: "2026-03-20", a: 3 }); - }); -}); - -describe("buildBucketTotals", () => { - const total = (date: string, prompt_tokens: number) => ({ - date, - spend: 0, - prompt_tokens, - completion_tokens: 1, - requests: 0, - }); - const totals = [total("2026-01-01", 9), total("2026-01-03", 4), total("2026-01-08", 99)]; - - it("keys each day's gateway-wide total by its own date", () => { - const daily = buildBucketTotals(totals, "tokens", { start: "2026-01-01", end: "2026-01-08", granularity: "day" }); - expect([...daily]).toEqual([ - ["2026-01-01", 10], - ["2026-01-03", 5], - ["2026-01-08", 100], - ]); - }); - - it("sums days into the same week start used by the chart's x-axis", () => { - const window = { start: "2026-01-01", end: "2026-01-08", granularity: "week" } as const; - const weekly = buildBucketTotals(totals, "tokens", window); - expect([...weekly]).toEqual([ - ["2026-01-01", 15], - ["2026-01-08", 100], - ]); - expect([...weekly.keys()]).toEqual(buildSeries([], [], "tokens", window).map((bucket) => bucket.date)); - }); -}); - -describe("modelOrder", () => { - it("orders models by the selected metric, largest first", () => { - const rows = [row({ model_group: "a", spend: 1, requests: 9 }), row({ model_group: "b", spend: 5, requests: 1 })]; - expect(modelOrder(rows, "spend")).toEqual(["b", "a"]); - expect(modelOrder(rows, "requests")).toEqual(["a", "b"]); - }); -}); - -describe("rankModels", () => { - const range = { start: "2026-01-01", end: "2026-01-10" }; - const totals = [row({ model_group: "a", requests: 40 }), row({ model_group: "b", requests: 40 })]; - - it("computes share and the change in share between the first and second half of the range", () => { - const daily = [ - row({ date: "2026-01-01", model_group: "a", requests: 30 }), - row({ date: "2026-01-01", model_group: "b", requests: 10 }), - row({ date: "2026-01-10", model_group: "a", requests: 10 }), - row({ date: "2026-01-10", model_group: "b", requests: 30 }), - ]; - const ranked = rankModels(totals, daily, "requests", range); - expect(ranked.find((m) => m.model_group === "a")).toMatchObject({ share: 50, delta: -50 }); - expect(ranked.find((m) => m.model_group === "b")).toMatchObject({ share: 50, delta: 50 }); - }); - - it("splits at the middle of the range, not the middle of the days that had usage", () => { - const daily = [ - row({ date: "2026-01-01", model_group: "a", requests: 10 }), - row({ date: "2026-01-02", model_group: "b", requests: 10 }), - row({ date: "2026-01-03", model_group: "b", requests: 10 }), - ]; - const ranked = rankModels(totals, daily, "requests", range); - expect(ranked.find((m) => m.model_group === "a")?.delta).toBe(0); - }); - - it("shows no change when one half of the range has no usage to compare against", () => { - const daily = [row({ date: "2026-01-10", model_group: "a", requests: 10 })]; - const ranked = rankModels(totals, daily, "requests", range); - expect(ranked.map((m) => m.delta)).toEqual([0, 0]); - }); -}); - -describe("formatMetric", () => { - it("formats spend as currency and counts compactly", () => { - expect(formatMetric(12.5, "spend")).toBe("$12.50"); - expect(formatMetric(1_500_000, "tokens")).toBe("1.5M"); - }); -}); diff --git a/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.ts b/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.ts deleted file mode 100644 index 9e0aecc9e8e..00000000000 --- a/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.ts +++ /dev/null @@ -1,146 +0,0 @@ -export type Metric = "requests" | "spend" | "tokens"; - -export type ModelMetric = { - model_group: string; - model: string; - provider: string; - spend: number; - prompt_tokens: number; - completion_tokens: number; - requests: number; - successful_requests: number; - failed_requests: number; -}; -export type DailyMetric = ModelMetric & { date: string }; -type Usage = Pick; -export type DailyTotal = Usage & { date: string }; -export type ModelInsightsResponse = { - start_date: string; - end_date: string; - daily: DailyMetric[]; - daily_totals: DailyTotal[]; - top_models: ModelMetric[]; -}; - -export type RankedModel = { model_group: string; provider: string; share: number; delta: number }; -export type Granularity = "day" | "week"; - -const DAY_MS = 86_400_000; -const BUCKET_DAYS: Record = { day: 1, week: 7 }; -const MIN_VISIBLE_BUCKETS: Record = { day: 30, week: 12 }; - -export const metricValue = (row: Usage, metric: Metric) => { - if (metric === "requests") return row.requests; - if (metric === "spend") return row.spend; - return row.prompt_tokens + row.completion_tokens; -}; - -const COMPACT_SPEND_FROM = 10_000; - -export const formatMetric = (value: number, metric: Metric) => { - if (metric === "spend") { - const compact = value >= COMPACT_SPEND_FROM; - const options: Intl.NumberFormatOptions = { - style: "currency", - currency: "USD", - notation: compact ? "compact" : "standard", - maximumFractionDigits: compact ? 1 : 2, - }; - return new Intl.NumberFormat("en-US", options).format(value); - } - return new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(value); -}; - -const toDay = (date: string) => Date.parse(`${date}T00:00:00Z`); -const isoDay = (ms: number) => new Date(ms).toISOString().slice(0, 10); - -export type DateRange = { start: string; end: string }; - -export const modelOrder = (rows: DailyMetric[], metric: Metric) => { - const totals = new Map(); - for (const row of rows) totals.set(row.model_group, (totals.get(row.model_group) ?? 0) + metricValue(row, metric)); - return [...totals.entries()].sort((a, b) => b[1] - a[1]).map(([model]) => model); -}; - -export type SeriesWindow = DateRange & { granularity: Granularity }; - -export const buildSeries = (rows: DailyMetric[], models: string[], metric: Metric, window: SeriesWindow) => { - const bucketMs = BUCKET_DAYS[window.granularity] * DAY_MS; - const origin = toDay(window.start); - const bucketCount = Math.floor((toDay(window.end) - origin) / bucketMs) + 1; - const buckets = Array.from({ length: bucketCount }, (_, index) => ({ - date: isoDay(origin + index * bucketMs), - ...Object.fromEntries(models.map((model) => [model, 0])), - })) as Record[]; - for (const row of rows) { - const bucket = buckets[Math.floor((toDay(row.date) - origin) / bucketMs)]; - if (bucket) bucket[row.model_group] = Number(bucket[row.model_group] ?? 0) + metricValue(row, metric); - } - const firstActive = buckets.findIndex((bucket) => models.some((model) => Number(bucket[model]) > 0)); - const visibleFrom = Math.min( - firstActive === -1 ? buckets.length : firstActive, - buckets.length - MIN_VISIBLE_BUCKETS[window.granularity], - ); - return buckets.slice(Math.max(0, visibleFrom)); -}; - -export const buildBucketTotals = (totals: DailyTotal[], metric: Metric, window: SeriesWindow) => { - const bucketMs = BUCKET_DAYS[window.granularity] * DAY_MS; - const origin = toDay(window.start); - const byBucket = new Map(); - for (const row of totals) { - const bucket = isoDay(origin + Math.floor((toDay(row.date) - origin) / bucketMs) * bucketMs); - byBucket.set(bucket, (byBucket.get(bucket) ?? 0) + metricValue(row, metric)); - } - return byBucket; -}; - -const shareByModel = (rows: { model_group: string; provider: string }[], values: number[]) => { - const totals = new Map(); - rows.forEach((row, index) => { - const current = totals.get(row.model_group) ?? { provider: row.provider, value: 0 }; - totals.set(row.model_group, { provider: row.provider, value: current.value + values[index] }); - }); - const grand = [...totals.values()].reduce((sum, entry) => sum + entry.value, 0); - return { totals, grand }; -}; - -const halfShares = (daily: DailyMetric[], metric: Metric, range: DateRange) => { - const midpoint = isoDay(toDay(range.start) + Math.floor((toDay(range.end) - toDay(range.start)) / 2 + DAY_MS / 2)); - const share = (rows: DailyMetric[]) => { - const { totals, grand } = shareByModel( - rows, - rows.map((row) => metricValue(row, metric)), - ); - return { - hasUsage: grand > 0, - of: (model: string) => (grand === 0 ? 0 : ((totals.get(model)?.value ?? 0) / grand) * 100), - }; - }; - return { - earlier: share(daily.filter((row) => row.date < midpoint)), - later: share(daily.filter((row) => row.date >= midpoint)), - }; -}; - -export const rankModels = ( - rows: ModelMetric[], - daily: DailyMetric[], - metric: Metric, - range: DateRange, -): RankedModel[] => { - const { totals, grand } = shareByModel( - rows, - rows.map((row) => metricValue(row, metric)), - ); - const { earlier, later } = halfShares(daily, metric, range); - const comparable = earlier.hasUsage && later.hasUsage; - return [...totals.entries()] - .sort((a, b) => b[1].value - a[1].value) - .map(([model_group, entry]) => ({ - model_group, - provider: entry.provider, - share: grand === 0 ? 0 : (entry.value / grand) * 100, - delta: comparable ? later.of(model_group) - earlier.of(model_group) : 0, - })); -}; diff --git a/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/page.tsx b/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/page.tsx index 01a673ce10f..55e646a8f21 100644 --- a/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/page.tsx +++ b/litellm/ui/litellm-dashboard/src/app/(dashboard)/model-insights/page.tsx @@ -1,9 +1,16 @@ "use client"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import ModelInsightsView from "./_components/ModelInsightsView"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import LoadingScreen from "@/components/common_components/LoadingScreen"; +import { uiHref } from "@/utils/uiHref"; export default function ModelInsightsPage() { - const { accessToken } = useAuthorized(); - return ; + const router = useRouter(); + + useEffect(() => { + router.replace(`${uiHref("usage")}${window.location.search}${window.location.hash}`); + }, [router]); + + return ; } diff --git a/litellm/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageOverview.tsx b/litellm/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageOverview.tsx index 296b9221d6c..ec8bbeb80aa 100644 --- a/litellm/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageOverview.tsx +++ b/litellm/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageOverview.tsx @@ -1,11 +1,5 @@ "use client"; -/** - * Usage overview: a stat strip, the Model Leaderboard's stacked chart and - * tooltip applied to the usage aggregate, then a ranked share list in - * matching colors. - */ - import { type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import type { DailyData } from "@/components/UsagePage/types"; diff --git a/litellm/ui/litellm-dashboard/src/components/leftnav.tsx b/litellm/ui/litellm-dashboard/src/components/leftnav.tsx index 4a968fa39ec..6c23e5b9a70 100644 --- a/litellm/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/litellm/ui/litellm-dashboard/src/components/leftnav.tsx @@ -50,7 +50,6 @@ import { ShieldCheck, Tags, Terminal, - Trophy, User, Users, Receipt, @@ -333,17 +332,6 @@ const menuGroups: MenuGroup[] = [ icon: , roles: rolesWithCapability("viewGlobalSpend"), }, - { - key: "model-insights", - page: "model-insights", - icon: , - roles: all_admin_roles, - label: ( - - - - ), - }, { key: "cost-optimization", page: "cost-optimization", diff --git a/litellm/ui/litellm-dashboard/src/components/page_metadata.ts b/litellm/ui/litellm-dashboard/src/components/page_metadata.ts index d818f33c4d9..09992fa15ac 100644 --- a/litellm/ui/litellm-dashboard/src/components/page_metadata.ts +++ b/litellm/ui/litellm-dashboard/src/components/page_metadata.ts @@ -24,7 +24,6 @@ export const pageDescriptions: Record = { new_usage: "shell.pageDescriptions.newUsage", alerts: "shell.pageDescriptions.alerts", "cost-optimization": "shell.pageDescriptions.costOptimization", - "model-insights": "shell.pageDescriptions.modelInsights", logs: "shell.pageDescriptions.logs", lens: "shell.pageDescriptions.lens", "guardrails-monitor": "shell.pageDescriptions.guardrailsMonitor", diff --git a/litellm/ui/litellm-dashboard/src/components/shared/charts/StackedUsageChart.tsx b/litellm/ui/litellm-dashboard/src/components/shared/charts/StackedUsageChart.tsx index a0fb831c1c2..1602b5e3850 100644 --- a/litellm/ui/litellm-dashboard/src/components/shared/charts/StackedUsageChart.tsx +++ b/litellm/ui/litellm-dashboard/src/components/shared/charts/StackedUsageChart.tsx @@ -9,7 +9,6 @@ import { cn } from "@/lib/cva.config"; const LOG_SCALE_DOMAIN: [number, "auto"] = [1, "auto"]; const LINEAR_SCALE_DOMAIN: [number, "auto"] = [0, "auto"]; -/** The Model Leaderboard palette; shared so the usage page stacks in the same colors. */ export const STACKED_USAGE_PALETTE = [ "#ec4899", "#a855f7", diff --git a/litellm/ui/litellm-dashboard/src/i18n/locales/en.json b/litellm/ui/litellm-dashboard/src/i18n/locales/en.json index 02fb32d18d8..30439a3fb25 100644 --- a/litellm/ui/litellm-dashboard/src/i18n/locales/en.json +++ b/litellm/ui/litellm-dashboard/src/i18n/locales/en.json @@ -8131,7 +8131,6 @@ "toolPolicies": "Tool Policies", "usage": "Usage", "alerts": "Alerts", - "modelLeaderboard": "Model Leaderboard", "costOptimization": "Cost Optimization", "logs": "Logs", "lens": "Lens", @@ -8178,7 +8177,6 @@ "newUsage": "View usage analytics and metrics", "alerts": "Review and resolve model deprecation alerts across keys and teams", "costOptimization": "Track and configure cost-saving features: prompt compression, caching, and auto routing", - "modelInsights": "Model Leaderboard: compare usage, spend, tokens, and task mix across this gateway", "logs": "Access request and response logs", "lens": "Review agent activity and investigate patterns with supporting evidence", "guardrailsMonitor": "Monitor guardrail performance and view logs", @@ -19967,38 +19965,6 @@ "deleted": "Alias deleted successfully" } }, - "modelInsights": { - "modelInsightsView": { - "granularity": { - "day": "Daily", - "week": "Weekly" - }, - "metrics": { - "requests": "requests", - "spend": "spend", - "tokens": "tokens" - }, - "byProvider": "by {{provider}}", - "loadError": "Could not load model insights", - "title": "Model Leaderboard", - "description": "See which models your gateway used from {{start}} through {{end}}", - "topModels": { - "title": "Top models", - "description": "{{granularity}} {{metric}} across your gateway" - }, - "bucketSize": "Bucket size", - "gatewayTotal": "Gateway total", - "leaderboard": { - "title": "Leaderboard", - "description": "Share of {{metric}}, with the change between the first and second half of the period" - }, - "costPerSession": { - "title": "Cost per session", - "description": "Session cost is not estimated from request counts", - "hint": "Add a stable session_id to requests to unlock accurate session-level model comparisons in a future bounded session rollup" - } - } - }, "transformRequest": { "transformRequestPanel": { "title": "Playground",