From 3fb0044e4b668301f87f47081d73b9d6032de840 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Thu, 16 Jul 2026 07:45:27 +0000 Subject: [PATCH] fix(usage): nest router models under keys Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../common_daily_activity.py | 35 ++++- .../common_daily_activity.py | 1 + .../test_common_daily_activity.py | 53 +++++++ .../components/KeyModelUsageView.tsx | 5 +- .../src/components/UsagePage/types.ts | 9 ++ .../src/components/activity_metrics.test.tsx | 97 ++++++++++-- .../src/components/activity_metrics.tsx | 142 ++++++++++++------ ui/litellm-dashboard/src/lib/http/schema.d.ts | 4 + 8 files changed, 283 insertions(+), 63 deletions(-) diff --git a/litellm/proxy/management_endpoints/common_daily_activity.py b/litellm/proxy/management_endpoints/common_daily_activity.py index 113c75bfa4e..d598281ac73 100644 --- a/litellm/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/proxy/management_endpoints/common_daily_activity.py @@ -154,6 +154,14 @@ def update_breakdown_metrics( breakdown.model_groups[record.model_group].api_key_breakdown[record.api_key].metrics, record, ) + if record.model: + key_metrics = breakdown.model_groups[record.model_group].api_key_breakdown[record.api_key] + if record.model not in key_metrics.model_breakdown: + key_metrics.model_breakdown[record.model] = SpendMetrics() + key_metrics.model_breakdown[record.model] = update_metrics( + key_metrics.model_breakdown[record.model], + record, + ) if record.mcp_namespaced_tool_name: if record.mcp_namespaced_tool_name not in breakdown.mcp_servers: @@ -493,6 +501,7 @@ def _build_aggregated_sql_query( (date, model_group), (date, model, model_group), (date, model_group, api_key), + (date, api_key, model, model_group), (date, custom_llm_provider), (date, custom_llm_provider, api_key), (date, mcp_namespaced_tool_name), @@ -598,6 +607,7 @@ _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_MODEL_MODEL_GROUP_API_KEY = 7 # 0b0000111 _GROUP_DATE_PROVIDER = 59 # 0b0111011 _GROUP_DATE_PROVIDER_API_KEY = 27 # 0b0011011 _GROUP_DATE_MCP = 61 # 0b0111101 @@ -671,9 +681,14 @@ def _aggregate_grouping_sets_records_sync( if parent is None: parent = MetricWithMetadata(metrics=SpendMetrics(), metadata={}) target[parent_key] = parent - parent.api_key_breakdown[api_key] = KeyMetricWithMetadata( - metrics=metrics, metadata=_key_metadata(api_key_metadata, api_key) - ) + existing = parent.api_key_breakdown.get(api_key) + if existing is None: + parent.api_key_breakdown[api_key] = KeyMetricWithMetadata( + metrics=metrics, metadata=_key_metadata(api_key_metadata, api_key) + ) + else: + existing.metrics = metrics + existing.metadata = _key_metadata(api_key_metadata, api_key) for record in records: level = record.group_level @@ -719,6 +734,20 @@ def _aggregate_grouping_sets_records_sync( record.api_key, metrics, ) + elif level == _GROUP_DATE_MODEL_MODEL_GROUP_API_KEY: + if record.model_group and record.model and record.api_key: + parent = breakdown.model_groups.get(record.model_group) + if parent is None: + parent = MetricWithMetadata(metrics=SpendMetrics(), metadata={}) + breakdown.model_groups[record.model_group] = parent + key_metrics = parent.api_key_breakdown.get(record.api_key) + if key_metrics is None: + key_metrics = KeyMetricWithMetadata( + metrics=SpendMetrics(), + metadata=_key_metadata(api_key_metadata, record.api_key), + ) + parent.api_key_breakdown[record.api_key] = key_metrics + key_metrics.model_breakdown[record.model] = metrics elif level == _GROUP_DATE_PROVIDER: provider = record.custom_llm_provider or "unknown" assign_metric_with_metadata(breakdown.providers, provider, metrics) diff --git a/litellm/types/proxy/management_endpoints/common_daily_activity.py b/litellm/types/proxy/management_endpoints/common_daily_activity.py index 0eac9af3350..0ded66f9ce2 100644 --- a/litellm/types/proxy/management_endpoints/common_daily_activity.py +++ b/litellm/types/proxy/management_endpoints/common_daily_activity.py @@ -43,6 +43,7 @@ class KeyMetricWithMetadata(MetricBase): """Base class for metrics with additional metadata""" metadata: KeyMetadata = Field(default_factory=KeyMetadata) + model_breakdown: Dict[str, SpendMetrics] = Field(default_factory=dict) class MetricWithMetadata(MetricBase): 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 5c0a8ef0e1b..deb387d74e1 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 @@ -854,6 +854,7 @@ class TestBuildAggregatedSqlQuery: ) assert "(date, model, model_group)" in sql + assert "(date, api_key, model, model_group)" in sql def test_aggregate_grouping_sets_populates_model_group_model_breakdown(): @@ -887,6 +888,51 @@ def test_aggregate_grouping_sets_populates_model_group_model_breakdown(): assert metrics.cache_creation_input_tokens == 1000 +def test_aggregate_grouping_sets_populates_key_model_group_model_breakdown(): + model_record = SimpleNamespace( + group_level=7, + date="2026-05-29", + api_key="key-hash", + 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, + ) + model_group_key_record = SimpleNamespace( + **{ + **model_record.__dict__, + "group_level": 23, + "model": None, + } + ) + + result = _aggregate_grouping_sets_records_sync( + records=[model_record, model_group_key_record], + api_key_metadata={}, + ) + + key_metrics = ( + result["results"][0] + .breakdown.model_groups["smart-router"] + .api_key_breakdown["key-hash"] + ) + metrics = key_metrics.model_breakdown["bedrock/claude-opus-4-8"] + assert key_metrics.metrics.api_requests == 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", @@ -920,6 +966,13 @@ def test_update_breakdown_metrics_populates_model_group_model_breakdown(): assert metrics.total_tokens == 4000 assert metrics.cache_read_input_tokens == 1500 assert metrics.cache_creation_input_tokens == 500 + key_metrics = result.model_groups["smart-router"].api_key_breakdown["key-hash"].model_breakdown[ + "bedrock/claude-sonnet-4-5" + ] + assert key_metrics.api_requests == 2 + assert key_metrics.total_tokens == 4000 + assert key_metrics.cache_read_input_tokens == 1500 + assert key_metrics.cache_creation_input_tokens == 500 @pytest.mark.asyncio diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx index 260d5dcd2d6..295332dbac3 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.tsx @@ -9,6 +9,7 @@ import { TopModelData } from "../types"; interface KeyModelUsageViewProps { topModels: TopModelData[]; + title?: string; } const VISIBLE_ROWS = 5; @@ -70,7 +71,7 @@ const columns: ColumnsType = [ }, ]; -const KeyModelUsageView: React.FC = ({ topModels }) => { +const KeyModelUsageView: React.FC = ({ topModels, title = "Model Usage" }) => { const [viewMode, setViewMode] = useState<"chart" | "table">("table"); if (topModels.length === 0) { @@ -80,7 +81,7 @@ const KeyModelUsageView: React.FC = ({ topModels }) => { return ( - Model Usage + {title}
+ } + > + + + ))} + +
+); + const ModelSection = ({ modelName, metrics, @@ -82,6 +107,10 @@ const ModelSection = ({ {metrics.top_models && metrics.top_models.length > 0 && } + {metrics.top_model_groups && metrics.top_model_groups.length > 0 && ( + + )} + {/* Spend per day - Full width card */}
@@ -395,6 +424,7 @@ export const processActivityData = ( total_cache_creation_input_tokens: 0, top_api_keys: [], top_models: [], + top_model_groups: [], daily_data: [], }; } @@ -462,57 +492,77 @@ export const processActivityData = ( }); } - 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 || {}; + const aggregateModelBreakdown = (modelEntries: [string, SpendMetrics][]): TopModelData[] => { + const modelBreakdown = modelEntries.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, + }; - return Object.entries(perModelMetrics); - }); - const modelBreakdown = modelMetricsByDay.reduce>((totals, [modelName, metrics]) => { - const current = totals[modelName] ?? { + return { + ...totals, + [modelName]: { model: modelName, - spend: 0, - requests: 0, + spend: current.spend + metrics.spend, + requests: current.requests + metrics.api_requests, request_share: 0, - successful_requests: 0, - failed_requests: 0, - tokens: 0, - cache_read_input_tokens: 0, - cache_creation_input_tokens: 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); - 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), - }, - }; - }, {}); + return Object.values(modelBreakdown) + .map((model) => ({ + ...model, + request_share: totalRequests === 0 ? 0 : (model.requests / totalRequests) * 100, + })) + .sort((a, b) => b.requests - a.requests); + }; - 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, - })) + if (key === "model_groups") { + Object.entries(modelMetrics).forEach(([metricName]) => { + const modelEntries = dailyActivity.results.flatMap((day) => + Object.entries(day.breakdown.model_groups?.[metricName]?.model_breakdown || {}), + ); + modelMetrics[metricName].top_models = aggregateModelBreakdown(modelEntries); + }); + } + + if (key === "api_keys") { + Object.entries(modelMetrics).forEach(([metricName]) => { + const groupRecords = dailyActivity.results.flatMap((day) => + Object.entries(day.breakdown.model_groups || {}).flatMap(([modelGroup, groupData]) => { + const keyData = groupData.api_key_breakdown?.[metricName]; + return keyData ? [{ modelGroup, keyData }] : []; + }), + ); + const modelGroupNames = Array.from(new Set(groupRecords.map(({ modelGroup }) => modelGroup))); + + modelMetrics[metricName].top_model_groups = modelGroupNames + .map((modelGroup) => { + const records = groupRecords.filter((record) => record.modelGroup === modelGroup); + return { + model_group: modelGroup, + spend: records.reduce((total, { keyData }) => total + keyData.metrics.spend, 0), + requests: records.reduce((total, { keyData }) => total + keyData.metrics.api_requests, 0), + top_models: aggregateModelBreakdown( + records.flatMap(({ keyData }) => Object.entries(keyData.model_breakdown || {})), + ), + }; + }) .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 26d43f0b847..aa78d2f4b31 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -24599,6 +24599,10 @@ export interface components { KeyMetricWithMetadata: { metadata?: components["schemas"]["KeyMetadata"]; metrics: components["schemas"]["SpendMetrics"]; + /** Model Breakdown */ + model_breakdown?: { + [key: string]: components["schemas"]["SpendMetrics"]; + }; }; /** KeyRequest */ KeyRequest: {