From 2e391dcfd25ff31714e5658c448a58ec7efa43fe Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:39:46 +0000 Subject: [PATCH] fix(ui): guard usage breakdown aggregation against entries missing metrics --- .../components/UsagePageView.test.tsx | 47 +++++++++++++++++++ .../_components/components/UsagePageView.tsx | 4 ++ .../src/components/activity_metrics.tsx | 4 +- 3 files changed, 54 insertions(+), 1 deletion(-) 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..c39db5b93a6 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 @@ -596,6 +596,53 @@ describe("UsagePage", () => { expect(screen.getByText("Top Virtual Keys")).toBeInTheDocument(); }); + it("should not crash when a breakdown entry is missing its metrics (partial/large dataset)", async () => { + const malformedSpendData = { + ...mockSpendData, + results: [ + { + ...mockSpendData.results[0], + breakdown: { + ...mockSpendData.results[0].breakdown, + api_keys: { + ...mockSpendData.results[0].breakdown.api_keys, + "sk-broken": { + metadata: { key_alias: "Broken Key", tags: [] }, + }, + }, + models: { + ...mockSpendData.results[0].breakdown.models, + "broken-model": { + metadata: {}, + api_key_breakdown: {}, + }, + }, + model_groups: { + ...mockSpendData.results[0].breakdown.model_groups, + "broken-group": { + metadata: {}, + api_key_breakdown: {}, + }, + }, + providers: { + ...mockSpendData.results[0].breakdown.providers, + "broken-provider": {}, + }, + }, + }, + ], + }; + mockUserDailyActivityAggregatedCall.mockResolvedValue(malformedSpendData as any); + + renderWithProviders(); + + await waitFor(() => { + expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled(); + }); + + expect(screen.getByText("Top Virtual Keys")).toBeInTheDocument(); + }); + it("should render the daily spend and top models charts with cyan bars", async () => { const { container } = renderWithProviders(); 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..cffc3c17a7a 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 @@ -252,6 +252,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { const modelSpend: { [key: string]: MetricWithMetadata } = {}; userSpendData.results.forEach((day) => { Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => { + if (!metrics?.metrics) return; if (!modelSpend[model]) { modelSpend[model] = { metrics: { @@ -298,6 +299,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { const modelGroupSpend: { [key: string]: MetricWithMetadata } = {}; userSpendData.results.forEach((day) => { Object.entries(day.breakdown.model_groups || {}).forEach(([modelGroup, metrics]) => { + if (!metrics?.metrics) return; if (!modelGroupSpend[modelGroup]) { modelGroupSpend[modelGroup] = { metrics: { @@ -346,6 +348,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { const providerSpendMap: { [key: string]: MetricWithMetadata } = {}; userSpendData.results.forEach((day) => { Object.entries(day.breakdown.providers || {}).forEach(([provider, metrics]) => { + if (!metrics?.metrics) return; if (!providerSpendMap[provider]) { providerSpendMap[provider] = { metrics: { @@ -391,6 +394,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { const keySpend: { [key: string]: KeyMetricWithMetadata } = {}; userSpendData.results.forEach((day) => { Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => { + if (!metrics?.metrics) return; if (!keySpend[key]) { keySpend[key] = { metrics: { diff --git a/ui/litellm-dashboard/src/components/activity_metrics.tsx b/ui/litellm-dashboard/src/components/activity_metrics.tsx index 54ac5ae0ee6..97634bd6d0d 100644 --- a/ui/litellm-dashboard/src/components/activity_metrics.tsx +++ b/ui/litellm-dashboard/src/components/activity_metrics.tsx @@ -369,6 +369,7 @@ export const processActivityData = ( dailyActivity.results.forEach((day) => { Object.entries(day.breakdown[key] || {}).forEach(([model, modelData]) => { + if (!modelData?.metrics) return; if (!modelMetrics[model]) { modelMetrics[model] = { label: @@ -430,6 +431,7 @@ export const processActivityData = ( const modelData = day.breakdown[key]?.[model]; if (modelData && "api_key_breakdown" in modelData) { Object.entries(modelData.api_key_breakdown || {}).forEach(([apiKey, keyData]) => { + if (!keyData?.metrics) return; if (!apiKeyBreakdown[apiKey]) { apiKeyBreakdown[apiKey] = { api_key: apiKey, @@ -466,7 +468,7 @@ export const processActivityData = ( 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 (keyDataForModel?.metrics) { if (!modelBreakdown[modelName]) { modelBreakdown[modelName] = { model: modelName,