diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx new file mode 100644 index 0000000000..a4292de480 --- /dev/null +++ b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx @@ -0,0 +1,108 @@ +import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { BarChart, Card, Title } from "@tremor/react"; +import { Table } from "antd"; +import type { ColumnsType } from "antd/es/table"; +import React, { useState } from "react"; +import { TopModelData } from "../types"; + +interface KeyModelUsageViewProps { + topModels: TopModelData[]; +} + +const VISIBLE_ROWS = 5; +// antd Table with size="small" has a row height of ~39px +const ANTD_SMALL_TABLE_ROW_HEIGHT = 39; + +const columns: ColumnsType = [ + { + title: "Model", + dataIndex: "model", + key: "model", + render: (value) => value || "-", + }, + { + title: "Spend (USD)", + dataIndex: "spend", + key: "spend", + render: (value) => `$${formatNumberWithCommas(value, 2)}`, + }, + { + title: "Successful", + dataIndex: "successful_requests", + key: "successful_requests", + render: (value) => {value?.toLocaleString() || 0}, + }, + { + title: "Failed", + dataIndex: "failed_requests", + key: "failed_requests", + render: (value) => {value?.toLocaleString() || 0}, + }, + { + title: "Tokens", + dataIndex: "tokens", + key: "tokens", + render: (value) => value?.toLocaleString() || 0, + }, +]; + +const KeyModelUsageView: React.FC = ({ topModels }) => { + const [viewMode, setViewMode] = useState<"chart" | "table">("table"); + + if (topModels.length === 0) { + return null; + } + + return ( + +
+ Model Usage +
+ + +
+
+ {viewMode === "chart" ? ( +
+ ({ key: m.model, spend: m.spend }))} + index="key" + categories={["spend"]} + colors={["cyan"]} + valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`} + layout="vertical" + yAxisWidth={180} + tickGap={5} + showLegend={false} + /> +
+ ) : ( + VISIBLE_ROWS + ? { y: VISIBLE_ROWS * ANTD_SMALL_TABLE_ROW_HEIGHT } + : undefined + } + /> + )} + + ); +}; + +export default KeyModelUsageView; diff --git a/ui/litellm-dashboard/src/components/UsagePage/types.ts b/ui/litellm-dashboard/src/components/UsagePage/types.ts index 2539dfaec1..c70a6a0ed9 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/types.ts +++ b/ui/litellm-dashboard/src/components/UsagePage/types.ts @@ -52,6 +52,15 @@ export interface TopApiKeyData { tokens: number; } +export interface TopModelData { + model: string; + spend: number; + requests: number; + successful_requests: number; + failed_requests: number; + tokens: number; +} + export interface ModelActivityData { label: string; total_requests: number; @@ -64,6 +73,7 @@ export interface ModelActivityData { completion_tokens: number; total_spend: number; top_api_keys: TopApiKeyData[]; + top_models: TopModelData[]; daily_data: { date: string; metrics: { diff --git a/ui/litellm-dashboard/src/components/activity_metrics.tsx b/ui/litellm-dashboard/src/components/activity_metrics.tsx index be12ae2a88..61006fa614 100644 --- a/ui/litellm-dashboard/src/components/activity_metrics.tsx +++ b/ui/litellm-dashboard/src/components/activity_metrics.tsx @@ -5,7 +5,8 @@ import { Collapse } from "antd"; import React from "react"; import { CustomLegend, CustomTooltip } from "./common_components/chartUtils"; import { Team } from "./key_team_helpers/key_list"; -import { DailyData, KeyMetricWithMetadata, ModelActivityData, TopApiKeyData } from "./UsagePage/types"; +import KeyModelUsageView from "./UsagePage/components/KeyModelUsageView"; +import { DailyData, KeyMetricWithMetadata, ModelActivityData, TopApiKeyData, TopModelData } from "./UsagePage/types"; import { valueFormatter } from "./UsagePage/utils/value_formatters"; interface ActivityMetricsProps { @@ -72,8 +73,29 @@ const ModelSection = ({ )} + {metrics.top_models && metrics.top_models.length > 0 && ( + + )} + + {/* Spend per day - Full width card */} + +
+ Spend per day + +
+ `$${formatNumberWithCommas(value, 2, true)}`} + yAxisWidth={72} + /> +
+ {/* Charts */} - +
Total Tokens @@ -111,22 +133,6 @@ const ModelSection = ({ /> - -
- Spend per day - -
- `$${formatNumberWithCommas(value, 2, true)}`} - yAxisWidth={72} - /> -
-
Success vs Failed Requests @@ -377,6 +383,7 @@ export const processActivityData = ( total_cache_read_input_tokens: 0, total_cache_creation_input_tokens: 0, top_api_keys: [], + top_models: [], daily_data: [], }; } @@ -444,6 +451,45 @@ export const processActivityData = ( }); } + // Process Model breakdowns for each API key (only when key is 'api_keys') + if (key === "api_keys") { + Object.entries(modelMetrics).forEach(([apiKeyHash, _]) => { + const modelBreakdown: Record = {}; + + // Aggregate Model data for this key across all days + // We need to look in breakdown.models[model].api_key_breakdown[apiKeyHash] + dailyActivity.results.forEach((day) => { + Object.entries(day.breakdown.models || {}).forEach(([modelName, modelData]) => { + if (modelData && "api_key_breakdown" in modelData) { + const keyDataForModel = modelData.api_key_breakdown?.[apiKeyHash]; + if (keyDataForModel) { + if (!modelBreakdown[modelName]) { + modelBreakdown[modelName] = { + model: modelName, + spend: 0, + requests: 0, + successful_requests: 0, + failed_requests: 0, + tokens: 0, + }; + } + + modelBreakdown[modelName].spend += keyDataForModel.metrics.spend; + modelBreakdown[modelName].requests += keyDataForModel.metrics.api_requests; + modelBreakdown[modelName].successful_requests += keyDataForModel.metrics.successful_requests || 0; + modelBreakdown[modelName].failed_requests += keyDataForModel.metrics.failed_requests || 0; + modelBreakdown[modelName].tokens += keyDataForModel.metrics.total_tokens; + } + } + }); + }); + + // Sort by spend + modelMetrics[apiKeyHash].top_models = Object.values(modelBreakdown) + .sort((a, b) => b.spend - a.spend); + }); + } + // Sort daily data Object.values(modelMetrics).forEach((metrics) => { metrics.daily_data.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());