diff --git a/ui/litellm-dashboard/src/components/activity_metrics.tsx b/ui/litellm-dashboard/src/components/activity_metrics.tsx index 5b30f290bb..1c4fbfa480 100644 --- a/ui/litellm-dashboard/src/components/activity_metrics.tsx +++ b/ui/litellm-dashboard/src/components/activity_metrics.tsx @@ -1,15 +1,160 @@ -import React from 'react'; -import { Card, Grid, Text, Title, Accordion, AccordionHeader, AccordionBody } from '@tremor/react'; -import { AreaChart, BarChart } from '@tremor/react'; -import { SpendMetrics, DailyData, ModelActivityData, MetricWithMetadata, KeyMetricWithMetadata } from './usage/types'; -import { Collapse } from 'antd'; -import { formatNumberWithCommas } from '@/utils/dataUtils'; +import React from "react"; +import { + Card, + Grid, + Text, + Title, +} from "@tremor/react"; +import { AreaChart, BarChart } from "@tremor/react"; +import type { CustomTooltipProps } from "@tremor/react"; +import { + SpendMetrics, + DailyData, + ModelActivityData, + KeyMetricWithMetadata, +} from "./usage/types"; +import { Collapse } from "antd"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { + valueFormatter, + valueFormatterSpend, +} from "../components/usage/utils/value_formatters"; interface ActivityMetricsProps { modelMetrics: Record; } -const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: ModelActivityData }) => { +interface ChartDataPoint { + date: string; + metrics: SpendMetrics; +} + +const colorNameToHex: { [key: string]: string } = { + blue: "#3b82f6", + cyan: "#06b6d4", + indigo: "#6366f1", + green: "#22c55e", + red: "#ef4444", + purple: "#8b5cf6", +}; + +export const CustomTooltip = ({ + active, + payload, + label, +}: CustomTooltipProps) => { + if (active && payload && payload.length) { + const formatCategoryName = (name: string): string => { + return name + .replace("metrics.", "") + .replace(/_/g, " ") + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + }; + + const getRawValue = ( + dataPoint: ChartDataPoint, + key: string + ): number | undefined => { + // key is like "metrics.total_tokens" + const metricKey = key.substring( + key.indexOf(".") + 1 + ) as keyof SpendMetrics; + if (dataPoint.metrics && metricKey in dataPoint.metrics) { + return dataPoint.metrics[metricKey]; + } + return undefined; + }; + + return ( +
+

{label}

+ {payload.map((item) => { + const dataKey = item.dataKey?.toString(); + if (!dataKey || !item.payload) return null; + + const rawValue = getRawValue(item.payload, dataKey); + const isSpend = dataKey.includes("spend"); + const formattedValue = + rawValue !== undefined + ? isSpend + ? `$${rawValue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` + : rawValue.toLocaleString() + : "N/A"; + + const colorName = item.color as keyof typeof colorNameToHex; + const hexColor = colorNameToHex[colorName] || item.color; + return ( +
+
+ +

+ {formatCategoryName(dataKey)} +

+
+

+ {formattedValue} +

+
+ ); + })} +
+ ); + } + return null; +}; + +const CustomLegend = ({ + categories, + colors, +}: { + categories: string[]; + colors: string[]; +}) => { + const formatCategoryName = (name: string): string => { + return name + .replace("metrics.", "") + .replace(/_/g, " ") + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + }; + + return ( +
+ {categories.map((category, idx) => { + const colorName = colors[idx] as keyof typeof colorNameToHex; + const hexColor = colorNameToHex[colorName] || colors[idx]; + return ( +
+ +

+ {formatCategoryName(category)} +

+
+ ); + })} +
+ ); +}; + +const ModelSection = ({ + modelName, + metrics, +}: { + modelName: string; + metrics: ModelActivityData; +}) => { return (
{/* Summary Cards */} @@ -25,74 +170,156 @@ const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: Mode Total Tokens {metrics.total_tokens.toLocaleString()} - {Math.round(metrics.total_tokens / metrics.total_successful_requests)} avg per successful request + + {Math.round( + metrics.total_tokens / metrics.total_successful_requests + )}{" "} + avg per successful request + Total Spend ${formatNumberWithCommas(metrics.total_spend, 2)} - ${formatNumberWithCommas((metrics.total_spend / metrics.total_successful_requests), 3)} per successful request + + $ + {formatNumberWithCommas( + metrics.total_spend / metrics.total_successful_requests, + 3 + )}{" "} + per successful request + {/* Charts */} - Total Tokens - + Total Tokens + +
+ number.toLocaleString()} + valueFormatter={valueFormatter} + customTooltip={CustomTooltip} + showLegend={false} /> - Requests per day +
+ Requests per day + +
number.toLocaleString()} + valueFormatter={valueFormatter} + customTooltip={CustomTooltip} + showLegend={false} />
- Spend per day +
+ Spend per day + +
`$${formatNumberWithCommas(value, 2)}`} + valueFormatter={valueFormatterSpend} + customTooltip={CustomTooltip} + showLegend={false} />
- Success vs Failed Requests - number.toLocaleString()} - stack - /> - - - - Prompt Caching Metrics -
- Cache Read: {metrics.total_cache_read_input_tokens?.toLocaleString() || 0} tokens - Cache Creation: {metrics.total_cache_creation_input_tokens?.toLocaleString() || 0} tokens +
+ Success vs Failed Requests +
+ + + +
+ Prompt Caching Metrics + +
+
+ + Cache Read:{" "} + {metrics.total_cache_read_input_tokens?.toLocaleString() || 0}{" "} + tokens + + + Cache Creation:{" "} + {metrics.total_cache_creation_input_tokens?.toLocaleString() || 0}{" "} + tokens + +
+ number.toLocaleString()} + valueFormatter={valueFormatter} + customTooltip={CustomTooltip} + showLegend={false} />
@@ -100,10 +327,12 @@ const ModelSection = ({ modelName, metrics }: { modelName: string; metrics: Mode ); }; -export const ActivityMetrics: React.FC = ({ modelMetrics }) => { +export const ActivityMetrics: React.FC = ({ + modelMetrics, +}) => { const modelNames = Object.keys(modelMetrics).sort((a, b) => { - if (a === '') return 1; - if (b === '') return -1; + if (a === "") return 1; + if (b === "") return -1; return modelMetrics[b].total_spend - modelMetrics[a].total_spend; }); @@ -115,30 +344,35 @@ export const ActivityMetrics: React.FC = ({ modelMetrics } total_spend: 0, total_cache_read_input_tokens: 0, total_cache_creation_input_tokens: 0, - daily_data: {} as Record + daily_data: {} as Record< + string, + { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + api_requests: number; + spend: number; + successful_requests: number; + failed_requests: number; + cache_read_input_tokens: number; + cache_creation_input_tokens: number; + } + >, }; // Aggregate data - Object.values(modelMetrics).forEach(model => { + Object.values(modelMetrics).forEach((model) => { totalMetrics.total_requests += model.total_requests; totalMetrics.total_successful_requests += model.total_successful_requests; totalMetrics.total_tokens += model.total_tokens; totalMetrics.total_spend += model.total_spend; - totalMetrics.total_cache_read_input_tokens += model.total_cache_read_input_tokens || 0; - totalMetrics.total_cache_creation_input_tokens += model.total_cache_creation_input_tokens || 0; + totalMetrics.total_cache_read_input_tokens += + model.total_cache_read_input_tokens || 0; + totalMetrics.total_cache_creation_input_tokens += + model.total_cache_creation_input_tokens || 0; // Aggregate daily data - model.daily_data.forEach(day => { + model.daily_data.forEach((day) => { if (!totalMetrics.daily_data[day.date]) { totalMetrics.daily_data[day.date] = { prompt_tokens: 0, @@ -149,18 +383,26 @@ export const ActivityMetrics: React.FC = ({ modelMetrics } successful_requests: 0, failed_requests: 0, cache_read_input_tokens: 0, - cache_creation_input_tokens: 0 + cache_creation_input_tokens: 0, }; } - totalMetrics.daily_data[day.date].prompt_tokens += day.metrics.prompt_tokens; - totalMetrics.daily_data[day.date].completion_tokens += day.metrics.completion_tokens; - totalMetrics.daily_data[day.date].total_tokens += day.metrics.total_tokens; - totalMetrics.daily_data[day.date].api_requests += day.metrics.api_requests; + totalMetrics.daily_data[day.date].prompt_tokens += + day.metrics.prompt_tokens; + totalMetrics.daily_data[day.date].completion_tokens += + day.metrics.completion_tokens; + totalMetrics.daily_data[day.date].total_tokens += + day.metrics.total_tokens; + totalMetrics.daily_data[day.date].api_requests += + day.metrics.api_requests; totalMetrics.daily_data[day.date].spend += day.metrics.spend; - totalMetrics.daily_data[day.date].successful_requests += day.metrics.successful_requests; - totalMetrics.daily_data[day.date].failed_requests += day.metrics.failed_requests; - totalMetrics.daily_data[day.date].cache_read_input_tokens += day.metrics.cache_read_input_tokens || 0; - totalMetrics.daily_data[day.date].cache_creation_input_tokens += day.metrics.cache_creation_input_tokens || 0; + totalMetrics.daily_data[day.date].successful_requests += + day.metrics.successful_requests; + totalMetrics.daily_data[day.date].failed_requests += + day.metrics.failed_requests; + totalMetrics.daily_data[day.date].cache_read_input_tokens += + day.metrics.cache_read_input_tokens || 0; + totalMetrics.daily_data[day.date].cache_creation_input_tokens += + day.metrics.cache_creation_input_tokens || 0; }); }); @@ -181,7 +423,9 @@ export const ActivityMetrics: React.FC = ({ modelMetrics } Total Successful Requests - {totalMetrics.total_successful_requests.toLocaleString()} + + {totalMetrics.total_successful_requests.toLocaleString()} + Total Tokens @@ -189,30 +433,64 @@ export const ActivityMetrics: React.FC = ({ modelMetrics } Total Spend - ${formatNumberWithCommas(totalMetrics.total_spend, 2)} + + ${formatNumberWithCommas(totalMetrics.total_spend, 2)} + - Total Tokens Over Time - + Total Tokens Over Time + +
+ number.toLocaleString()} + valueFormatter={valueFormatter} + customTooltip={CustomTooltip} + showLegend={false} />
- Total Requests Over Time +
+ Total Requests Over Time + +
number.toLocaleString()} + categories={[ + "metrics.successful_requests", + "metrics.failed_requests", + ]} + colors={["green", "red"]} + valueFormatter={valueFormatter} stack + customTooltip={CustomTooltip} + showLegend={false} />
@@ -221,21 +499,30 @@ export const ActivityMetrics: React.FC = ({ modelMetrics } {/* Individual Model Sections */} {modelNames.map((modelName) => ( - - {modelMetrics[modelName].label || 'Unknown Item'} + {modelMetrics[modelName].label || "Unknown Item"}
- ${formatNumberWithCommas(modelMetrics[modelName].total_spend, 2)} - {modelMetrics[modelName].total_requests.toLocaleString()} requests + + $ + {formatNumberWithCommas( + modelMetrics[modelName].total_spend, + 2 + )} + + + {modelMetrics[modelName].total_requests.toLocaleString()}{" "} + requests +
} > -
))} @@ -245,23 +532,30 @@ export const ActivityMetrics: React.FC = ({ modelMetrics } }; // Helper function to format key label -const formatKeyLabel = (modelData: KeyMetricWithMetadata, model: string): string => { +const formatKeyLabel = ( + modelData: KeyMetricWithMetadata, + model: string +): string => { const keyAlias = modelData.metadata.key_alias || `key-hash-${model}`; const teamId = modelData.metadata.team_id; return teamId ? `${keyAlias} (team_id: ${teamId})` : keyAlias; }; // Process data function -export const processActivityData = (dailyActivity: { results: DailyData[] }, key: "models" | "api_keys"): Record => { +export const processActivityData = ( + dailyActivity: { results: DailyData[] }, + key: "models" | "api_keys" +): Record => { const modelMetrics: Record = {}; dailyActivity.results.forEach((day) => { Object.entries(day.breakdown[key] || {}).forEach(([model, modelData]) => { if (!modelMetrics[model]) { modelMetrics[model] = { - label: key === 'api_keys' - ? formatKeyLabel(modelData as KeyMetricWithMetadata, model) - : model, + label: + key === "api_keys" + ? formatKeyLabel(modelData as KeyMetricWithMetadata, model) + : model, total_requests: 0, total_successful_requests: 0, total_failed_requests: 0, @@ -271,20 +565,25 @@ export const processActivityData = (dailyActivity: { results: DailyData[] }, key total_spend: 0, total_cache_read_input_tokens: 0, total_cache_creation_input_tokens: 0, - daily_data: [] + daily_data: [], }; } // Update totals modelMetrics[model].total_requests += modelData.metrics.api_requests; modelMetrics[model].prompt_tokens += modelData.metrics.prompt_tokens; - modelMetrics[model].completion_tokens += modelData.metrics.completion_tokens; + modelMetrics[model].completion_tokens += + modelData.metrics.completion_tokens; modelMetrics[model].total_tokens += modelData.metrics.total_tokens; modelMetrics[model].total_spend += modelData.metrics.spend; - modelMetrics[model].total_successful_requests += modelData.metrics.successful_requests; - modelMetrics[model].total_failed_requests += modelData.metrics.failed_requests; - modelMetrics[model].total_cache_read_input_tokens += modelData.metrics.cache_read_input_tokens || 0; - modelMetrics[model].total_cache_creation_input_tokens += modelData.metrics.cache_creation_input_tokens || 0; + modelMetrics[model].total_successful_requests += + modelData.metrics.successful_requests; + modelMetrics[model].total_failed_requests += + modelData.metrics.failed_requests; + modelMetrics[model].total_cache_read_input_tokens += + modelData.metrics.cache_read_input_tokens || 0; + modelMetrics[model].total_cache_creation_input_tokens += + modelData.metrics.cache_creation_input_tokens || 0; // Add daily data modelMetrics[model].daily_data.push({ @@ -297,17 +596,21 @@ export const processActivityData = (dailyActivity: { results: DailyData[] }, key spend: modelData.metrics.spend, successful_requests: modelData.metrics.successful_requests, failed_requests: modelData.metrics.failed_requests, - cache_read_input_tokens: modelData.metrics.cache_read_input_tokens || 0, - cache_creation_input_tokens: modelData.metrics.cache_creation_input_tokens || 0 - } + cache_read_input_tokens: + modelData.metrics.cache_read_input_tokens || 0, + cache_creation_input_tokens: + modelData.metrics.cache_creation_input_tokens || 0, + }, }); }); }); // Sort daily data - Object.values(modelMetrics).forEach(metrics => { - metrics.daily_data.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + Object.values(modelMetrics).forEach((metrics) => { + metrics.daily_data.sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() + ); }); return modelMetrics; -}; \ No newline at end of file +}; diff --git a/ui/litellm-dashboard/src/components/entity_usage.tsx b/ui/litellm-dashboard/src/components/entity_usage.tsx index af66ea39ff..4b43b3293a 100644 --- a/ui/litellm-dashboard/src/components/entity_usage.tsx +++ b/ui/litellm-dashboard/src/components/entity_usage.tsx @@ -1,18 +1,38 @@ import React, { useState, useEffect } from "react"; -import { - BarChart, Card, Title, Text, - Grid, Col, DateRangePicker, DateRangePickerValue, - Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell, +import { + BarChart, + Card, + Title, + Text, + Grid, + Col, + DateRangePickerValue, + Table, + TableHead, + TableRow, + TableHeaderCell, + TableBody, + TableCell, DonutChart, - TabPanel, TabGroup, TabList, Tab, TabPanels + TabPanel, + TabGroup, + TabList, + Tab, + TabPanels, + Subtitle, } from "@tremor/react"; import UsageDatePicker from "./shared/usage_date_picker"; -import { Select } from 'antd'; -import { ActivityMetrics, processActivityData } from './activity_metrics'; -import { DailyData, KeyMetricWithMetadata, EntityMetricWithMetadata } from './usage/types'; -import { tagDailyActivityCall, teamDailyActivityCall } from './networking'; +import { Select } from "antd"; +import { ActivityMetrics, processActivityData } from "./activity_metrics"; +import { + DailyData, + KeyMetricWithMetadata, + EntityMetricWithMetadata, +} from "./usage/types"; +import { tagDailyActivityCall, teamDailyActivityCall } from "./networking"; import TopKeyView from "./top_key_view"; import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { valueFormatterSpend } from "./usage/utils/value_formatters"; interface EntityMetrics { metrics: { @@ -58,7 +78,7 @@ export interface EntityList { interface EntityUsageProps { accessToken: string | null; - entityType: 'tag' | 'team'; + entityType: "tag" | "team"; entityId?: string | null; userID: string | null; userRole: string | null; @@ -73,17 +93,17 @@ const EntityUsage: React.FC = ({ userID, userRole, entityList, - premiumUser + premiumUser, }) => { - const [spendData, setSpendData] = useState({ - results: [], + const [spendData, setSpendData] = useState({ + results: [], metadata: { total_spend: 0, total_api_requests: 0, total_successful_requests: 0, total_failed_requests: 0, - total_tokens: 0 - } + total_tokens: 0, + }, }); const modelMetrics = processActivityData(spendData, "models"); @@ -99,22 +119,22 @@ const EntityUsage: React.FC = ({ // Create new Date objects to avoid mutating the original dates const startTime = new Date(dateValue.from); const endTime = new Date(dateValue.to); - - if (entityType === 'tag') { + + if (entityType === "tag") { const data = await tagDailyActivityCall( - accessToken, - startTime, - endTime, - 1, + accessToken, + startTime, + endTime, + 1, selectedTags.length > 0 ? selectedTags : null ); setSpendData(data); - } else if (entityType === 'team') { + } else if (entityType === "team") { const data = await teamDailyActivityCall( - accessToken, - startTime, - endTime, - 1, + accessToken, + startTime, + endTime, + 1, selectedTags.length > 0 ? selectedTags : null ); setSpendData(data); @@ -129,7 +149,7 @@ const EntityUsage: React.FC = ({ const getTopModels = () => { const modelSpend: { [key: string]: any } = {}; - spendData.results.forEach(day => { + spendData.results.forEach((day) => { Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => { if (!modelSpend[model]) { modelSpend[model] = { @@ -137,25 +157,28 @@ const EntityUsage: React.FC = ({ requests: 0, successful_requests: 0, failed_requests: 0, - tokens: 0 + tokens: 0, }; } try { modelSpend[model].spend += metrics.metrics.spend; } catch (e) { - console.log(`Error adding spend for ${model}: ${e}, got metrics: ${JSON.stringify(metrics)}`); + console.error( + `Error adding spend for ${model}: ${e}, got metrics: ${JSON.stringify(metrics)}` + ); } modelSpend[model].requests += metrics.metrics.api_requests; - modelSpend[model].successful_requests += metrics.metrics.successful_requests; + modelSpend[model].successful_requests += + metrics.metrics.successful_requests; modelSpend[model].failed_requests += metrics.metrics.failed_requests; modelSpend[model].tokens += metrics.metrics.total_tokens; }); }); - + return Object.entries(modelSpend) .map(([model, metrics]) => ({ key: model, - ...metrics + ...metrics, })) .sort((a, b) => b.spend - a.spend) .slice(0, 5); @@ -163,7 +186,7 @@ const EntityUsage: React.FC = ({ const getTopAPIKeys = () => { const keySpend: { [key: string]: KeyMetricWithMetadata } = {}; - spendData.results.forEach(day => { + spendData.results.forEach((day) => { Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => { if (!keySpend[key]) { keySpend[key] = { @@ -176,25 +199,30 @@ const EntityUsage: React.FC = ({ successful_requests: 0, failed_requests: 0, cache_read_input_tokens: 0, - cache_creation_input_tokens: 0 + cache_creation_input_tokens: 0, }, metadata: { - key_alias: metrics.metadata.key_alias - } + key_alias: metrics.metadata.key_alias, + }, }; } keySpend[key].metrics.spend += metrics.metrics.spend; keySpend[key].metrics.prompt_tokens += metrics.metrics.prompt_tokens; - keySpend[key].metrics.completion_tokens += metrics.metrics.completion_tokens; + keySpend[key].metrics.completion_tokens += + metrics.metrics.completion_tokens; keySpend[key].metrics.total_tokens += metrics.metrics.total_tokens; keySpend[key].metrics.api_requests += metrics.metrics.api_requests; - keySpend[key].metrics.successful_requests += metrics.metrics.successful_requests; - keySpend[key].metrics.failed_requests += metrics.metrics.failed_requests; - keySpend[key].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0; - keySpend[key].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0; + keySpend[key].metrics.successful_requests += + metrics.metrics.successful_requests; + keySpend[key].metrics.failed_requests += + metrics.metrics.failed_requests; + keySpend[key].metrics.cache_read_input_tokens += + metrics.metrics.cache_read_input_tokens || 0; + keySpend[key].metrics.cache_creation_input_tokens += + metrics.metrics.cache_creation_input_tokens || 0; }); }); - + return Object.entries(keySpend) .map(([api_key, metrics]) => ({ api_key, @@ -207,32 +235,36 @@ const EntityUsage: React.FC = ({ const getProviderSpend = () => { const providerSpend: { [key: string]: any } = {}; - spendData.results.forEach(day => { - Object.entries(day.breakdown.providers || {}).forEach(([provider, metrics]) => { - if (!providerSpend[provider]) { - providerSpend[provider] = { - provider, - spend: 0, - requests: 0, - successful_requests: 0, - failed_requests: 0, - tokens: 0 - }; + spendData.results.forEach((day) => { + Object.entries(day.breakdown.providers || {}).forEach( + ([provider, metrics]) => { + if (!providerSpend[provider]) { + providerSpend[provider] = { + provider, + spend: 0, + requests: 0, + successful_requests: 0, + failed_requests: 0, + tokens: 0, + }; + } + try { + providerSpend[provider].spend += metrics.metrics.spend; + providerSpend[provider].requests += metrics.metrics.api_requests; + providerSpend[provider].successful_requests += + metrics.metrics.successful_requests; + providerSpend[provider].failed_requests += + metrics.metrics.failed_requests; + providerSpend[provider].tokens += metrics.metrics.total_tokens; + } catch (e) { + console.error(`Error processing provider ${provider}: ${e}`); + } } - try { - providerSpend[provider].spend += metrics.metrics.spend; - providerSpend[provider].requests += metrics.metrics.api_requests; - providerSpend[provider].successful_requests += metrics.metrics.successful_requests; - providerSpend[provider].failed_requests += metrics.metrics.failed_requests; - providerSpend[provider].tokens += metrics.metrics.total_tokens; - } catch (e) { - console.log(`Error processing provider ${provider}: ${e}`); - } - }); + ); }); - + return Object.values(providerSpend) - .filter(provider => provider.spend > 0) + .filter((provider) => provider.spend > 0) .sort((a, b) => b.spend - a.spend); }; @@ -244,12 +276,12 @@ const EntityUsage: React.FC = ({ const filterDataByTags = (data: EntityMetricWithMetadata[]) => { if (selectedTags.length === 0) return data; - return data.filter(item => selectedTags.includes(item.metadata.id)); + return data.filter((item) => selectedTags.includes(item.metadata.id)); }; const getEntityBreakdown = () => { const entitySpend: { [key: string]: EntityMetricWithMetadata } = {}; - spendData.results.forEach(day => { + spendData.results.forEach((day) => { Object.entries(day.breakdown.entities || {}).forEach(([entity, data]) => { if (!entitySpend[entity]) { entitySpend[entity] = { @@ -262,55 +294,68 @@ const EntityUsage: React.FC = ({ successful_requests: 0, failed_requests: 0, cache_read_input_tokens: 0, - cache_creation_input_tokens: 0 + cache_creation_input_tokens: 0, }, metadata: { alias: data.metadata.team_alias || entity, - id: 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.successful_requests += + data.metrics.successful_requests; + entitySpend[entity].metrics.failed_requests += + data.metrics.failed_requests; entitySpend[entity].metrics.total_tokens += data.metrics.total_tokens; }); }); - - const result = Object.values(entitySpend) - .sort((a, b) => b.metrics.spend - a.metrics.spend); - + + const result = Object.values(entitySpend).sort( + (a, b) => b.metrics.spend - a.metrics.spend + ); + return filterDataByTags(result); }; - + const getProcessedEntityBreakdownForChart = () => { + const data = getEntityBreakdown(); + const topEntities = data.slice(0, 5); + return topEntities.map((e) => ({ + ...e, + metadata: { + ...e.metadata, + alias_display: + e.metadata.alias && e.metadata.alias.length > 15 + ? `${e.metadata.alias.slice(0, 15)}...` + : e.metadata.alias, + }, + })); + }; return (
+ + + + {entityList && entityList.length > 0 && ( - - - {entityList && entityList.length > 0 && ( - - Filter by {entityType === 'tag' ? 'Tags' : 'Teams'} - - - )} - + /> + + )} + Cost @@ -323,12 +368,18 @@ const EntityUsage: React.FC = ({ {/* Total Spend Card */} - {entityType === 'tag' ? 'Tag' : 'Team'} Spend Overview + + {entityType === "tag" ? "Tag" : "Team"} Spend Overview + Total Spend - ${formatNumberWithCommas(spendData.metadata.total_spend, 2)} + $ + {formatNumberWithCommas( + spendData.metadata.total_spend, + 2 + )} @@ -361,45 +412,89 @@ const EntityUsage: React.FC = ({ {/* Daily Spend Chart */} - - Daily Spend - - new Date(a.date).getTime() - new Date(b.date).getTime() - )} - index="date" - categories={["metrics.spend"]} - colors={["cyan"]} - valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`} - yAxisWidth={100} - showLegend={false} - customTooltip={({ payload, active }) => { - if (!active || !payload?.[0]) return null; - const data = payload[0].payload; - return ( -
-

{data.date}

-

Total Spend: ${formatNumberWithCommas(data.metrics.spend, 2)}

-

Total Requests: {data.metrics.api_requests}

-

Successful: {data.metrics.successful_requests}

-

Failed: {data.metrics.failed_requests}

-

Total Tokens: {data.metrics.total_tokens}

-
-

Spend by {entityType === 'tag' ? 'Tag' : 'Team'}:

- {Object.entries(data.breakdown.entities || {}).map(([entity, entityData]) => { - const metrics = entityData as EntityMetrics; - return ( -

- {metrics.metadata.team_alias || entity}: ${formatNumberWithCommas(metrics.metrics.spend, 2)} -

- ); - })} -
-
- ); - }} - /> -
+ + Daily Spend + + new Date(a.date).getTime() - new Date(b.date).getTime() + )} + index="date" + categories={["metrics.spend"]} + colors={["cyan"]} + valueFormatter={valueFormatterSpend} + yAxisWidth={100} + showLegend={false} + customTooltip={({ payload, active }) => { + if (!active || !payload?.[0]) return null; + const data = payload[0].payload; + const entityCount = Object.keys( + data.breakdown.entities || {} + ).length; + return ( +
+

{data.date}

+

+ Total Spend: $ + {formatNumberWithCommas(data.metrics.spend, 2)} +

+

+ Total Requests: {data.metrics.api_requests} +

+

+ Successful: {data.metrics.successful_requests} +

+

+ Failed: {data.metrics.failed_requests} +

+

+ Total Tokens: {data.metrics.total_tokens} +

+

+ {entityType === "tag" + ? "Total Tags" + : "Total Teams"} + : {entityCount} +

+
+

+ Spend by {entityType === "tag" ? "Tag" : "Team"}: +

+ {Object.entries(data.breakdown.entities || {}) + .sort(([, a], [, b]) => { + const spendA = (a as EntityMetrics).metrics + .spend; + const spendB = (b as EntityMetrics).metrics + .spend; + return spendB - spendA; + }) + .slice(0, 5) + .map(([entity, entityData]) => { + const metrics = entityData as EntityMetrics; + return ( +

+ {metrics.metadata.team_alias || entity}: $ + {formatNumberWithCommas( + metrics.metrics.spend, + 2 + )} +

+ ); + })} + {entityCount > 5 && ( +

+ ...and {entityCount - 5} more +

+ )} +
+
+ ); + }} + /> +
{/* Entity Breakdown Section */} @@ -407,76 +502,137 @@ const EntityUsage: React.FC = ({
- Spend Per {entityType === 'tag' ? 'Tag' : 'Team'} + + Spend Per {entityType === "tag" ? "Tag" : "Team"} + + + Showing Top 5 by Spend +
- Get Started Tracking cost per {entityType} - + + Get Started by Tracking cost per {entityType}{" "} + + here
- - + + `$${formatNumberWithCommas(value, 4)}`} + valueFormatter={valueFormatterSpend} layout="vertical" showLegend={false} - yAxisWidth={100} + yAxisWidth={150} + customTooltip={({ payload, active }) => { + if (!active || !payload?.[0]) return null; + const data = payload[0].payload; + return ( +
+

+ {data.metadata.alias} +

+

+ Spend: $ + {formatNumberWithCommas( + data.metrics.spend, + 4 + )} +

+

+ Requests:{" "} + {data.metrics.api_requests.toLocaleString()} +

+

+ Successful:{" "} + {data.metrics.successful_requests.toLocaleString()} +

+

+ Failed:{" "} + {data.metrics.failed_requests.toLocaleString()} +

+

+ Tokens:{" "} + {data.metrics.total_tokens.toLocaleString()} +

+
+ ); + }} /> - - - - {entityType === 'tag' ? 'Tag' : 'Team'} - Spend - Successful - Failed - Tokens - - - - {getEntityBreakdown() - .filter(entity => entity.metrics.spend > 0) - .map((entity) => ( - - {entity.metadata.alias} - ${formatNumberWithCommas(entity.metrics.spend, 4)} - - {entity.metrics.successful_requests.toLocaleString()} - - - {entity.metrics.failed_requests.toLocaleString()} - - {entity.metrics.total_tokens.toLocaleString()} - - ))} - -
+
+ + + + + {entityType === "tag" ? "Tag" : "Team"} + + Spend + + Successful + + + Failed + + Tokens + + + + {getEntityBreakdown() + .filter((entity) => entity.metrics.spend > 0) + .map((entity) => ( + + + {entity.metadata.alias} + + + $ + {formatNumberWithCommas( + entity.metrics.spend, + 4 + )} + + + {entity.metrics.successful_requests.toLocaleString()} + + + {entity.metrics.failed_requests.toLocaleString()} + + + {entity.metrics.total_tokens.toLocaleString()} + + + ))} + +
+
- {/* Top API Keys */} Top API Keys - + @@ -490,7 +646,9 @@ const EntityUsage: React.FC = ({ index="key" categories={["spend"]} colors={["cyan"]} - valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`} + valueFormatter={(value) => + `$${formatNumberWithCommas(value, 2)}` + } layout="vertical" yAxisWidth={200} showLegend={false} @@ -498,8 +656,6 @@ const EntityUsage: React.FC = ({
- - {/* Spend by Provider */} @@ -512,8 +668,16 @@ const EntityUsage: React.FC = ({ data={getProviderSpend()} index="provider" category="spend" - valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`} - colors={["cyan", "blue", "indigo", "violet", "purple"]} + valueFormatter={(value) => + `$${formatNumberWithCommas(value, 2)}` + } + colors={[ + "cyan", + "blue", + "indigo", + "violet", + "purple", + ]} /> @@ -522,8 +686,12 @@ const EntityUsage: React.FC = ({ Provider Spend - Successful - Failed + + Successful + + + Failed + Tokens @@ -531,14 +699,18 @@ const EntityUsage: React.FC = ({ {getProviderSpend().map((provider) => ( {provider.provider} - ${formatNumberWithCommas(provider.spend, 2)} + + ${formatNumberWithCommas(provider.spend, 2)} + {provider.successful_requests.toLocaleString()} {provider.failed_requests.toLocaleString()} - {provider.tokens.toLocaleString()} + + {provider.tokens.toLocaleString()} + ))} @@ -551,10 +723,10 @@ const EntityUsage: React.FC = ({
- + - +
@@ -562,4 +734,4 @@ const EntityUsage: React.FC = ({ ); }; -export default EntityUsage; \ No newline at end of file +export default EntityUsage; diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index 9269a6586b..180a99bef3 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -1,19 +1,34 @@ /** * New Usage Page - * + * * Uses the new `/user/daily/activity` endpoint to get daily activity data for a user. - * + * * Works at 1m+ spend logs, by querying an aggregate table instead. */ import React, { useState, useEffect } from "react"; -import { - BarChart, Card, Title, Text, - Grid, Col, TabGroup, TabList, Tab, - TabPanel, TabPanels, DonutChart, - Table, TableHead, TableRow, - TableHeaderCell, TableBody, TableCell, - Subtitle, DateRangePicker, DateRangePickerValue +import { + BarChart, + Card, + Title, + Text, + Grid, + Col, + TabGroup, + TabList, + Tab, + TabPanel, + TabPanels, + DonutChart, + Table, + TableHead, + TableRow, + TableHeaderCell, + TableBody, + TableCell, + Subtitle, + DateRangePicker, + DateRangePickerValue, } from "@tremor/react"; import UsageDatePicker from "./shared/usage_date_picker"; import { AreaChart } from "@tremor/react"; @@ -22,13 +37,27 @@ import { userDailyActivityCall, tagListCall } from "./networking"; import { Tag } from "./tag_management/types"; import ViewUserSpend from "./view_user_spend"; import TopKeyView from "./top_key_view"; -import { ActivityMetrics, processActivityData } from './activity_metrics'; -import { SpendMetrics, DailyData, ModelActivityData, MetricWithMetadata, KeyMetricWithMetadata } from './usage/types'; -import EntityUsage from './entity_usage'; -import { old_admin_roles, v2_admin_role_names, all_admin_roles, rolesAllowedToSeeUsage, rolesWithWriteAccess, internalUserRoles } from '../utils/roles'; +import { ActivityMetrics, processActivityData } from "./activity_metrics"; +import { + SpendMetrics, + DailyData, + ModelActivityData, + MetricWithMetadata, + KeyMetricWithMetadata, +} from "./usage/types"; +import EntityUsage from "./entity_usage"; +import { + old_admin_roles, + v2_admin_role_names, + all_admin_roles, + rolesAllowedToSeeUsage, + rolesWithWriteAccess, + internalUserRoles, +} from "../utils/roles"; import { Team } from "./key_team_helpers/key_list"; import { EntityList } from "./entity_usage"; import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { valueFormatterSpend } from "./usage/utils/value_formatters"; interface NewUsagePageProps { accessToken: string | null; @@ -43,7 +72,7 @@ const NewUsagePage: React.FC = ({ userRole, userID, teams, - premiumUser + premiumUser, }) => { const [userSpendData, setUserSpendData] = useState<{ results: DailyData[]; @@ -56,17 +85,19 @@ const NewUsagePage: React.FC = ({ to: new Date(), }); - const [allTags, setAllTags] = useState([]); + const [allTags, setAllTags] = useState([]); const getAllTags = async () => { if (!accessToken) { return; } const tags = await tagListCall(accessToken); - setAllTags(Object.values(tags).map((tag: Tag) => ({ - label: tag.name, - value: tag.name - }))); + setAllTags( + Object.values(tags).map((tag: Tag) => ({ + label: tag.name, + value: tag.name, + })) + ); }; useEffect(() => { @@ -79,7 +110,7 @@ const NewUsagePage: React.FC = ({ // Calculate top models from the breakdown data const getTopModels = () => { const modelSpend: { [key: string]: MetricWithMetadata } = {}; - userSpendData.results.forEach(day => { + userSpendData.results.forEach((day) => { Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => { if (!modelSpend[model]) { modelSpend[model] = { @@ -92,23 +123,29 @@ const NewUsagePage: React.FC = ({ successful_requests: 0, failed_requests: 0, cache_read_input_tokens: 0, - cache_creation_input_tokens: 0 + cache_creation_input_tokens: 0, }, - metadata: {} + metadata: {}, }; } modelSpend[model].metrics.spend += metrics.metrics.spend; - modelSpend[model].metrics.prompt_tokens += metrics.metrics.prompt_tokens; - modelSpend[model].metrics.completion_tokens += metrics.metrics.completion_tokens; + modelSpend[model].metrics.prompt_tokens += + metrics.metrics.prompt_tokens; + modelSpend[model].metrics.completion_tokens += + metrics.metrics.completion_tokens; modelSpend[model].metrics.total_tokens += metrics.metrics.total_tokens; modelSpend[model].metrics.api_requests += metrics.metrics.api_requests; - modelSpend[model].metrics.successful_requests += metrics.metrics.successful_requests || 0; - modelSpend[model].metrics.failed_requests += metrics.metrics.failed_requests || 0; - modelSpend[model].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0; - modelSpend[model].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0; + modelSpend[model].metrics.successful_requests += + metrics.metrics.successful_requests || 0; + modelSpend[model].metrics.failed_requests += + metrics.metrics.failed_requests || 0; + modelSpend[model].metrics.cache_read_input_tokens += + metrics.metrics.cache_read_input_tokens || 0; + modelSpend[model].metrics.cache_creation_input_tokens += + metrics.metrics.cache_creation_input_tokens || 0; }); }); - + return Object.entries(modelSpend) .map(([model, metrics]) => ({ key: model, @@ -116,7 +153,7 @@ const NewUsagePage: React.FC = ({ requests: metrics.metrics.api_requests, successful_requests: metrics.metrics.successful_requests, failed_requests: metrics.metrics.failed_requests, - tokens: metrics.metrics.total_tokens + tokens: metrics.metrics.total_tokens, })) .sort((a, b) => b.spend - a.spend) .slice(0, 5); @@ -125,51 +162,60 @@ const NewUsagePage: React.FC = ({ // Calculate provider spend from the breakdown data const getProviderSpend = () => { const providerSpend: { [key: string]: MetricWithMetadata } = {}; - userSpendData.results.forEach(day => { - Object.entries(day.breakdown.providers || {}).forEach(([provider, metrics]) => { - if (!providerSpend[provider]) { - providerSpend[provider] = { - 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: {} - }; + userSpendData.results.forEach((day) => { + Object.entries(day.breakdown.providers || {}).forEach( + ([provider, metrics]) => { + if (!providerSpend[provider]) { + providerSpend[provider] = { + 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: {}, + }; + } + providerSpend[provider].metrics.spend += metrics.metrics.spend; + providerSpend[provider].metrics.prompt_tokens += + metrics.metrics.prompt_tokens; + providerSpend[provider].metrics.completion_tokens += + metrics.metrics.completion_tokens; + providerSpend[provider].metrics.total_tokens += + metrics.metrics.total_tokens; + providerSpend[provider].metrics.api_requests += + metrics.metrics.api_requests; + providerSpend[provider].metrics.successful_requests += + metrics.metrics.successful_requests || 0; + providerSpend[provider].metrics.failed_requests += + metrics.metrics.failed_requests || 0; + providerSpend[provider].metrics.cache_read_input_tokens += + metrics.metrics.cache_read_input_tokens || 0; + providerSpend[provider].metrics.cache_creation_input_tokens += + metrics.metrics.cache_creation_input_tokens || 0; } - providerSpend[provider].metrics.spend += metrics.metrics.spend; - providerSpend[provider].metrics.prompt_tokens += metrics.metrics.prompt_tokens; - providerSpend[provider].metrics.completion_tokens += metrics.metrics.completion_tokens; - providerSpend[provider].metrics.total_tokens += metrics.metrics.total_tokens; - providerSpend[provider].metrics.api_requests += metrics.metrics.api_requests; - providerSpend[provider].metrics.successful_requests += metrics.metrics.successful_requests || 0; - providerSpend[provider].metrics.failed_requests += metrics.metrics.failed_requests || 0; - providerSpend[provider].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0; - providerSpend[provider].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0; - }); + ); }); - - return Object.entries(providerSpend) - .map(([provider, metrics]) => ({ - provider, - spend: metrics.metrics.spend, - requests: metrics.metrics.api_requests, - successful_requests: metrics.metrics.successful_requests, - failed_requests: metrics.metrics.failed_requests, - tokens: metrics.metrics.total_tokens - })); + + return Object.entries(providerSpend).map(([provider, metrics]) => ({ + provider, + spend: metrics.metrics.spend, + requests: metrics.metrics.api_requests, + successful_requests: metrics.metrics.successful_requests, + failed_requests: metrics.metrics.failed_requests, + tokens: metrics.metrics.total_tokens, + })); }; // Calculate top API keys from the breakdown data const getTopKeys = () => { const keySpend: { [key: string]: KeyMetricWithMetadata } = {}; - userSpendData.results.forEach(day => { + userSpendData.results.forEach((day) => { Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => { if (!keySpend[key]) { keySpend[key] = { @@ -182,25 +228,30 @@ const NewUsagePage: React.FC = ({ successful_requests: 0, failed_requests: 0, cache_read_input_tokens: 0, - cache_creation_input_tokens: 0 + cache_creation_input_tokens: 0, }, metadata: { - key_alias: metrics.metadata.key_alias - } + key_alias: metrics.metadata.key_alias, + }, }; } keySpend[key].metrics.spend += metrics.metrics.spend; keySpend[key].metrics.prompt_tokens += metrics.metrics.prompt_tokens; - keySpend[key].metrics.completion_tokens += metrics.metrics.completion_tokens; + keySpend[key].metrics.completion_tokens += + metrics.metrics.completion_tokens; keySpend[key].metrics.total_tokens += metrics.metrics.total_tokens; keySpend[key].metrics.api_requests += metrics.metrics.api_requests; - keySpend[key].metrics.successful_requests += metrics.metrics.successful_requests; - keySpend[key].metrics.failed_requests += metrics.metrics.failed_requests; - keySpend[key].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0; - keySpend[key].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0; + keySpend[key].metrics.successful_requests += + metrics.metrics.successful_requests; + keySpend[key].metrics.failed_requests += + metrics.metrics.failed_requests; + keySpend[key].metrics.cache_read_input_tokens += + metrics.metrics.cache_read_input_tokens || 0; + keySpend[key].metrics.cache_creation_input_tokens += + metrics.metrics.cache_creation_input_tokens || 0; }); }); - + return Object.entries(keySpend) .map(([api_key, metrics]) => ({ api_key, @@ -216,14 +267,20 @@ const NewUsagePage: React.FC = ({ // Create new Date objects to avoid mutating the original dates const startTime = new Date(dateValue.from); const endTime = new Date(dateValue.to); - + try { // Get first page - const firstPageData = await userDailyActivityCall(accessToken, startTime, endTime); - + const firstPageData = await userDailyActivityCall( + accessToken, + startTime, + endTime + ); + // Check if we need to fetch more pages if (firstPageData.metadata.total_pages > 10) { - throw new Error("Too many pages of data (>10). Please select a smaller date range."); + throw new Error( + "Too many pages of data (>10). Please select a smaller date range." + ); } // If only one page, just set the data @@ -235,23 +292,32 @@ const NewUsagePage: React.FC = ({ // Fetch all pages const allResults = [...firstPageData.results]; const aggregatedMetadata = { ...firstPageData.metadata }; - + for (let page = 2; page <= firstPageData.metadata.total_pages; page++) { - const pageData = await userDailyActivityCall(accessToken, startTime, endTime, page); + const pageData = await userDailyActivityCall( + accessToken, + startTime, + endTime, + page + ); allResults.push(...pageData.results); if (pageData.metadata) { - aggregatedMetadata.total_spend += pageData.metadata.total_spend || 0; - aggregatedMetadata.total_api_requests += pageData.metadata.total_api_requests || 0; - aggregatedMetadata.total_successful_requests += pageData.metadata.total_successful_requests || 0; - aggregatedMetadata.total_failed_requests += pageData.metadata.total_failed_requests || 0; - aggregatedMetadata.total_tokens += pageData.metadata.total_tokens || 0; + aggregatedMetadata.total_spend += pageData.metadata.total_spend || 0; + aggregatedMetadata.total_api_requests += + pageData.metadata.total_api_requests || 0; + aggregatedMetadata.total_successful_requests += + pageData.metadata.total_successful_requests || 0; + aggregatedMetadata.total_failed_requests += + pageData.metadata.total_failed_requests || 0; + aggregatedMetadata.total_tokens += + pageData.metadata.total_tokens || 0; } } // Combine all results with the first page's metadata setUserSpendData({ results: allResults, - metadata: aggregatedMetadata + metadata: aggregatedMetadata, }); } catch (error) { console.error("Error fetching user spend data:", error); @@ -268,16 +334,33 @@ const NewUsagePage: React.FC = ({ return (
- {all_admin_roles.includes(userRole || "") ? - - Note: If you see key/model-level inconsistencies between Global View and Team Usage, it's because the Global View was missing spend when user_id = null, prior to v1.71.2. Learn more here. - - : null} + {all_admin_roles.includes(userRole || "") ? ( + + Note: If you see key/model-level inconsistencies between Global View + and Team Usage, it's because the Global View was missing spend + when user_id = null, prior to v1.71.2.{" "} + + Learn more here + + . + + ) : null} - {all_admin_roles.includes(userRole || "") ? Global Usage : Your Usage} + {all_admin_roles.includes(userRole || "") ? ( + Global Usage + ) : ( + Your Usage + )} Team Usage - {all_admin_roles.includes(userRole || "") ? Tag Usage : <>} + {all_admin_roles.includes(userRole || "") ? ( + Tag Usage + ) : ( + <> + )} {/* Your Usage Panel */} @@ -305,9 +388,18 @@ const NewUsagePage: React.FC = ({ {/* Total Spend Card */} - Project Spend {new Date().toLocaleString('default', { month: 'long' })} 1 - {new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0).getDate()} + Project Spend{" "} + {new Date().toLocaleString("default", { + month: "long", + })}{" "} + 1 -{" "} + {new Date( + new Date().getFullYear(), + new Date().getMonth() + 1, + 0 + ).getDate()} - + = ({ Total Requests - {userSpendData.metadata?.total_api_requests?.toLocaleString() || 0} + {userSpendData.metadata?.total_api_requests?.toLocaleString() || + 0} Successful Requests - {userSpendData.metadata?.total_successful_requests?.toLocaleString() || 0} + {userSpendData.metadata?.total_successful_requests?.toLocaleString() || + 0} Failed Requests - {userSpendData.metadata?.total_failed_requests?.toLocaleString() || 0} + {userSpendData.metadata?.total_failed_requests?.toLocaleString() || + 0} Total Tokens - {userSpendData.metadata?.total_tokens?.toLocaleString() || 0} + {userSpendData.metadata?.total_tokens?.toLocaleString() || + 0} Average Cost per Request - ${formatNumberWithCommas(((totalSpend || 0) / (userSpendData.metadata?.total_api_requests || 1)), 4)} + $ + {formatNumberWithCommas( + (totalSpend || 0) / + (userSpendData.metadata?.total_api_requests || + 1), + 4 + )} @@ -361,13 +463,15 @@ const NewUsagePage: React.FC = ({ Daily Spend - new Date(a.date).getTime() - new Date(b.date).getTime() + data={[...userSpendData.results].sort( + (a, b) => + new Date(a.date).getTime() - + new Date(b.date).getTime() )} index="date" categories={["metrics.spend"]} colors={["cyan"]} - valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`} + valueFormatter={valueFormatterSpend} yAxisWidth={100} showLegend={false} customTooltip={({ payload, active }) => { @@ -376,11 +480,25 @@ const NewUsagePage: React.FC = ({ return (

{data.date}

-

Spend: ${formatNumberWithCommas(data.metrics.spend, 2)}

-

Requests: {data.metrics.api_requests}

-

Successful: {data.metrics.successful_requests}

-

Failed: {data.metrics.failed_requests}

-

Tokens: {data.metrics.total_tokens}

+

+ Spend: $ + {formatNumberWithCommas( + data.metrics.spend, + 2 + )} +

+

+ Requests: {data.metrics.api_requests} +

+

+ Successful: {data.metrics.successful_requests} +

+

+ Failed: {data.metrics.failed_requests} +

+

+ Tokens: {data.metrics.total_tokens} +

); }} @@ -414,7 +532,7 @@ const NewUsagePage: React.FC = ({ index="key" categories={["spend"]} colors={["cyan"]} - valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`} + valueFormatter={valueFormatterSpend} layout="vertical" yAxisWidth={200} showLegend={false} @@ -424,11 +542,25 @@ const NewUsagePage: React.FC = ({ return (

{data.key}

-

Spend: ${formatNumberWithCommas(data.spend, 2)}

-

Total Requests: {data.requests.toLocaleString()}

-

Successful: {data.successful_requests.toLocaleString()}

-

Failed: {data.failed_requests.toLocaleString()}

-

Tokens: {data.tokens.toLocaleString()}

+

+ Spend: $ + {formatNumberWithCommas(data.spend, 2)} +

+

+ Total Requests:{" "} + {data.requests.toLocaleString()} +

+

+ Successful:{" "} + {data.successful_requests.toLocaleString()} +

+

+ Failed:{" "} + {data.failed_requests.toLocaleString()} +

+

+ Tokens: {data.tokens.toLocaleString()} +

); }} @@ -449,7 +581,9 @@ const NewUsagePage: React.FC = ({ data={getProviderSpend()} index="provider" category="spend" - valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`} + valueFormatter={(value) => + `$${formatNumberWithCommas(value, 2)}` + } colors={["cyan"]} /> @@ -459,29 +593,39 @@ const NewUsagePage: React.FC = ({ Provider Spend - Successful - Failed + + Successful + + + Failed + Tokens {getProviderSpend() - .filter(provider => provider.spend > 0) + .filter((provider) => provider.spend > 0) .map((provider) => ( {provider.provider} - ${formatNumberWithCommas(provider.spend, 2)} - - - {provider.successful_requests.toLocaleString()} - - - {provider.failed_requests.toLocaleString()} - - {provider.tokens.toLocaleString()} - - ))} + $ + {formatNumberWithCommas( + provider.spend, + 2 + )} + + + {provider.successful_requests.toLocaleString()} + + + {provider.failed_requests.toLocaleString()} + + + {provider.tokens.toLocaleString()} + + + ))} @@ -490,7 +634,6 @@ const NewUsagePage: React.FC = ({ {/* Usage Metrics */} - @@ -507,22 +650,24 @@ const NewUsagePage: React.FC = ({ {/* Team Usage Panel */} - ({ - label: team.team_alias, - value: team.team_id - })) || null} + entityList={ + teams?.map((team) => ({ + label: team.team_alias, + value: team.team_id, + })) || null + } premiumUser={premiumUser} /> {/* Tag Usage Panel */} - = ({ premiumUser={premiumUser} /> -
@@ -561,16 +705,16 @@ const getModelActivityData = (userSpendData: { modelData[model] = { total_requests: 0, total_tokens: 0, - daily_data: [] + daily_data: [], }; } - + modelData[model].total_requests += metrics.metrics.api_requests; modelData[model].total_tokens += metrics.metrics.total_tokens; modelData[model].daily_data.push({ date: day.date, api_requests: metrics.metrics.api_requests, - total_tokens: metrics.metrics.total_tokens + total_tokens: metrics.metrics.total_tokens, }); }); }); @@ -578,14 +722,4 @@ const getModelActivityData = (userSpendData: { return modelData; }; -// Add this helper function for number formatting -function valueFormatterNumbers(number: number) { - const formatter = new Intl.NumberFormat('en-US', { - maximumFractionDigits: 0, - notation: 'compact', - compactDisplay: 'short', - }); - return formatter.format(number); -} - -export default NewUsagePage; \ No newline at end of file +export default NewUsagePage; diff --git a/ui/litellm-dashboard/src/components/usage.tsx b/ui/litellm-dashboard/src/components/usage.tsx index e6e957fc51..fbe7dde172 100644 --- a/ui/litellm-dashboard/src/components/usage.tsx +++ b/ui/litellm-dashboard/src/components/usage.tsx @@ -994,7 +994,7 @@ const UsagePage: React.FC = ({ Spend Per Tag - Get Started Tracking cost per tag here + Get Started by Tracking cost per tag here = 1000000) { + return (number / 1000000).toFixed(2) + "M"; + } + if (number >= 1000) { + return number / 1000 + "k"; + } + return number.toString(); +} + +export function valueFormatterSpend(number: number) { + if (number === 0) return "$0"; + if (number >= 1000000) { + return "$" + number / 1000000 + "M"; + } + if (number >= 1000) { + return "$" + number / 1000 + "k"; + } + return "$" + number; +}