From ac827e669a5e726bc02aea3d3d4266897d4e5410 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 16 Jul 2026 07:17:22 +0000 Subject: [PATCH] feat(usage): show smart router model breakdowns Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../common_daily_activity.py | 16 ++++ .../common_daily_activity.py | 1 + .../test_common_daily_activity.py | 86 ++++++++++++++++- .../components/UsagePageView.test.tsx | 1 + .../_components/components/UsagePageView.tsx | 8 ++ .../components/KeyModelUsageView.test.tsx | 75 ++++++++++++++- .../components/KeyModelUsageView.tsx | 34 +++++-- .../src/components/UsagePage/types.ts | 4 + .../src/components/activity_metrics.test.tsx | 69 +++++++++++++- .../src/components/activity_metrics.tsx | 92 ++++++++++++------- ui/litellm-dashboard/src/lib/http/schema.d.ts | 4 + 11 files changed, 345 insertions(+), 45 deletions(-) diff --git a/litellm/proxy/management_endpoints/common_daily_activity.py b/litellm/proxy/management_endpoints/common_daily_activity.py index 60cb3ccd30d..113c75bfa4e 100644 --- a/litellm/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/proxy/management_endpoints/common_daily_activity.py @@ -133,6 +133,13 @@ def update_breakdown_metrics( breakdown.model_groups[record.model_group].metrics = update_metrics( breakdown.model_groups[record.model_group].metrics, record ) + if record.model: + if record.model not in breakdown.model_groups[record.model_group].model_breakdown: + breakdown.model_groups[record.model_group].model_breakdown[record.model] = SpendMetrics() + breakdown.model_groups[record.model_group].model_breakdown[record.model] = update_metrics( + breakdown.model_groups[record.model_group].model_breakdown[record.model], + record, + ) # Update API key breakdown for this model if record.api_key not in breakdown.model_groups[record.model_group].api_key_breakdown: @@ -484,6 +491,7 @@ def _build_aggregated_sql_query( (date, model), (date, model, api_key), (date, model_group), + (date, model, model_group), (date, model_group, api_key), (date, custom_llm_provider), (date, custom_llm_provider, api_key), @@ -588,6 +596,7 @@ _GROUP_DATE_API_KEY = 31 # 0b0011111 _GROUP_DATE_MODEL = 47 # 0b0101111 _GROUP_DATE_MODEL_API_KEY = 15 # 0b0001111 _GROUP_DATE_MODEL_GROUP = 55 # 0b0110111 +_GROUP_DATE_MODEL_MODEL_GROUP = 39 # 0b0100111 _GROUP_DATE_MODEL_GROUP_API_KEY = 23 # 0b0010111 _GROUP_DATE_PROVIDER = 59 # 0b0111011 _GROUP_DATE_PROVIDER_API_KEY = 27 # 0b0011011 @@ -695,6 +704,13 @@ def _aggregate_grouping_sets_records_sync( elif level == _GROUP_DATE_MODEL_GROUP: if record.model_group: assign_metric_with_metadata(breakdown.model_groups, record.model_group, metrics) + elif level == _GROUP_DATE_MODEL_MODEL_GROUP: + if record.model_group and record.model: + parent = breakdown.model_groups.get(record.model_group) + if parent is None: + parent = MetricWithMetadata(metrics=SpendMetrics(), metadata={}) + breakdown.model_groups[record.model_group] = parent + parent.model_breakdown[record.model] = metrics elif level == _GROUP_DATE_MODEL_GROUP_API_KEY: if record.model_group and record.api_key: assign_api_key_breakdown( diff --git a/litellm/types/proxy/management_endpoints/common_daily_activity.py b/litellm/types/proxy/management_endpoints/common_daily_activity.py index b57d39f4c1a..0eac9af3350 100644 --- a/litellm/types/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/types/proxy/management_endpoints/common_daily_activity.py @@ -49,6 +49,7 @@ class MetricWithMetadata(MetricBase): metadata: Dict[str, Any] = Field(default_factory=dict) # API key breakdown for this metric (e.g., which API keys are using this MCP server) api_key_breakdown: Dict[str, KeyMetricWithMetadata] = Field(default_factory=dict) # api_key -> {metrics, metadata} + model_breakdown: Dict[str, SpendMetrics] = Field(default_factory=dict) class BreakdownMetrics(BaseModel): diff --git a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py index b882090e8f1..5c0a8ef0e1b 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py +++ b/tests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py @@ -11,15 +11,20 @@ sys.path.insert( from litellm.proxy.management_endpoints.common_daily_activity import ( _adjust_dates_for_timezone, + _aggregate_grouping_sets_records_sync, _build_aggregated_sql_query, _is_user_agent_tag, _record_to_spend_metrics, get_api_key_metadata, get_daily_activity, get_daily_activity_aggregated, + update_breakdown_metrics, update_metrics, ) -from litellm.types.proxy.management_endpoints.common_daily_activity import SpendMetrics +from litellm.types.proxy.management_endpoints.common_daily_activity import ( + BreakdownMetrics, + SpendMetrics, +) @pytest.mark.asyncio @@ -837,6 +842,85 @@ class TestBuildAggregatedSqlQuery: assert "model = $4" in sql assert "api_key = $5" in sql + def test_includes_model_breakdown_for_model_groups(self): + sql, _ = _build_aggregated_sql_query( + table_name="litellm_dailyuserspend", + entity_id_field="user_id", + entity_id=None, + start_date="2026-05-29", + end_date="2026-05-29", + model=None, + api_key=None, + ) + + assert "(date, model, model_group)" in sql + + +def test_aggregate_grouping_sets_populates_model_group_model_breakdown(): + record = SimpleNamespace( + group_level=39, + date="2026-05-29", + api_key=None, + model="bedrock/claude-opus-4-8", + model_group="smart-router", + custom_llm_provider=None, + mcp_namespaced_tool_name=None, + endpoint=None, + spend=12.5, + prompt_tokens=8000, + completion_tokens=2000, + cache_read_input_tokens=4000, + cache_creation_input_tokens=1000, + api_requests=8, + successful_requests=8, + failed_requests=0, + ) + + result = _aggregate_grouping_sets_records_sync(records=[record], api_key_metadata={}) + + metrics = result["results"][0].breakdown.model_groups["smart-router"].model_breakdown[ + "bedrock/claude-opus-4-8" + ] + assert metrics.api_requests == 8 + assert metrics.total_tokens == 10000 + assert metrics.cache_read_input_tokens == 4000 + assert metrics.cache_creation_input_tokens == 1000 + + +def test_update_breakdown_metrics_populates_model_group_model_breakdown(): + record = SimpleNamespace( + model="bedrock/claude-sonnet-4-5", + model_group="smart-router", + api_key="key-hash", + custom_llm_provider="bedrock", + mcp_namespaced_tool_name=None, + endpoint=None, + spend=4.5, + prompt_tokens=3000, + completion_tokens=1000, + cache_read_input_tokens=1500, + cache_creation_input_tokens=500, + api_requests=2, + successful_requests=2, + failed_requests=0, + ) + + result = update_breakdown_metrics( + breakdown=BreakdownMetrics(), + record=record, + model_metadata={}, + provider_metadata={}, + api_key_metadata={}, + ) + + metrics = result.model_groups["smart-router"].model_breakdown[ + "bedrock/claude-sonnet-4-5" + ] + assert metrics.api_requests == 2 + assert metrics.total_tokens == 4000 + assert metrics.cache_read_input_tokens == 1500 + assert metrics.cache_creation_input_tokens == 500 + @pytest.mark.asyncio async def test_get_daily_activity_aggregated_empty_result_set(): diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx index cf122137f91..716d8cdc971 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.test.tsx @@ -1157,6 +1157,7 @@ describe("UsagePage", () => { expect(screen.getByText("Cost")).toBeInTheDocument(); expect(screen.getByText("Model Activity")).toBeInTheDocument(); + expect(screen.getByText("Public Model Activity")).toBeInTheDocument(); expect(screen.getByText("Key Activity")).toBeInTheDocument(); expect(screen.getByText("MCP Server Activity")).toBeInTheDocument(); expect(screen.getByText("Endpoint Activity")).toBeInTheDocument(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx index d2f75609d18..810168e8456 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx @@ -439,6 +439,10 @@ const UsagePage: React.FC = ({ teams, organizations }) => { [userSpendData.results], ); const modelMetrics = useMemo(() => processActivityData(userSpendData, "models", teams), [userSpendData, teams]); + const modelGroupMetrics = useMemo( + () => processActivityData(userSpendData, "model_groups", teams), + [userSpendData, teams], + ); const keyMetrics = useMemo(() => processActivityData(userSpendData, "api_keys", teams), [userSpendData, teams]); const mcpServerMetrics = useMemo( () => processActivityData(userSpendData, "mcp_servers", teams), @@ -534,6 +538,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { Cost Model Activity + Public Model Activity Key Activity MCP Server Activity Endpoint Activity @@ -841,6 +846,9 @@ const UsagePage: React.FC = ({ teams, organizations }) => { + + + diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx index 024226c3c03..ed13c533637 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it } from "vitest"; import KeyModelUsageView from "./KeyModelUsageView"; @@ -13,6 +13,9 @@ describe("KeyModelUsageView", () => { successful_requests: 100, failed_requests: 5, tokens: 50000, + request_share: 34.4, + cache_read_input_tokens: 20000, + cache_creation_input_tokens: 10000, }, { model: "gpt-3.5-turbo", @@ -21,6 +24,9 @@ describe("KeyModelUsageView", () => { successful_requests: 195, failed_requests: 5, tokens: 100000, + request_share: 65.6, + cache_read_input_tokens: 40000, + cache_creation_input_tokens: 20000, }, ]; @@ -59,9 +65,12 @@ describe("KeyModelUsageView", () => { render(); expect(screen.getByText("Model")).toBeInTheDocument(); expect(screen.getByText("Spend (USD)")).toBeInTheDocument(); + expect(screen.getByText("Requests")).toBeInTheDocument(); + expect(screen.getByText("Total Tokens")).toBeInTheDocument(); expect(screen.getByText("Successful")).toBeInTheDocument(); expect(screen.getByText("Failed")).toBeInTheDocument(); - expect(screen.getByText("Tokens")).toBeInTheDocument(); + expect(screen.getByText("Cache Read")).toBeInTheDocument(); + expect(screen.getByText("Cache Write")).toBeInTheDocument(); }); it("should display model data in table view", () => { @@ -102,6 +111,12 @@ describe("KeyModelUsageView", () => { expect(screen.getByText("$1,234,567.89")).toBeInTheDocument(); }); + it("should display request counts and request share", () => { + render(); + expect(screen.getByText("105 (34.4%)")).toBeInTheDocument(); + expect(screen.getByText("200 (65.6%)")).toBeInTheDocument(); + }); + it("should display successful requests with green styling", () => { render(); const successfulElements = screen.getAllByText("100"); @@ -109,6 +124,13 @@ describe("KeyModelUsageView", () => { expect(greenElement).toBeDefined(); }); + it("should display prompt cache tokens", () => { + render(); + expect(screen.getAllByText("20,000")).toHaveLength(2); + expect(screen.getByText("10,000")).toBeInTheDocument(); + expect(screen.getByText("40,000")).toBeInTheDocument(); + }); + it("should display failed requests with red styling", () => { render(); const failedElements = screen.getAllByText("5"); @@ -233,6 +255,22 @@ describe("KeyModelUsageView", () => { expect(screen.getByText("model-10")).toBeInTheDocument(); }); + it("should format request counts with toLocaleString", () => { + const modelsWithLargeNumbers: TopModelData[] = [ + { + model: "test-model", + spend: 100, + requests: 999999, + successful_requests: 999999, + failed_requests: 1, + tokens: 1000, + request_share: 100, + }, + ]; + render(); + expect(screen.getByText("999,999 (100.0%)")).toBeInTheDocument(); + }); + it("should format successful requests with toLocaleString", () => { const modelsWithLargeNumbers: TopModelData[] = [ { @@ -263,6 +301,39 @@ describe("KeyModelUsageView", () => { expect(screen.getByText("999,999")).toBeInTheDocument(); }); + it("should display zero percent for missing request_share", () => { + const modelsWithMissingFields: TopModelData[] = [ + { + model: "test-model", + spend: 100, + requests: 10, + successful_requests: 10, + failed_requests: 0, + tokens: 1000, + }, + ]; + render(); + expect(screen.getByText("10 (0.0%)")).toBeInTheDocument(); + }); + + it("should display zero for missing cache token fields", () => { + const modelsWithMissingFields: TopModelData[] = [ + { + model: "test-model", + spend: 100, + requests: 10, + successful_requests: 10, + failed_requests: 0, + tokens: 1000, + }, + ]; + render(); + const row = screen.getByText("test-model").closest("tr"); + const cells = within(row as HTMLTableRowElement).getAllByRole("cell"); + expect(cells[6]).toHaveTextContent("0"); + expect(cells[7]).toHaveTextContent("0"); + }); + it("should display zero for missing successful_requests", () => { const modelsWithMissingFields: TopModelData[] = [ { diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx index ed78b7e3dba..260d5dcd2d6 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx @@ -28,6 +28,22 @@ const columns: ColumnsType = [ key: "spend", render: (value) => , }, + { + title: "Requests", + dataIndex: "requests", + key: "requests", + render: (value, record) => ( + + {value?.toLocaleString() || 0} ({record.request_share?.toFixed(1) || "0.0"}%) + + ), + }, + { + title: "Total Tokens", + dataIndex: "tokens", + key: "tokens", + render: (value) => value?.toLocaleString() || 0, + }, { title: "Successful", dataIndex: "successful_requests", @@ -41,9 +57,15 @@ const columns: ColumnsType = [ render: (value) => {value?.toLocaleString() || 0}, }, { - title: "Tokens", - dataIndex: "tokens", - key: "tokens", + title: "Cache Read", + dataIndex: "cache_read_input_tokens", + key: "cache_read_input_tokens", + render: (value) => value?.toLocaleString() || 0, + }, + { + title: "Cache Write", + dataIndex: "cache_creation_input_tokens", + key: "cache_creation_input_tokens", render: (value) => value?.toLocaleString() || 0, }, ]; @@ -81,11 +103,11 @@ const KeyModelUsageView: React.FC = ({ topModels }) => {
({ key: m.model, spend: m.spend }))} + data={topModels.map((model) => ({ key: model.model, requests: model.requests }))} index="key" - categories={["spend"]} + categories={["requests"]} colors={["cyan"]} - valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`} + valueFormatter={(value) => formatNumberWithCommas(value, 0)} layout="vertical" yAxisWidth={180} tickGap={5} diff --git a/ui/litellm-dashboard/src/components/UsagePage/types.ts b/ui/litellm-dashboard/src/components/UsagePage/types.ts index 6c1297613fa..106e978ee19 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/types.ts +++ b/ui/litellm-dashboard/src/components/UsagePage/types.ts @@ -30,6 +30,7 @@ export interface MetricWithMetadata { metrics: SpendMetrics; metadata: object; api_key_breakdown: { [key: string]: KeyMetricWithMetadata }; + model_breakdown?: { [key: string]: SpendMetrics }; } export interface KeyMetricWithMetadata { @@ -56,9 +57,12 @@ export interface TopModelData { model: string; spend: number; requests: number; + request_share?: number; successful_requests: number; failed_requests: number; tokens: number; + cache_read_input_tokens?: number; + cache_creation_input_tokens?: number; } export interface ModelActivityData { diff --git a/ui/litellm-dashboard/src/components/activity_metrics.test.tsx b/ui/litellm-dashboard/src/components/activity_metrics.test.tsx index f37643f2366..b39555a31c3 100644 --- a/ui/litellm-dashboard/src/components/activity_metrics.test.tsx +++ b/ui/litellm-dashboard/src/components/activity_metrics.test.tsx @@ -1195,8 +1195,8 @@ describe("processActivityData", () => { api_requests: 60, successful_requests: 57, failed_requests: 3, - cache_read_input_tokens: 0, - cache_creation_input_tokens: 0, + cache_read_input_tokens: 6000, + cache_creation_input_tokens: 3000, }, metadata: { key_alias: "key-alias-1", team_id: "team1" }, }, @@ -1234,6 +1234,71 @@ describe("processActivityData", () => { expect(result["api-key-hash-1"].top_models[0].model).toBe("gpt-4"); expect(result["api-key-hash-1"].top_models[0].spend).toBe(60.0); expect(result["api-key-hash-1"].top_models[0].requests).toBe(60); + expect(result["api-key-hash-1"].top_models[0].request_share).toBe(100); + expect(result["api-key-hash-1"].top_models[0].cache_read_input_tokens).toBe(6000); + expect(result["api-key-hash-1"].top_models[0].cache_creation_input_tokens).toBe(3000); + }); + + it("should populate selected model usage for public model groups", () => { + const dailyActivity: { results: DailyData[] } = { + results: [ + { + date: "2025-01-01", + metrics: EMPTY_SPEND_METRICS, + breakdown: { + ...EMPTY_BREAKDOWN, + model_groups: { + "smart-router": { + metrics: { + ...EMPTY_SPEND_METRICS, + api_requests: 10, + total_tokens: 11000, + cache_read_input_tokens: 4000, + cache_creation_input_tokens: 2000, + }, + metadata: {}, + api_key_breakdown: {}, + model_breakdown: { + "bedrock/claude-opus-4-8": { + ...EMPTY_SPEND_METRICS, + api_requests: 8, + total_tokens: 8000, + cache_read_input_tokens: 3000, + cache_creation_input_tokens: 1500, + }, + "bedrock/claude-sonnet-4-5": { + ...EMPTY_SPEND_METRICS, + api_requests: 2, + total_tokens: 3000, + cache_read_input_tokens: 1000, + cache_creation_input_tokens: 500, + }, + }, + }, + }, + }, + }, + ], + }; + + const result = processActivityData(dailyActivity, "model_groups"); + const expectedOpusMetrics = { + model: "bedrock/claude-opus-4-8", + requests: 8, + request_share: 80, + tokens: 8000, + cache_read_input_tokens: 3000, + cache_creation_input_tokens: 1500, + }; + + expect(result["smart-router"].top_models).toEqual([ + expect.objectContaining(expectedOpusMetrics), + expect.objectContaining({ + model: "bedrock/claude-sonnet-4-5", + requests: 2, + request_share: 20, + }), + ]); }); it("should not process api_key_breakdown when key is api_keys", () => { diff --git a/ui/litellm-dashboard/src/components/activity_metrics.tsx b/ui/litellm-dashboard/src/components/activity_metrics.tsx index 54ac5ae0ee6..027a4b7a3ed 100644 --- a/ui/litellm-dashboard/src/components/activity_metrics.tsx +++ b/ui/litellm-dashboard/src/components/activity_metrics.tsx @@ -6,7 +6,14 @@ import { Collapse } from "antd"; import React from "react"; import { Team } from "./key_team_helpers/key_list"; import KeyModelUsageView from "./UsagePage/components/KeyModelUsageView"; -import { DailyData, KeyMetricWithMetadata, ModelActivityData, TopApiKeyData, TopModelData } from "./UsagePage/types"; +import { + DailyData, + KeyMetricWithMetadata, + ModelActivityData, + SpendMetrics, + TopApiKeyData, + TopModelData, +} from "./UsagePage/types"; import { valueFormatter } from "./UsagePage/utils/value_formatters"; interface ActivityMetricsProps { @@ -362,7 +369,7 @@ export const formatKeyLabel = (modelData: KeyMetricWithMetadata, model: string, // Process data function export const processActivityData = ( dailyActivity: { results: DailyData[] }, - key: "models" | "api_keys" | "mcp_servers" | "entities", + key: "models" | "model_groups" | "api_keys" | "mcp_servers" | "entities", teams: Team[] = [], ): Record => { const modelMetrics: Record = {}; @@ -455,41 +462,58 @@ 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 = {}; + if (key === "api_keys" || key === "model_groups") { + Object.entries(modelMetrics).forEach(([metricName]) => { + const modelMetricsByDay = dailyActivity.results.flatMap((day) => { + const perModelMetrics: Record = + key === "api_keys" + ? Object.fromEntries( + Object.entries(day.breakdown.models || {}).flatMap(([modelName, modelData]) => { + const keyData = modelData.api_key_breakdown?.[metricName]; + return keyData ? [[modelName, keyData.metrics]] : []; + }), + ) + : day.breakdown.model_groups?.[metricName]?.model_breakdown || {}; - // 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; - } - } - }); + return Object.entries(perModelMetrics); }); + const modelBreakdown = modelMetricsByDay.reduce>((totals, [modelName, metrics]) => { + const current = totals[modelName] ?? { + model: modelName, + spend: 0, + requests: 0, + request_share: 0, + successful_requests: 0, + failed_requests: 0, + tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }; - // Sort by spend - modelMetrics[apiKeyHash].top_models = Object.values(modelBreakdown).sort((a, b) => b.spend - a.spend); + return { + ...totals, + [modelName]: { + model: modelName, + spend: current.spend + metrics.spend, + requests: current.requests + metrics.api_requests, + request_share: 0, + successful_requests: current.successful_requests + (metrics.successful_requests || 0), + failed_requests: current.failed_requests + (metrics.failed_requests || 0), + tokens: current.tokens + metrics.total_tokens, + cache_read_input_tokens: (current.cache_read_input_tokens ?? 0) + (metrics.cache_read_input_tokens || 0), + cache_creation_input_tokens: + (current.cache_creation_input_tokens ?? 0) + (metrics.cache_creation_input_tokens || 0), + }, + }; + }, {}); + + const totalRequests = Object.values(modelBreakdown).reduce((total, model) => total + model.requests, 0); + modelMetrics[metricName].top_models = Object.values(modelBreakdown) + .map((model) => ({ + ...model, + request_share: totalRequests === 0 ? 0 : (model.requests / totalRequests) * 100, + })) + .sort((a, b) => b.requests - a.requests); }); } diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 87c760257d1..26d43f0b847 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -27517,6 +27517,10 @@ export interface components { [key: string]: unknown; }; metrics: components["schemas"]["SpendMetrics"]; + /** Model Breakdown */ + model_breakdown?: { + [key: string]: components["schemas"]["SpendMetrics"]; + }; }; /** Mode */ Mode: {