diff --git a/ui/litellm-dashboard/src/components/entity_usage_export_modal.tsx b/ui/litellm-dashboard/src/components/entity_usage_export_modal.tsx new file mode 100644 index 00000000000..e309df5f8f9 --- /dev/null +++ b/ui/litellm-dashboard/src/components/entity_usage_export_modal.tsx @@ -0,0 +1,346 @@ +import React, { useState } from "react"; +import { Text, Button } from "@tremor/react"; +import { Modal, Radio, Select } from "antd"; +import Papa from "papaparse"; +import NotificationsManager from "./molecules/notifications_manager"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; +import type { DateRangePickerValue } from "@tremor/react"; + +interface EntitySpendData { + results: any[]; + metadata: { + total_spend: number; + total_api_requests: number; + total_successful_requests: number; + total_failed_requests: number; + total_tokens: number; + }; +} + +interface EntityUsageExportModalProps { + isOpen: boolean; + onClose: () => void; + entityType: "tag" | "team"; + spendData: EntitySpendData; + dateRange: DateRangePickerValue; + selectedFilters: string[]; +} + +type ExportFormat = "csv" | "json"; +type ExportScope = "daily" | "daily_with_models"; + +const EntityUsageExportModal: React.FC = ({ + isOpen, + onClose, + entityType, + spendData, + dateRange, + selectedFilters, +}) => { + const [exportFormat, setExportFormat] = useState("csv"); + const [exportScope, setExportScope] = useState("daily"); + const [isExporting, setIsExporting] = useState(false); + + const entityLabel = entityType === "tag" ? "Tag" : "Team"; + const entityLabelPlural = entityType === "tag" ? "Tags" : "Teams"; + + const getEntityBreakdown = () => { + const entitySpend: { [key: string]: any } = {}; + spendData.results.forEach((day) => { + Object.entries(day.breakdown.entities || {}).forEach(([entity, data]: [string, any]) => { + if (!entitySpend[entity]) { + entitySpend[entity] = { + metrics: { + spend: 0, + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + api_requests: 0, + successful_requests: 0, + failed_requests: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }, + metadata: { + alias: data.metadata?.team_alias || entity, + id: entity, + }, + }; + } + entitySpend[entity].metrics.spend += data.metrics.spend; + entitySpend[entity].metrics.api_requests += data.metrics.api_requests; + entitySpend[entity].metrics.successful_requests += data.metrics.successful_requests; + entitySpend[entity].metrics.failed_requests += data.metrics.failed_requests; + entitySpend[entity].metrics.total_tokens += data.metrics.total_tokens; + entitySpend[entity].metrics.prompt_tokens += data.metrics.prompt_tokens || 0; + entitySpend[entity].metrics.completion_tokens += data.metrics.completion_tokens || 0; + entitySpend[entity].metrics.cache_read_input_tokens += data.metrics.cache_read_input_tokens || 0; + entitySpend[entity].metrics.cache_creation_input_tokens += data.metrics.cache_creation_input_tokens || 0; + }); + }); + + return Object.values(entitySpend).sort((a: any, b: any) => b.metrics.spend - a.metrics.spend); + }; + + const generateDailyData = () => { + const dailyBreakdown: any[] = []; + + spendData.results.forEach((day) => { + Object.entries(day.breakdown.entities || {}).forEach(([entity, data]: [string, any]) => { + dailyBreakdown.push({ + Date: day.date, + [entityLabel]: data.metadata?.team_alias || entity, + [`${entityLabel} ID`]: entity, + "Spend ($)": formatNumberWithCommas(data.metrics.spend, 4), + "Requests": data.metrics.api_requests, + "Successful Requests": data.metrics.successful_requests, + "Failed Requests": data.metrics.failed_requests, + "Total Tokens": data.metrics.total_tokens, + "Prompt Tokens": data.metrics.prompt_tokens || 0, + "Completion Tokens": data.metrics.completion_tokens || 0, + }); + }); + }); + + return dailyBreakdown.sort((a, b) => new Date(a.Date).getTime() - new Date(b.Date).getTime()); + }; + + const generateDailyWithModelsData = () => { + const dailyModelBreakdown: any[] = []; + + spendData.results.forEach((day) => { + const dailyEntityModels: { [key: string]: { [key: string]: any } } = {}; + + Object.entries(day.breakdown.entities || {}).forEach(([entity, entityData]: [string, any]) => { + const entityName = entityData.metadata?.team_alias || entity; + + if (!dailyEntityModels[entity]) { + dailyEntityModels[entity] = {}; + } + + Object.entries(day.breakdown.models || {}).forEach(([model, modelData]: [string, any]) => { + const apiKeyBreakdown = entityData.api_key_breakdown || {}; + + Object.entries(apiKeyBreakdown).forEach(([apiKey, apiKeyData]: [string, any]) => { + if (!dailyEntityModels[entity][model]) { + dailyEntityModels[entity][model] = { + spend: 0, + requests: 0, + successful: 0, + failed: 0, + tokens: 0, + }; + } + dailyEntityModels[entity][model].spend += apiKeyData.metrics.spend || 0; + dailyEntityModels[entity][model].requests += apiKeyData.metrics.api_requests || 0; + dailyEntityModels[entity][model].successful += apiKeyData.metrics.successful_requests || 0; + dailyEntityModels[entity][model].failed += apiKeyData.metrics.failed_requests || 0; + dailyEntityModels[entity][model].tokens += apiKeyData.metrics.total_tokens || 0; + }); + }); + }); + + Object.entries(dailyEntityModels).forEach(([entity, models]) => { + const entityData = day.breakdown.entities?.[entity]; + const entityName = entityData?.metadata?.team_alias || entity; + + Object.entries(models).forEach(([model, metrics]: [string, any]) => { + dailyModelBreakdown.push({ + Date: day.date, + [entityLabel]: entityName, + [`${entityLabel} ID`]: entity, + Model: model, + "Spend ($)": formatNumberWithCommas(metrics.spend, 4), + "Requests": metrics.requests, + "Successful": metrics.successful, + "Failed": metrics.failed, + "Total Tokens": metrics.tokens, + }); + }); + }); + }); + + return dailyModelBreakdown.sort((a, b) => new Date(a.Date).getTime() - new Date(b.Date).getTime()); + }; + + const generateExportData = () => { + switch (exportScope) { + case "daily": + return generateDailyData(); + case "daily_with_models": + return generateDailyWithModelsData(); + default: + return generateDailyData(); + } + }; + + const generateMetadata = () => ({ + export_date: new Date().toISOString(), + entity_type: entityType, + date_range: { + from: dateRange.from?.toISOString(), + to: dateRange.to?.toISOString(), + }, + filters_applied: selectedFilters.length > 0 ? selectedFilters : "None", + export_scope: exportScope, + summary: { + total_spend: spendData.metadata.total_spend, + total_requests: spendData.metadata.total_api_requests, + successful_requests: spendData.metadata.total_successful_requests, + failed_requests: spendData.metadata.total_failed_requests, + total_tokens: spendData.metadata.total_tokens, + }, + }); + + const handleExportCSV = () => { + const data = generateExportData(); + const csv = Papa.unparse(data); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + const fileName = `${entityType}_usage_${exportScope}_${new Date().toISOString().split("T")[0]}.csv`; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }; + + const handleExportJSON = () => { + const data = generateExportData(); + const metadata = generateMetadata(); + const exportObject = { + metadata, + data, + }; + const jsonString = JSON.stringify(exportObject, null, 2); + const blob = new Blob([jsonString], { type: "application/json" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + const fileName = `${entityType}_usage_${exportScope}_${new Date().toISOString().split("T")[0]}.json`; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + }; + + const handleExport = async (format?: ExportFormat) => { + const formatToUse = format || exportFormat; + setIsExporting(true); + try { + if (formatToUse === "csv") { + handleExportCSV(); + NotificationsManager.success(`${entityLabel} usage data exported successfully as CSV`); + } else { + handleExportJSON(); + NotificationsManager.success(`${entityLabel} usage data exported successfully as JSON`); + } + onClose(); + } catch (error) { + console.error("Error exporting data:", error); + NotificationsManager.fromBackend("Failed to export data"); + } finally { + setIsExporting(false); + } + }; + + const getScopeDescription = () => { + switch (exportScope) { + case "daily": + return `Day-by-day breakdown for each ${entityType}`; + case "daily_with_models": + return `Day-by-day breakdown by ${entityType} and model`; + default: + return ""; + } + }; + + const getRecordCount = () => { + const data = generateExportData(); + return data.length; + }; + + return ( + Export {entityLabel} Usage} + open={isOpen} + onCancel={onClose} + footer={null} + width={480} + destroyOnClose + > +
+
+ {dateRange.from?.toLocaleDateString()} - {dateRange.to?.toLocaleDateString()} + {selectedFilters.length > 0 && ` ยท ${selectedFilters.length} filter${selectedFilters.length > 1 ? "s" : ""}`} +
+ +
+ + setExportScope(e.target.value)} + className="w-full" + > +
+ + + +
+
+
+ +
+ +