diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx index 06e4f2d79fa..053f1235168 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx @@ -49,8 +49,8 @@ describe("ExportTypeSelector", () => { it("should hide the per-user scope for user exports while keeping the other scopes", () => { renderWithProviders(); - expect(screen.queryByRole("radio", { name: /and user/i })).toBeNull(); - expect(screen.getByRole("radio", { name: /Day-by-day breakdown by user$/i })).toBeInTheDocument(); + expect(screen.queryByRole("radio", { name: /and user/i })).not.toBeInTheDocument(); + expect(screen.getByRole("radio", { name: /Day-by-day breakdown by user Daily metrics for each user$/i })).toBeInTheDocument(); expect(screen.getByRole("radio", { name: /Day-by-day breakdown by user and key/i })).toBeInTheDocument(); expect(screen.getByRole("radio", { name: /Day-by-day by user and model/i })).toBeInTheDocument(); }); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx index edd055ba7a7..f6fdece83a8 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx @@ -9,7 +9,7 @@ interface ExportTypeSelectorProps { } const ExportTypeSelector: React.FC = ({ value, onChange, entityType }) => { - const scopes: { value: ExportScope; title: string; description: string }[] = [ + const allScopes: { value: ExportScope; title: string; description: string }[] = [ { value: "daily", title: `Day-by-day breakdown by ${entityType}`, @@ -25,7 +25,13 @@ const ExportTypeSelector: React.FC = ({ value, onChange title: `Day-by-day by ${entityType} and model`, description: "Daily metrics split by model", }, + { + value: "daily_with_users", + title: `Day-by-day breakdown by ${entityType} and user`, + description: `Daily metrics for each ${entityType}, split by key owner`, + }, ]; + const scopes = allScopes.filter((scope) => scope.value !== "daily_with_users" || entityType !== "user"); return (
diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts index 30714ad632d..15f193ecc3f 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts @@ -2,7 +2,7 @@ import type { DateRangePickerValue } from "@/components/shared/date_picker_types import type { Team } from "@/components/key_team_helpers/key_list"; export type ExportFormat = "csv" | "json"; -export type ExportScope = "daily" | "daily_with_keys" | "daily_with_models"; +export type ExportScope = "daily" | "daily_with_keys" | "daily_with_models" | "daily_with_users"; export type EntityType = "tag" | "team" | "organization" | "customer" | "agent" | "user"; export interface EntitySpendData { diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts index 8fd75134bcc..1c47c609b8f 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts @@ -166,6 +166,8 @@ export const generateDailyWithKeysData = ( entityAlias: string; keyId: string; keyAlias: string | null; + userId: string | null; + userEmail: string | null; metrics: { spend: number; api_requests: number; @@ -200,6 +202,8 @@ export const generateDailyWithKeysData = ( entityAlias, keyId, keyAlias, + userId: keyData?.metadata?.user_id || null, + userEmail: keyData?.metadata?.user_email || null, metrics: { spend: keyData.metrics?.spend || 0, api_requests: keyData.metrics?.api_requests || 0, @@ -236,6 +240,9 @@ export const generateDailyWithKeysData = ( [`${entityLabel} ID`]: item.entityId, "Key Alias": item.keyAlias || "-", "Key ID": item.keyId, + ...(entityLabel === "User" + ? {} + : { "User ID": item.userId || "-", "User Email": item.userEmail || "-" }), "Spend ($)": formatNumberWithCommas(item.metrics.spend, 4), Requests: item.metrics.api_requests, "Successful Requests": item.metrics.successful_requests, @@ -250,6 +257,74 @@ export const generateDailyWithKeysData = ( return dailyKeyBreakdown.sort((a, b) => new Date(a.Date).getTime() - new Date(b.Date).getTime()); }; +export const generateDailyWithUsersData = ( + spendData: EntitySpendData, + entityLabel: string, + teamAliasMap: Record = {}, +): any[] => { + const aggregatedData: { + [key: string]: { + Date: string; + entityId: string; + entityAlias: string; + userId: string; + userEmail: string | null; + keyIds: Set; + metrics: Record<(typeof METRIC_KEYS)[number], number>; + }; + } = {}; + + spendData.results.forEach((day) => { + Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => { + const { id: entityId, alias: entityAlias } = resolveEntityDisplay(entity, teamAliasMap, data.metadata); + Object.entries(data.api_key_breakdown || {}).forEach(([keyId, keyData]: [string, any]) => { + const userId = keyData?.metadata?.user_id || "Unassigned"; + const uniqueKey = `${day.date}_${entityId}_${userId}`; + if (!aggregatedData[uniqueKey]) { + aggregatedData[uniqueKey] = { + Date: day.date, + entityId, + entityAlias, + userId, + userEmail: null, + keyIds: new Set(), + metrics: Object.fromEntries(METRIC_KEYS.map((k) => [k, 0])) as Record< + (typeof METRIC_KEYS)[number], + number + >, + }; + } + const bucket = aggregatedData[uniqueKey]; + bucket.userEmail = bucket.userEmail || keyData?.metadata?.user_email || null; + bucket.keyIds.add(keyId); + for (const k of METRIC_KEYS) { + bucket.metrics[k] += keyData?.metrics?.[k] || 0; + } + }); + }); + }); + + return Object.values(aggregatedData) + .map((item) => ({ + Date: item.Date, + [entityLabel]: item.entityAlias, + [`${entityLabel} ID`]: item.entityId, + "User ID": item.userId, + "User Email": item.userEmail || "-", + Keys: item.keyIds.size, + "Spend ($)": formatNumberWithCommas(item.metrics.spend, 4), + Requests: item.metrics.api_requests, + "Successful Requests": item.metrics.successful_requests, + "Failed Requests": item.metrics.failed_requests, + "Total Tokens": item.metrics.total_tokens, + "Prompt Tokens": item.metrics.prompt_tokens, + "Completion Tokens": item.metrics.completion_tokens, + "Cache Read Input Tokens": item.metrics.cache_read_input_tokens, + "Cache Creation Input Tokens": item.metrics.cache_creation_input_tokens, + })) + .sort((a, b) => new Date(a.Date).getTime() - new Date(b.Date).getTime()); +}; + export const generateDailyWithModelsData = ( spendData: EntitySpendData, entityLabel: string, @@ -340,6 +415,8 @@ export const generateExportData = ( return generateDailyWithKeysData(spendData, entityLabel, teamAliasMap); case "daily_with_models": return generateDailyWithModelsData(spendData, entityLabel, teamAliasMap); + case "daily_with_users": + return generateDailyWithUsersData(spendData, entityLabel, teamAliasMap); default: return generateDailyData(spendData, entityLabel, teamAliasMap); }