diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectSpendBreakdown.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectSpendBreakdown.test.tsx
new file mode 100644
index 00000000000..4e67c870de0
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectSpendBreakdown.test.tsx
@@ -0,0 +1,66 @@
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+import ProjectSpendBreakdown from "./ProjectSpendBreakdown";
+import type { ProjectSpendRow } from "./projectUsageAggregations";
+
+vi.mock("@/components/shared/chart_loader", () => ({
+ ChartLoader: ({ isDateChanging }: { isDateChanging: boolean }) => (
+
{isDateChanging ? "Processing date selection..." : "Loading chart data..."}
+ ),
+}));
+
+const mockProjectSpend: ProjectSpendRow[] = [
+ {
+ project_id: "project-alpha",
+ project_alias: "Project Alpha",
+ spend: 150.5,
+ requests: 100,
+ successful_requests: 95,
+ failed_requests: 5,
+ tokens: 50000,
+ },
+ {
+ project_id: "project-beta",
+ project_alias: "Project Beta",
+ spend: 200.75,
+ requests: 120,
+ successful_requests: 115,
+ failed_requests: 5,
+ tokens: 75000,
+ },
+];
+
+describe("ProjectSpendBreakdown", () => {
+ it("displays the title", () => {
+ render();
+ expect(screen.getByText("Spend by Project")).toBeInTheDocument();
+ });
+
+ it("shows the loader instead of chart content while loading", () => {
+ render();
+ expect(screen.getByTestId("chart-loader")).toBeInTheDocument();
+ expect(screen.queryByText("No project usage data")).not.toBeInTheDocument();
+ });
+
+ it("displays table headers", () => {
+ render();
+ expect(screen.getByText("Project")).toBeInTheDocument();
+ expect(screen.getByText("Spend")).toBeInTheDocument();
+ expect(screen.getByText("Successful")).toBeInTheDocument();
+ expect(screen.getByText("Failed")).toBeInTheDocument();
+ expect(screen.getByText("Tokens")).toBeInTheDocument();
+ });
+
+ it("displays each project's alias and formatted spend in the table", () => {
+ render();
+ expect(screen.getAllByText("Project Alpha").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("Project Beta").length).toBeGreaterThan(0);
+ expect(screen.getByText("$150.50")).toBeInTheDocument();
+ expect(screen.getByText("$200.75")).toBeInTheDocument();
+ });
+
+ it("shows the empty message when no project has usage", () => {
+ render();
+ expect(screen.getByText("No project usage data")).toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectSpendBreakdown.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectSpendBreakdown.tsx
new file mode 100644
index 00000000000..e7e41126861
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectSpendBreakdown.tsx
@@ -0,0 +1,84 @@
+import React from "react";
+import type { ColumnDef } from "@tanstack/react-table";
+
+import { DonutChart } from "@/components/shared/charts";
+import { DataTable } from "@/components/shared/DataTable";
+import { MoneyCell } from "@/components/shared/table_cells";
+import { ChartLoader } from "@/components/shared/chart_loader";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { formatNumberWithCommas } from "@/utils/dataUtils";
+
+import type { ProjectSpendRow } from "./projectUsageAggregations";
+
+interface ProjectSpendBreakdownProps {
+ loading: boolean;
+ isDateChanging: boolean;
+ projectSpend: ProjectSpendRow[];
+}
+
+const columns: ColumnDef[] = [
+ {
+ header: "Project",
+ accessorKey: "project_alias",
+ },
+ {
+ header: "Spend",
+ accessorKey: "spend",
+ meta: { numeric: true },
+ cell: ({ row }) => ,
+ },
+ {
+ header: "Successful",
+ accessorKey: "successful_requests",
+ meta: { numeric: true, className: "text-success" },
+ cell: ({ row }) => row.original.successful_requests.toLocaleString(),
+ },
+ {
+ header: "Failed",
+ accessorKey: "failed_requests",
+ meta: { numeric: true, className: "text-destructive" },
+ cell: ({ row }) => row.original.failed_requests.toLocaleString(),
+ },
+ {
+ header: "Tokens",
+ accessorKey: "tokens",
+ meta: { numeric: true },
+ cell: ({ row }) => row.original.tokens.toLocaleString(),
+ },
+];
+
+const ProjectSpendBreakdown: React.FC = ({ loading, isDateChanging, projectSpend }) => (
+
+
+ Spend by Project
+
+
+ {loading ? (
+
+ ) : (
+
+ `$${formatNumberWithCommas(value, 2)}`}
+ colors={["cyan"]}
+ showLabel
+ startAngle={90}
+ endAngle={-270}
+ />
+ row.project_id}
+ noDataMessage="No project usage data"
+ size="compact"
+ />
+
+ )}
+
+
+);
+
+export default ProjectSpendBreakdown;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.test.tsx
new file mode 100644
index 00000000000..1e9e0dffefb
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.test.tsx
@@ -0,0 +1,87 @@
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { renderWithProviders, screen, testQueryClient, waitFor } from "@/../tests/test-utils";
+import * as networking from "@/components/networking";
+
+import ProjectUsage from "./ProjectUsage";
+import type { EntityList } from "../EntityUsage/EntityUsage";
+
+vi.mock("@/components/networking", () => ({
+ projectDailyActivityCall: vi.fn(),
+}));
+
+const PROJECT_LIST: EntityList[] = [
+ { label: "Project Alpha", value: "project-alpha" },
+ { label: "Project Beta", value: "project-beta" },
+];
+
+const DATE_VALUE = { from: new Date("2026-09-01"), to: new Date("2026-09-08") };
+
+describe("ProjectUsage", () => {
+ const mockProjectDailyActivityCall = vi.mocked(networking.projectDailyActivityCall);
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ testQueryClient.clear();
+ });
+
+ it("shows an enterprise upsell and never calls the API when the caller is not a premium user", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Project Usage is an Enterprise feature")).toBeInTheDocument();
+ expect(mockProjectDailyActivityCall).not.toHaveBeenCalled();
+ });
+
+ it("prompts for a project before fetching anything", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Select at least one project above to view its usage.")).toBeInTheDocument();
+ expect(mockProjectDailyActivityCall).not.toHaveBeenCalled();
+ });
+
+ it("fetches and renders the picked project's daily spend", async () => {
+ mockProjectDailyActivityCall.mockResolvedValue({
+ start_date: "2026-09-01",
+ end_date: "2026-09-08",
+ results: [
+ {
+ date: "2026-09-01",
+ project_id: "project-alpha",
+ project_alias: "Project Alpha",
+ spend: 12.5,
+ prompt_tokens: 100,
+ completion_tokens: 50,
+ total_tokens: 150,
+ api_requests: 4,
+ successful_requests: 3,
+ failed_requests: 1,
+ },
+ ],
+ });
+
+ const user = userEvent.setup();
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getByRole("combobox"));
+ await user.click(screen.getByRole("option", { name: "Project Alpha" }));
+
+ await waitFor(() => expect(mockProjectDailyActivityCall).toHaveBeenCalledTimes(1));
+ expect(mockProjectDailyActivityCall).toHaveBeenCalledWith(
+ "test-token",
+ DATE_VALUE.from,
+ DATE_VALUE.to,
+ ["project-alpha"],
+ );
+
+ await waitFor(() => expect(screen.getAllByText("$12.50").length).toBeGreaterThan(0));
+ expect(screen.getAllByText("4").length).toBeGreaterThan(0);
+ expect(screen.getAllByText("Project Alpha").length).toBeGreaterThan(0);
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.tsx
new file mode 100644
index 00000000000..b4f06af67c8
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/ProjectUsage.tsx
@@ -0,0 +1,158 @@
+import React, { useMemo, useState } from "react";
+import { keepPreviousData, useQuery } from "@tanstack/react-query";
+import { Info } from "lucide-react";
+
+import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
+import { BarChart } from "@/components/shared/charts";
+import { ChartLoader } from "@/components/shared/chart_loader";
+import { MultiSelect, type MultiSelectOption } from "@/components/shared/MultiSelect";
+import type { DateRangePickerValue } from "@/components/shared/date_picker_types";
+import { Card as ShadcnCard, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { projectDailyActivityCall } from "@/components/networking";
+import { valueFormatterSpend } from "@/components/UsagePage/utils/value_formatters";
+
+import { buildSummaryTiles, type SummaryTile } from "../EntityUsage/entityUsageSummary";
+import type { EntityList } from "../EntityUsage/EntityUsage";
+import ProjectSpendBreakdown from "./ProjectSpendBreakdown";
+import { buildDailySpendSeries, buildProjectSpendBreakdown, summarizeProjectUsage } from "./projectUsageAggregations";
+
+interface ProjectUsageProps {
+ accessToken: string | null;
+ projectList: EntityList[] | null;
+ dateValue: DateRangePickerValue;
+ premiumUser: boolean;
+}
+
+const renderSummaryTile = ({ title, value, className }: SummaryTile) => (
+
+
+ {title}
+ {value}
+
+
+);
+
+const ProjectUsage: React.FC = ({ accessToken, projectList, dateValue, premiumUser }) => {
+ const [selectedProjectIds, setSelectedProjectIds] = useState([]);
+
+ const startTime = useMemo(() => (dateValue.from ? new Date(dateValue.from) : null), [dateValue.from]);
+ const endTime = useMemo(() => (dateValue.to ? new Date(dateValue.to) : null), [dateValue.to]);
+
+ const projectOptions = useMemo(
+ () => (projectList ?? []).map((project) => ({ label: project.label || project.value, value: project.value })),
+ [projectList],
+ );
+
+ const hasSelection = selectedProjectIds.length > 0;
+ const hasDateRange = !!startTime && !!endTime;
+ const hasRequestWindow = premiumUser && !!accessToken && hasDateRange;
+ const enabled = hasRequestWindow && hasSelection;
+
+ const queryOptions = {
+ queryKey: ["project-daily-activity", selectedProjectIds, startTime?.toISOString(), endTime?.toISOString()],
+ queryFn: () => projectDailyActivityCall(accessToken as string, startTime as Date, endTime as Date, selectedProjectIds),
+ enabled,
+ placeholderData: keepPreviousData,
+ };
+ const { data, isFetching, isPlaceholderData } = useQuery(queryOptions);
+
+ if (!premiumUser) {
+ return (
+
+ Project Usage is an Enterprise feature
+
+ Filtering usage by project requires a LiteLLM Enterprise license. Get a 7 day trial at{" "}
+
+ litellm.ai/enterprise
+
+ .
+
+
+ );
+ }
+
+ const rows = data?.results ?? [];
+ const summary = summarizeProjectUsage(rows);
+ const dailySpend = buildDailySpendSeries(rows);
+ const projectBreakdown = buildProjectSpendBreakdown(rows);
+ const isLoadingRows = isFetching && !isPlaceholderData;
+
+ return (
+
+
+
+
+
+
Projects
+
+ } />
+ Select one or more projects to compare their usage
+
+
+
+
+
+
+
+ {!hasSelection ? (
+
+
+
+
+ Select at least one project above to view its usage.
+
+
+
+
+ ) : (
+ <>
+
+
+
+ Project Spend Overview
+
+ {buildSummaryTiles(summary, false).map(renderSummaryTile)}
+
+
+
+
+
+
+
+
+ Daily Spend
+
+
+ {isLoadingRows ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ >
+ )}
+
+ );
+};
+
+export default ProjectUsage;
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts
new file mode 100644
index 00000000000..090ee973dd4
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.test.ts
@@ -0,0 +1,106 @@
+import { describe, expect, it } from "vitest";
+
+import type { ProjectDailySpendRow } from "@/components/networking";
+
+import { buildDailySpendSeries, buildProjectSpendBreakdown, summarizeProjectUsage } from "./projectUsageAggregations";
+
+const row = (overrides: Partial = {}): ProjectDailySpendRow => ({
+ date: "2026-09-01",
+ project_id: "project-alpha",
+ project_alias: "Project Alpha",
+ spend: 1,
+ prompt_tokens: 10,
+ completion_tokens: 5,
+ total_tokens: 15,
+ api_requests: 3,
+ successful_requests: 2,
+ failed_requests: 1,
+ ...overrides,
+});
+
+describe("summarizeProjectUsage", () => {
+ it("returns all-zero totals for no rows", () => {
+ expect(summarizeProjectUsage([])).toEqual({
+ total_spend: 0,
+ total_api_requests: 0,
+ total_successful_requests: 0,
+ total_failed_requests: 0,
+ total_tokens: 0,
+ });
+ });
+
+ it("sums spend, requests, and tokens across every row regardless of project or date", () => {
+ const projectBetaRow: Partial = {
+ project_id: "project-beta",
+ date: "2026-09-02",
+ spend: 2.25,
+ api_requests: 4,
+ successful_requests: 4,
+ failed_requests: 0,
+ total_tokens: 40,
+ };
+ const rows = [row({ spend: 1.5 }), row(projectBetaRow)];
+
+ expect(summarizeProjectUsage(rows)).toEqual({
+ total_spend: 3.75,
+ total_api_requests: 7,
+ total_successful_requests: 6,
+ total_failed_requests: 1,
+ total_tokens: 55,
+ });
+ });
+});
+
+describe("buildDailySpendSeries", () => {
+ it("sums same-day spend across projects into a single point", () => {
+ const rows = [row(), row({ project_id: "project-beta", spend: 2 })];
+
+ expect(buildDailySpendSeries(rows)).toEqual([{ date: "2026-09-01", spend: 3 }]);
+ });
+
+ it("sorts distinct dates oldest first regardless of input order", () => {
+ const rows = [row({ date: "2026-09-03", spend: 3 }), row(), row({ date: "2026-09-02", spend: 2 })];
+
+ expect(buildDailySpendSeries(rows).map((point) => point.date)).toEqual(["2026-09-01", "2026-09-02", "2026-09-03"]);
+ });
+});
+
+describe("buildProjectSpendBreakdown", () => {
+ it("sums a project's spend and tokens across every day into a single row", () => {
+ const secondDayRow: Partial = {
+ date: "2026-09-02",
+ spend: 2,
+ total_tokens: 20,
+ successful_requests: 1,
+ failed_requests: 2,
+ };
+ const rows = [row({ total_tokens: 10, api_requests: 2, failed_requests: 0 }), row(secondDayRow)];
+
+ expect(buildProjectSpendBreakdown(rows)).toEqual([
+ {
+ project_id: "project-alpha",
+ project_alias: "Project Alpha",
+ spend: 3,
+ requests: 5,
+ successful_requests: 3,
+ failed_requests: 2,
+ tokens: 30,
+ },
+ ]);
+ });
+
+ it("sorts projects by spend descending", () => {
+ const rows = [
+ row({ project_id: "project-cheap", project_alias: "Cheap" }),
+ row({ project_id: "project-costly", project_alias: "Costly", spend: 9 }),
+ ];
+
+ expect(buildProjectSpendBreakdown(rows).map((r) => r.project_id)).toEqual(["project-costly", "project-cheap"]);
+ });
+
+ it("falls back to the project id when the project has no alias", () => {
+ const rows = [row({ project_id: "project-untitled", project_alias: null })];
+
+ expect(buildProjectSpendBreakdown(rows)[0].project_alias).toBe("project-untitled");
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts
new file mode 100644
index 00000000000..8a0422fbadf
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ProjectUsage/projectUsageAggregations.ts
@@ -0,0 +1,86 @@
+import type { ProjectDailySpendRow } from "@/components/networking";
+
+/** One row per project, summed across the whole range: the shape the breakdown table and donut chart render. */
+export interface ProjectSpendRow extends Record {
+ project_id: string;
+ project_alias: string;
+ spend: number;
+ requests: number;
+ successful_requests: number;
+ failed_requests: number;
+ tokens: number;
+}
+
+/** One point per day, spend summed across every selected project: the shape the daily spend chart renders. */
+export interface DailyProjectSpendPoint extends Record {
+ date: string;
+ spend: number;
+}
+
+export interface ProjectUsageSummary {
+ total_spend: number;
+ total_api_requests: number;
+ total_successful_requests: number;
+ total_failed_requests: number;
+ total_tokens: number;
+}
+
+const EMPTY_SUMMARY: ProjectUsageSummary = {
+ total_spend: 0,
+ total_api_requests: 0,
+ total_successful_requests: 0,
+ total_failed_requests: 0,
+ total_tokens: 0,
+};
+
+/** Aggregate totals across every row, for the summary tiles. */
+export const summarizeProjectUsage = (rows: ProjectDailySpendRow[]): ProjectUsageSummary =>
+ rows.reduce(
+ (totals, row) => ({
+ total_spend: totals.total_spend + row.spend,
+ total_api_requests: totals.total_api_requests + row.api_requests,
+ total_successful_requests: totals.total_successful_requests + row.successful_requests,
+ total_failed_requests: totals.total_failed_requests + row.failed_requests,
+ total_tokens: totals.total_tokens + row.total_tokens,
+ }),
+ EMPTY_SUMMARY,
+ );
+
+/** One point per day, spend summed across every selected project, sorted oldest first. */
+export const buildDailySpendSeries = (rows: ProjectDailySpendRow[]): DailyProjectSpendPoint[] => {
+ const spendByDate = new Map();
+ rows.forEach((row) => {
+ spendByDate.set(row.date, (spendByDate.get(row.date) ?? 0) + row.spend);
+ });
+ return Array.from(spendByDate, ([date, spend]) => ({ date, spend })).sort((a, b) => a.date.localeCompare(b.date));
+};
+
+/**
+ * One row per project, spend/tokens/requests summed across the whole range, sorted by
+ * spend descending like every other "top X" breakdown on the usage page.
+ */
+export const buildProjectSpendBreakdown = (rows: ProjectDailySpendRow[]): ProjectSpendRow[] => {
+ const byProject = new Map();
+ rows.forEach((row) => {
+ const existing = byProject.get(row.project_id);
+ if (existing) {
+ existing.spend += row.spend;
+ existing.requests += row.api_requests;
+ existing.successful_requests += row.successful_requests;
+ existing.failed_requests += row.failed_requests;
+ existing.tokens += row.total_tokens;
+ return;
+ }
+ const newRow: ProjectSpendRow = {
+ project_id: row.project_id,
+ project_alias: row.project_alias || row.project_id,
+ spend: row.spend,
+ requests: row.api_requests,
+ successful_requests: row.successful_requests,
+ failed_requests: row.failed_requests,
+ tokens: row.total_tokens,
+ };
+ byProject.set(row.project_id, newRow);
+ });
+ return Array.from(byProject.values()).sort((a, b) => b.spend - a.spend);
+};
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx
index a92d1209567..55aae0940a0 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx
@@ -20,6 +20,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
import { useCustomers } from "@/app/(dashboard)/hooks/customers/useCustomers";
+import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useIsOrgAdmin from "@/app/(dashboard)/hooks/useIsOrgAdmin";
import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser";
@@ -59,6 +60,7 @@ import {
import EndpointUsage from "./EndpointUsage/EndpointUsage";
import EntityUsage, { EntityList } from "./EntityUsage/EntityUsage";
import ModelViewToggle, { ModelViewType } from "./ModelViewToggle";
+import ProjectUsage from "./ProjectUsage/ProjectUsage";
import SpendByProvider from "./EntityUsage/SpendByProvider";
import { TOP_MODEL_LIMITS } from "./EntityUsage/TopModelView";
import TopKeyView from "@/components/UsagePage/components/EntityUsage/TopKeyView";
@@ -102,12 +104,14 @@ const UsagePage: React.FC = ({ teams, organizations }) => {
// filter reads as loading rather than as a range with no customers.
const { data: customers } = useCustomers();
const { data: agentsResponse } = useAgents();
+ const { data: projectsResponse } = useProjects();
const { data: currentUser } = useCurrentUser();
const isAdmin = all_admin_roles.includes(userRole || "");
const canViewTagUsage = isAdmin || internalUserRoles.includes(userRole || "");
const isOrgAdmin = useIsOrgAdmin();
const canViewOrganizationUsage = hasCapability(userRole, "viewOrganizationUsage", isOrgAdmin);
const canViewAgentUsage = hasCapability(userRole, "viewAgentUsage");
+ const canViewProjectUsage = hasCapability(userRole, "viewProjectUsage");
// For admins: null means global view (all users), a string means filter by that user
// For non-admins: always set to their own user ID
@@ -935,6 +939,21 @@ const UsagePage: React.FC = ({ teams, organizations }) => {
/>
)}
+ {/* Project Usage Panel */}
+ {usageView === "project" && canViewProjectUsage && (
+ ({
+ label: project.project_alias || project.project_id,
+ value: project.project_id,
+ })) || null
+ }
+ dateValue={dateValue}
+ premiumUser={premiumUser}
+ />
+ )}
+
{/* Customer Usage Panel */}
{usageView === "customer" && (
{
expect(offers(container, "Tag Usage")).toBe(false);
});
- it.each(["Organization Usage", "Agent Usage (A2A)"])("should show %s to an admin", async (optionName) => {
- const user = userEvent.setup();
- const { container } = render();
+ it.each(["Organization Usage", "Agent Usage (A2A)", "Project Usage"])(
+ "should show %s to an admin",
+ async (optionName) => {
+ const user = userEvent.setup();
+ const { container } = render();
- await openMenu(user);
- expect(offers(container, optionName)).toBe(true);
- });
+ await openMenu(user);
+ expect(offers(container, optionName)).toBe(true);
+ },
+ );
- it.each(["Organization Usage", "Agent Usage (A2A)"])("should hide %s from an internal user", async (optionName) => {
+ it.each(["Organization Usage", "Agent Usage (A2A)", "Project Usage"])("should hide %s from an internal user", async (optionName) => {
const user = userEvent.setup();
const { container } = render(
,
@@ -86,6 +89,7 @@ describe("UsageViewSelect", () => {
it.each([
["Organization Usage", true],
["Agent Usage (A2A)", false],
+ ["Project Usage", false],
] as const)("should offer %s to an org admin: %s", async (optionName, expected) => {
const user = userEvent.setup();
const { container } = render(
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx
index ea21a2155c1..a79267860ea 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx
@@ -1,4 +1,4 @@
-import { BarChart3, Bot, Building2, Globe, LineChart, ShoppingCart, Tags, User, Users } from "lucide-react";
+import { BarChart3, Bot, Building2, Folder, Globe, LineChart, ShoppingCart, Tags, User, Users } from "lucide-react";
import React from "react";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
@@ -9,6 +9,7 @@ export type UsageOption =
| "my-usage"
| "organization"
| "team"
+ | "project"
| "customer"
| "tag"
| "agent"
@@ -68,6 +69,13 @@ const OPTIONS: OptionConfig[] = [
description: "View usage by team",
icon: ,
},
+ {
+ value: "project",
+ label: "Project Usage",
+ description: "View usage by project",
+ icon: ,
+ capability: "viewProjectUsage",
+ },
{
value: "customer",
label: "Customer Usage",
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index 169b279bf79..21c71f9a774 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -1623,6 +1623,38 @@ export const agentDailyActivityCall = async (
});
};
+export type ProjectDailySpendResponse = components["schemas"]["ProjectDailySpendResponse"];
+export type ProjectDailySpendRow = components["schemas"]["ProjectDailySpendRow"];
+
+export const projectDailyActivityCall = async (
+ accessToken: string,
+ startTime: Date,
+ endTime: Date,
+ projectIds: string[],
+): Promise => {
+ /**
+ * Get daily spend per project from /project/daily/activity.
+ *
+ * Unlike team/user/tag/agent, there is no daily-aggregated project spend
+ * table, so this scans spend logs directly and returns the whole range in
+ * one response instead of paginating. project_ids is comma-joined because
+ * the endpoint takes a single string, not repeated query params.
+ */
+ try {
+ return await apiClient.get(`/project/daily/activity`, {
+ accessToken,
+ query: {
+ project_ids: projectIds.join(","),
+ start_date: formatDate(startTime),
+ end_date: formatDate(endTime),
+ },
+ });
+ } catch (error) {
+ console.error("Failed to fetch project daily activity:", error);
+ throw error;
+ }
+};
+
export const getOnboardingCredentials = async (inviteUUID: string) => {
/**
* Get all models on proxy
diff --git a/ui/litellm-dashboard/src/utils/capabilities.test.ts b/ui/litellm-dashboard/src/utils/capabilities.test.ts
index 25314a3f46a..ad0b4eda3ce 100644
--- a/ui/litellm-dashboard/src/utils/capabilities.test.ts
+++ b/ui/litellm-dashboard/src/utils/capabilities.test.ts
@@ -24,6 +24,7 @@ const ADMIN_ONLY_CAPABILITIES: Capability[] = [
"viewPrompts",
"viewOrganizationUsage",
"viewAgentUsage",
+ "viewProjectUsage",
];
const PROXY_ADMIN_ONLY_PAGE_CAPABILITIES: Capability[] = [
diff --git a/ui/litellm-dashboard/src/utils/capabilities.ts b/ui/litellm-dashboard/src/utils/capabilities.ts
index ec41531f2ac..5adb88d0f46 100644
--- a/ui/litellm-dashboard/src/utils/capabilities.ts
+++ b/ui/litellm-dashboard/src/utils/capabilities.ts
@@ -10,6 +10,7 @@ const CAPABILITY_ROLES = {
viewPrompts: all_admin_roles,
viewOrganizationUsage: all_admin_roles,
viewAgentUsage: all_admin_roles,
+ viewProjectUsage: all_admin_roles,
viewGlobalSpend: proxyAdminOnlyRoles,
viewWorkflowRuns: proxyAdminOnlyRoles,
viewMemory: proxyAdminOnlyRoles,