Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const UsagePage = () => {
userID={userId}
teams={teams ?? []}
premiumUser={premiumUser}
organizations={[]}
/>
);
};
Expand Down
1 change: 1 addition & 0 deletions ui/litellm-dashboard/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ export default function CreateKeyPage() {
userRole={userRole}
accessToken={accessToken}
teams={(teams as Team[]) ?? []}
organizations={(organizations as Organization[]) ?? []}
premiumUser={premiumUser}
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
const [exportScope, setExportScope] = useState<ExportScope>("daily");
const [isExporting, setIsExporting] = useState(false);

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

const handleExportCSV = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ExportScope } from "./types";
interface ExportTypeSelectorProps {
value: ExportScope;
onChange: (value: ExportScope) => void;
entityType: "tag" | "team";
entityType: "tag" | "team" | "organization";
}

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

export default ExportTypeSelector;

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { EntitySpendData } from "./types";

interface UsageExportHeaderProps {
dateValue: DateRangePickerValue;
entityType: "tag" | "team";
entityType: "tag" | "team" | "organization";
spendData: EntitySpendData;
// Optional filter props
showFilters?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface EntitySpendData {
export interface EntityUsageExportModalProps {
isOpen: boolean;
onClose: () => void;
entityType: "tag" | "team";
entityType: "tag" | "team" | "organization";
spendData: EntitySpendData;
dateRange: DateRangePickerValue;
selectedFilters: string[];
Expand Down Expand Up @@ -59,4 +59,3 @@ export interface EntityBreakdown {
id: string;
};
}

11 changes: 5 additions & 6 deletions ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const generateDailyData = (spendData: EntitySpendData, entityLabel: strin
[entityLabel]: data.metadata?.team_alias || entity,
[`${entityLabel} ID`]: entity,
"Spend ($)": formatNumberWithCommas(data.metrics.spend, 4),
"Requests": data.metrics.api_requests,
Requests: data.metrics.api_requests,
"Successful Requests": data.metrics.successful_requests,
"Failed Requests": data.metrics.failed_requests,
"Total Tokens": data.metrics.total_tokens,
Expand Down Expand Up @@ -109,9 +109,9 @@ export const generateDailyWithModelsData = (spendData: EntitySpendData, entityLa
[`${entityLabel} ID`]: entity,
Model: model,
"Spend ($)": formatNumberWithCommas(metrics.spend, 4),
"Requests": metrics.requests,
"Successful": metrics.successful,
"Failed": metrics.failed,
Requests: metrics.requests,
Successful: metrics.successful,
Failed: metrics.failed,
"Total Tokens": metrics.tokens,
});
});
Expand All @@ -137,7 +137,7 @@ export const generateExportData = (
};

export const generateMetadata = (
entityType: "tag" | "team",
entityType: "tag" | "team" | "organization",
dateRange: { from?: Date; to?: Date },
selectedFilters: string[],
exportScope: ExportScope,
Expand All @@ -159,4 +159,3 @@ export const generateMetadata = (
total_tokens: spendData.metadata.total_tokens,
},
});

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Organization } from "../networking";

export const defaultOrg = {
organization_id: null,
organization_id: "default_organization",
organization_alias: "Default Organization",
} as Organization;
18 changes: 17 additions & 1 deletion ui/litellm-dashboard/src/components/entity_usage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import EntityUsage from "./entity_usage";
import * as networking from "./networking";

// Polyfill ResizeObserver for test environment
beforeAll(() => {
if (typeof window !== "undefined" && !window.ResizeObserver) {
window.ResizeObserver = class ResizeObserver {
Expand All @@ -18,6 +17,7 @@ beforeAll(() => {
vi.mock("./networking", () => ({
tagDailyActivityCall: vi.fn(),
teamDailyActivityCall: vi.fn(),
organizationDailyActivityCall: vi.fn(),
}));

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

const mockSpendData = {
results: [
Expand Down Expand Up @@ -126,8 +127,10 @@ describe("EntityUsage", () => {
beforeEach(() => {
mockTagDailyActivityCall.mockClear();
mockTeamDailyActivityCall.mockClear();
mockOrganizationDailyActivityCall.mockClear();
mockTagDailyActivityCall.mockResolvedValue(mockSpendData);
mockTeamDailyActivityCall.mockResolvedValue(mockSpendData);
mockOrganizationDailyActivityCall.mockResolvedValue(mockSpendData);
});

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

it("should render with organization entity type and call organization API", async () => {
const { getByText, getAllByText } = render(<EntityUsage {...defaultProps} entityType="organization" />);

await waitFor(() => {
expect(mockOrganizationDailyActivityCall).toHaveBeenCalled();
});

expect(getByText("Organization Spend Overview")).toBeInTheDocument();

const spendElements = getAllByText("$100.50");
expect(spendElements.length).toBeGreaterThan(0);
});

it("should switch between tabs", async () => {
render(<EntityUsage {...defaultProps} />);

Expand Down
39 changes: 29 additions & 10 deletions ui/litellm-dashboard/src/components/entity_usage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from "@tremor/react";
import { ActivityMetrics, processActivityData } from "./activity_metrics";
import { DailyData, BreakdownMetrics, KeyMetricWithMetadata, EntityMetricWithMetadata, TagUsage } from "./usage/types";
import { tagDailyActivityCall, teamDailyActivityCall } from "./networking";
import { organizationDailyActivityCall, tagDailyActivityCall, teamDailyActivityCall } from "./networking";
import TopKeyView from "./top_key_view";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { valueFormatterSpend } from "./usage/utils/value_formatters";
Expand Down Expand Up @@ -68,7 +68,7 @@ export interface EntityList {

interface EntityUsageProps {
accessToken: string | null;
entityType: "tag" | "team";
entityType: "tag" | "team" | "organization";
entityId?: string | null;
userID: string | null;
userRole: string | null;
Expand Down Expand Up @@ -126,6 +126,15 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
selectedTags.length > 0 ? selectedTags : null,
);
setSpendData(data);
} else if (entityType === "organization") {
const data = await organizationDailyActivityCall(
accessToken,
startTime,
endTime,
1,
selectedTags.length > 0 ? selectedTags : null,
);
setSpendData(data);
} else {
throw new Error("Invalid entity type");
}
Expand Down Expand Up @@ -325,15 +334,25 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
}));
};

const getFilterLabel = (entityType: string) => {
return `Filter by ${entityType}`;
};

const getFilterPlaceholder = (entityType: string) => {
return `Select ${entityType} to filter...`;
};

const capitalizedEntityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1);

return (
<div style={{ width: "100%" }} className="relative">
<UsageExportHeader
dateValue={dateValue}
entityType={entityType}
spendData={spendData}
showFilters={entityList !== null && entityList.length > 0}
filterLabel={`Filter by ${entityType === "tag" ? "Tags" : "Teams"}`}
filterPlaceholder={`Select ${entityType === "tag" ? "tags" : "teams"} to filter...`}
filterLabel={getFilterLabel(entityType)}
filterPlaceholder={getFilterPlaceholder(entityType)}
selectedFilters={selectedTags}
onFiltersChange={setSelectedTags}
filterOptions={getAllTags() || undefined}
Expand All @@ -350,7 +369,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
{/* Total Spend Card */}
<Col numColSpan={2}>
<Card>
<Title>{entityType === "tag" ? "Tag" : "Team"} Spend Overview</Title>
<Title>{capitalizedEntityLabel} Spend Overview</Title>
<Grid numItems={5} className="gap-4 mt-4">
<Card>
<Title>Total Spend</Title>
Expand Down Expand Up @@ -413,10 +432,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
<p className="text-gray-600">Failed: {data.metrics.failed_requests}</p>
<p className="text-gray-600">Total Tokens: {data.metrics.total_tokens}</p>
<p className="text-gray-600">
{entityType === "tag" ? "Total Tags" : "Total Teams"}: {entityCount}
Total {capitalizedEntityLabel}s: {entityCount}
</p>
<div className="mt-2 border-t pt-2">
<p className="font-semibold">Spend by {entityType === "tag" ? "Tag" : "Team"}:</p>
<p className="font-semibold">Spend by {capitalizedEntityLabel}:</p>
{Object.entries(data.breakdown.entities || {})
.sort(([, a], [, b]) => {
const spendA = (a as EntityMetrics).metrics.spend;
Expand Down Expand Up @@ -449,10 +468,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
<Card>
<div className="flex flex-col space-y-4">
<div className="flex flex-col space-y-2">
<Title>Spend Per {entityType === "tag" ? "Tag" : "Team"}</Title>
<Title>Spend Per {capitalizedEntityLabel}</Title>
<Subtitle className="text-xs">Showing Top 5 by Spend</Subtitle>
<div className="flex items-center text-sm text-gray-500">
<span>Get Started by Tracking cost per {entityType} </span>
<span>Get Started by Tracking cost per {capitalizedEntityLabel} </span>
<a
href="https://docs.litellm.ai/docs/proxy/enterprise#spend-tracking"
className="text-blue-500 hover:text-blue-700 ml-1"
Expand Down Expand Up @@ -496,7 +515,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>{entityType === "tag" ? "Tag" : "Team"}</TableHeaderCell>
<TableHeaderCell>{capitalizedEntityLabel}</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
Expand Down
Loading
Loading