Skip to content

Commit 6825593

Browse files
authored
Merge pull request #17181 from BerriAI/litellm_ui_org_usage_2
[Feature] UI - Organization Usage in Usage Tab (#16614)
2 parents 688066f + 40db452 commit 6825593

File tree

13 files changed

+286
-141
lines changed

13 files changed

+286
-141
lines changed

ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const UsagePage = () => {
1515
userID={userId}
1616
teams={teams ?? []}
1717
premiumUser={premiumUser}
18+
organizations={[]}
1819
/>
1920
);
2021
};

ui/litellm-dashboard/src/app/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ export default function CreateKeyPage() {
480480
userRole={userRole}
481481
accessToken={accessToken}
482482
teams={(teams as Team[]) ?? []}
483+
organizations={(organizations as Organization[]) ?? []}
483484
premiumUser={premiumUser}
484485
/>
485486
) : (

ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
2222
const [exportScope, setExportScope] = useState<ExportScope>("daily");
2323
const [isExporting, setIsExporting] = useState(false);
2424

25-
const entityLabel = entityType === "tag" ? "Tag" : "Team";
25+
const entityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1);
2626
const modalTitle = customTitle || `Export ${entityLabel} Usage`;
2727

2828
const handleExportCSV = () => {

ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { ExportScope } from "./types";
55
interface ExportTypeSelectorProps {
66
value: ExportScope;
77
onChange: (value: ExportScope) => void;
8-
entityType: "tag" | "team";
8+
entityType: "tag" | "team" | "organization";
99
}
1010

1111
const ExportTypeSelector: React.FC<ExportTypeSelectorProps> = ({ value, onChange, entityType }) => {
@@ -36,4 +36,3 @@ const ExportTypeSelector: React.FC<ExportTypeSelectorProps> = ({ value, onChange
3636
};
3737

3838
export default ExportTypeSelector;
39-

ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { EntitySpendData } from "./types";
77

88
interface UsageExportHeaderProps {
99
dateValue: DateRangePickerValue;
10-
entityType: "tag" | "team";
10+
entityType: "tag" | "team" | "organization";
1111
spendData: EntitySpendData;
1212
// Optional filter props
1313
showFilters?: boolean;

ui/litellm-dashboard/src/components/EntityUsageExport/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface EntitySpendData {
1717
export interface EntityUsageExportModalProps {
1818
isOpen: boolean;
1919
onClose: () => void;
20-
entityType: "tag" | "team";
20+
entityType: "tag" | "team" | "organization";
2121
spendData: EntitySpendData;
2222
dateRange: DateRangePickerValue;
2323
selectedFilters: string[];
@@ -59,4 +59,3 @@ export interface EntityBreakdown {
5959
id: string;
6060
};
6161
}
62-

ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const generateDailyData = (spendData: EntitySpendData, entityLabel: strin
5050
[entityLabel]: data.metadata?.team_alias || entity,
5151
[`${entityLabel} ID`]: entity,
5252
"Spend ($)": formatNumberWithCommas(data.metrics.spend, 4),
53-
"Requests": data.metrics.api_requests,
53+
Requests: data.metrics.api_requests,
5454
"Successful Requests": data.metrics.successful_requests,
5555
"Failed Requests": data.metrics.failed_requests,
5656
"Total Tokens": data.metrics.total_tokens,
@@ -109,9 +109,9 @@ export const generateDailyWithModelsData = (spendData: EntitySpendData, entityLa
109109
[`${entityLabel} ID`]: entity,
110110
Model: model,
111111
"Spend ($)": formatNumberWithCommas(metrics.spend, 4),
112-
"Requests": metrics.requests,
113-
"Successful": metrics.successful,
114-
"Failed": metrics.failed,
112+
Requests: metrics.requests,
113+
Successful: metrics.successful,
114+
Failed: metrics.failed,
115115
"Total Tokens": metrics.tokens,
116116
});
117117
});
@@ -137,7 +137,7 @@ export const generateExportData = (
137137
};
138138

139139
export const generateMetadata = (
140-
entityType: "tag" | "team",
140+
entityType: "tag" | "team" | "organization",
141141
dateRange: { from?: Date; to?: Date },
142142
selectedFilters: string[],
143143
exportScope: ExportScope,
@@ -159,4 +159,3 @@ export const generateMetadata = (
159159
total_tokens: spendData.metadata.total_tokens,
160160
},
161161
});
162-
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Organization } from "../networking";
22

33
export const defaultOrg = {
4-
organization_id: null,
4+
organization_id: "default_organization",
55
organization_alias: "Default Organization",
66
} as Organization;

ui/litellm-dashboard/src/components/entity_usage.test.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
33
import EntityUsage from "./entity_usage";
44
import * as networking from "./networking";
55

6-
// Polyfill ResizeObserver for test environment
76
beforeAll(() => {
87
if (typeof window !== "undefined" && !window.ResizeObserver) {
98
window.ResizeObserver = class ResizeObserver {
@@ -18,6 +17,7 @@ beforeAll(() => {
1817
vi.mock("./networking", () => ({
1918
tagDailyActivityCall: vi.fn(),
2019
teamDailyActivityCall: vi.fn(),
20+
organizationDailyActivityCall: vi.fn(),
2121
}));
2222

2323
// Mock the child components to simplify testing
@@ -41,6 +41,7 @@ vi.mock("./EntityUsageExport", () => ({
4141
describe("EntityUsage", () => {
4242
const mockTagDailyActivityCall = vi.mocked(networking.tagDailyActivityCall);
4343
const mockTeamDailyActivityCall = vi.mocked(networking.teamDailyActivityCall);
44+
const mockOrganizationDailyActivityCall = vi.mocked(networking.organizationDailyActivityCall);
4445

4546
const mockSpendData = {
4647
results: [
@@ -126,8 +127,10 @@ describe("EntityUsage", () => {
126127
beforeEach(() => {
127128
mockTagDailyActivityCall.mockClear();
128129
mockTeamDailyActivityCall.mockClear();
130+
mockOrganizationDailyActivityCall.mockClear();
129131
mockTagDailyActivityCall.mockResolvedValue(mockSpendData);
130132
mockTeamDailyActivityCall.mockResolvedValue(mockSpendData);
133+
mockOrganizationDailyActivityCall.mockResolvedValue(mockSpendData);
131134
});
132135

133136
it("should render with tag entity type and display spend metrics", async () => {
@@ -164,6 +167,19 @@ describe("EntityUsage", () => {
164167
});
165168
});
166169

170+
it("should render with organization entity type and call organization API", async () => {
171+
const { getByText, getAllByText } = render(<EntityUsage {...defaultProps} entityType="organization" />);
172+
173+
await waitFor(() => {
174+
expect(mockOrganizationDailyActivityCall).toHaveBeenCalled();
175+
});
176+
177+
expect(getByText("Organization Spend Overview")).toBeInTheDocument();
178+
179+
const spendElements = getAllByText("$100.50");
180+
expect(spendElements.length).toBeGreaterThan(0);
181+
});
182+
167183
it("should switch between tabs", async () => {
168184
render(<EntityUsage {...defaultProps} />);
169185

ui/litellm-dashboard/src/components/entity_usage.tsx

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from "@tremor/react";
2424
import { ActivityMetrics, processActivityData } from "./activity_metrics";
2525
import { DailyData, BreakdownMetrics, KeyMetricWithMetadata, EntityMetricWithMetadata, TagUsage } from "./usage/types";
26-
import { tagDailyActivityCall, teamDailyActivityCall } from "./networking";
26+
import { organizationDailyActivityCall, tagDailyActivityCall, teamDailyActivityCall } from "./networking";
2727
import TopKeyView from "./top_key_view";
2828
import { formatNumberWithCommas } from "@/utils/dataUtils";
2929
import { valueFormatterSpend } from "./usage/utils/value_formatters";
@@ -68,7 +68,7 @@ export interface EntityList {
6868

6969
interface EntityUsageProps {
7070
accessToken: string | null;
71-
entityType: "tag" | "team";
71+
entityType: "tag" | "team" | "organization";
7272
entityId?: string | null;
7373
userID: string | null;
7474
userRole: string | null;
@@ -126,6 +126,15 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
126126
selectedTags.length > 0 ? selectedTags : null,
127127
);
128128
setSpendData(data);
129+
} else if (entityType === "organization") {
130+
const data = await organizationDailyActivityCall(
131+
accessToken,
132+
startTime,
133+
endTime,
134+
1,
135+
selectedTags.length > 0 ? selectedTags : null,
136+
);
137+
setSpendData(data);
129138
} else {
130139
throw new Error("Invalid entity type");
131140
}
@@ -325,15 +334,25 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
325334
}));
326335
};
327336

337+
const getFilterLabel = (entityType: string) => {
338+
return `Filter by ${entityType}`;
339+
};
340+
341+
const getFilterPlaceholder = (entityType: string) => {
342+
return `Select ${entityType} to filter...`;
343+
};
344+
345+
const capitalizedEntityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1);
346+
328347
return (
329348
<div style={{ width: "100%" }} className="relative">
330349
<UsageExportHeader
331350
dateValue={dateValue}
332351
entityType={entityType}
333352
spendData={spendData}
334353
showFilters={entityList !== null && entityList.length > 0}
335-
filterLabel={`Filter by ${entityType === "tag" ? "Tags" : "Teams"}`}
336-
filterPlaceholder={`Select ${entityType === "tag" ? "tags" : "teams"} to filter...`}
354+
filterLabel={getFilterLabel(entityType)}
355+
filterPlaceholder={getFilterPlaceholder(entityType)}
337356
selectedFilters={selectedTags}
338357
onFiltersChange={setSelectedTags}
339358
filterOptions={getAllTags() || undefined}
@@ -350,7 +369,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
350369
{/* Total Spend Card */}
351370
<Col numColSpan={2}>
352371
<Card>
353-
<Title>{entityType === "tag" ? "Tag" : "Team"} Spend Overview</Title>
372+
<Title>{capitalizedEntityLabel} Spend Overview</Title>
354373
<Grid numItems={5} className="gap-4 mt-4">
355374
<Card>
356375
<Title>Total Spend</Title>
@@ -413,10 +432,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
413432
<p className="text-gray-600">Failed: {data.metrics.failed_requests}</p>
414433
<p className="text-gray-600">Total Tokens: {data.metrics.total_tokens}</p>
415434
<p className="text-gray-600">
416-
{entityType === "tag" ? "Total Tags" : "Total Teams"}: {entityCount}
435+
Total {capitalizedEntityLabel}s: {entityCount}
417436
</p>
418437
<div className="mt-2 border-t pt-2">
419-
<p className="font-semibold">Spend by {entityType === "tag" ? "Tag" : "Team"}:</p>
438+
<p className="font-semibold">Spend by {capitalizedEntityLabel}:</p>
420439
{Object.entries(data.breakdown.entities || {})
421440
.sort(([, a], [, b]) => {
422441
const spendA = (a as EntityMetrics).metrics.spend;
@@ -449,10 +468,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
449468
<Card>
450469
<div className="flex flex-col space-y-4">
451470
<div className="flex flex-col space-y-2">
452-
<Title>Spend Per {entityType === "tag" ? "Tag" : "Team"}</Title>
471+
<Title>Spend Per {capitalizedEntityLabel}</Title>
453472
<Subtitle className="text-xs">Showing Top 5 by Spend</Subtitle>
454473
<div className="flex items-center text-sm text-gray-500">
455-
<span>Get Started by Tracking cost per {entityType} </span>
474+
<span>Get Started by Tracking cost per {capitalizedEntityLabel} </span>
456475
<a
457476
href="https://docs.litellm.ai/docs/proxy/enterprise#spend-tracking"
458477
className="text-blue-500 hover:text-blue-700 ml-1"
@@ -496,7 +515,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
496515
<Table>
497516
<TableHead>
498517
<TableRow>
499-
<TableHeaderCell>{entityType === "tag" ? "Tag" : "Team"}</TableHeaderCell>
518+
<TableHeaderCell>{capitalizedEntityLabel}</TableHeaderCell>
500519
<TableHeaderCell>Spend</TableHeaderCell>
501520
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
502521
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>

0 commit comments

Comments
 (0)