From 72a045761fe9372c251d7c6afb4df59615767773 Mon Sep 17 00:00:00 2001 From: Taranum01 Date: Wed, 30 Sep 2026 14:51:21 +0530 Subject: [PATCH] fix(ui): guard partial usage metrics in current aggregation helpers --- .../EndpointUsage/EndpointUsage.tsx | 18 ++--- .../entityUsageAggregations.test.ts | 33 +++++++++ .../EntityUsage/entityUsageAggregations.ts | 72 +++++++++---------- 3 files changed, 78 insertions(+), 45 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.test.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx index 51e451ca770..5f7b8a48c02 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx @@ -39,16 +39,16 @@ const EndpointUsage: React.FC = ({ userSpendData }) => { api_key_breakdown: {}, }; } - aggregatedEndpoints[endpoint].metrics.spend += metrics.metrics.spend; - aggregatedEndpoints[endpoint].metrics.prompt_tokens += metrics.metrics.prompt_tokens; - aggregatedEndpoints[endpoint].metrics.completion_tokens += metrics.metrics.completion_tokens; - aggregatedEndpoints[endpoint].metrics.total_tokens += metrics.metrics.total_tokens; - aggregatedEndpoints[endpoint].metrics.api_requests += metrics.metrics.api_requests; - aggregatedEndpoints[endpoint].metrics.successful_requests += metrics.metrics.successful_requests || 0; - aggregatedEndpoints[endpoint].metrics.failed_requests += metrics.metrics.failed_requests || 0; - aggregatedEndpoints[endpoint].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0; + aggregatedEndpoints[endpoint].metrics.spend += (metrics.metrics?.spend ?? 0); + aggregatedEndpoints[endpoint].metrics.prompt_tokens += (metrics.metrics?.prompt_tokens ?? 0); + aggregatedEndpoints[endpoint].metrics.completion_tokens += (metrics.metrics?.completion_tokens ?? 0); + aggregatedEndpoints[endpoint].metrics.total_tokens += (metrics.metrics?.total_tokens ?? 0); + aggregatedEndpoints[endpoint].metrics.api_requests += (metrics.metrics?.api_requests ?? 0); + aggregatedEndpoints[endpoint].metrics.successful_requests += (metrics.metrics?.successful_requests ?? 0) || 0; + aggregatedEndpoints[endpoint].metrics.failed_requests += (metrics.metrics?.failed_requests ?? 0) || 0; + aggregatedEndpoints[endpoint].metrics.cache_read_input_tokens += (metrics.metrics?.cache_read_input_tokens ?? 0) || 0; aggregatedEndpoints[endpoint].metrics.cache_creation_input_tokens += - metrics.metrics.cache_creation_input_tokens || 0; + (metrics.metrics?.cache_creation_input_tokens ?? 0) || 0; }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.test.ts new file mode 100644 index 00000000000..07e37a536a5 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { getGlobalTopKeys, getTopModels, getTopAgents } from "./entityUsageAggregations"; +import type { DailyData } from "@/components/UsagePage/types"; + +const partialDay = { + date: "2026-09-01", + breakdown: { + models: { model: { metadata: {} } }, + api_keys: { key: { metadata: { key_alias: "test key" } } }, + entities: { agent: { metadata: { agent_name: "test agent" }, api_key_breakdown: {} } }, + }, +} as unknown as DailyData; + +describe("partial breakdown metrics", () => { + it("defaults missing global-key metrics to zero", () => { + expect(getGlobalTopKeys([partialDay], 10)[0].spend).toBe(0); + }); + it("defaults missing model metrics to zero", () => { + expect(getTopModels([partialDay], "models", 10)[0].tokens).toBe(0); + }); + it("defaults missing agent metrics to zero", () => { + expect(getTopAgents([partialDay], 10)[0].spend).toBe(0); + }); + it("still sums populated days after a partial day", () => { + const goodDay = { + ...partialDay, + breakdown: { ...partialDay.breakdown, api_keys: { + key: { metadata: { key_alias: "test key" }, metrics: { spend: 5 } }, + } }, + } as unknown as DailyData; + expect(getGlobalTopKeys([partialDay, goodDay], 10)[0].spend).toBe(5); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.ts index 54569608b0e..521eff1b1e9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.ts @@ -35,14 +35,14 @@ export const getTopModels = ( }; } try { - modelSpend[model].spend += metrics.metrics.spend; + modelSpend[model].spend += (metrics.metrics?.spend ?? 0); } catch (e) { 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].failed_requests += metrics.metrics.failed_requests; - modelSpend[model].tokens += metrics.metrics.total_tokens; + modelSpend[model].requests += (metrics.metrics?.api_requests ?? 0); + modelSpend[model].successful_requests += (metrics.metrics?.successful_requests ?? 0); + modelSpend[model].failed_requests += (metrics.metrics?.failed_requests ?? 0); + modelSpend[model].tokens += (metrics.metrics?.total_tokens ?? 0); }); }); @@ -69,11 +69,11 @@ export const getTopAgents = (results: ExtendedDailyData[], topAgentsLimit: numbe agent_name: (data.metadata as any)?.agent_name || agentId, }; } - agentSpend[agentId].spend += data.metrics.spend; - agentSpend[agentId].requests += data.metrics.api_requests; - agentSpend[agentId].successful_requests += data.metrics.successful_requests; - agentSpend[agentId].failed_requests += data.metrics.failed_requests; - agentSpend[agentId].tokens += data.metrics.total_tokens; + agentSpend[agentId].spend += (data.metrics?.spend ?? 0); + agentSpend[agentId].requests += (data.metrics?.api_requests ?? 0); + agentSpend[agentId].successful_requests += (data.metrics?.successful_requests ?? 0); + agentSpend[agentId].failed_requests += (data.metrics?.failed_requests ?? 0); + agentSpend[agentId].tokens += (data.metrics?.total_tokens ?? 0); }); }); @@ -113,15 +113,15 @@ export const getGlobalTopKeys = (results: DailyData[], topKeysLimit: number): To }, }; } - 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.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.spend += (metrics.metrics?.spend ?? 0); + keySpend[key].metrics.prompt_tokens += (metrics.metrics?.prompt_tokens ?? 0); + keySpend[key].metrics.completion_tokens += (metrics.metrics?.completion_tokens ?? 0); + keySpend[key].metrics.total_tokens += (metrics.metrics?.total_tokens ?? 0); + keySpend[key].metrics.api_requests += (metrics.metrics?.api_requests ?? 0); + keySpend[key].metrics.successful_requests += (metrics.metrics?.successful_requests ?? 0); + keySpend[key].metrics.failed_requests += (metrics.metrics?.failed_requests ?? 0); + keySpend[key].metrics.cache_read_input_tokens += (metrics.metrics?.cache_read_input_tokens ?? 0) || 0; + keySpend[key].metrics.cache_creation_input_tokens += (metrics.metrics?.cache_creation_input_tokens ?? 0) || 0; }); }); @@ -132,7 +132,7 @@ export const getGlobalTopKeys = (results: DailyData[], topKeysLimit: number): To user: metrics.metadata.user_email ?? metrics.metadata.user_id ?? null, key_exists: metrics.metadata.key_exists, tags: metrics.metadata.tags || [], - spend: metrics.metrics.spend, + spend: (metrics.metrics?.spend ?? 0), })) .sort((a, b) => b.spend - a.spend) .slice(0, topKeysLimit); @@ -146,7 +146,7 @@ export const getTopAPIKeys = (results: ExtendedDailyData[], topKeysLimit: number const tagDictionary = Object.keys(entities).reduce((acc: { [key: string]: TagUsage[] }, entity) => { const { api_key_breakdown } = entities[entity]; Object.keys(api_key_breakdown).forEach((key) => { - const tagUsage = { tag: entity, usage: api_key_breakdown[key].metrics.spend }; + const tagUsage = { tag: entity, usage: (api_key_breakdown[key].metrics?.spend ?? 0) }; if (acc[key]) { acc[key].push(tagUsage); } else { @@ -179,15 +179,15 @@ export const getTopAPIKeys = (results: ExtendedDailyData[], topKeysLimit: number }, }; } - 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.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.spend += (metrics.metrics?.spend ?? 0); + keySpend[key].metrics.prompt_tokens += (metrics.metrics?.prompt_tokens ?? 0); + keySpend[key].metrics.completion_tokens += (metrics.metrics?.completion_tokens ?? 0); + keySpend[key].metrics.total_tokens += (metrics.metrics?.total_tokens ?? 0); + keySpend[key].metrics.api_requests += (metrics.metrics?.api_requests ?? 0); + keySpend[key].metrics.successful_requests += (metrics.metrics?.successful_requests ?? 0); + keySpend[key].metrics.failed_requests += (metrics.metrics?.failed_requests ?? 0); + keySpend[key].metrics.cache_read_input_tokens += (metrics.metrics?.cache_read_input_tokens ?? 0) || 0; + keySpend[key].metrics.cache_creation_input_tokens += (metrics.metrics?.cache_creation_input_tokens ?? 0) || 0; }); }); @@ -198,7 +198,7 @@ export const getTopAPIKeys = (results: ExtendedDailyData[], topKeysLimit: number user: metrics.metadata.user_email ?? metrics.metadata.user_id ?? null, key_exists: metrics.metadata.key_exists, tags: metrics.metadata.tags || [], - spend: metrics.metrics.spend, + spend: (metrics.metrics?.spend ?? 0), })) .sort((a, b) => b.spend - a.spend) .slice(0, topKeysLimit); @@ -219,11 +219,11 @@ export const getProviderSpend = (results: ExtendedDailyData[]): ProviderSpendRow }; } 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; + providerSpend[provider].spend += (metrics.metrics?.spend ?? 0); + providerSpend[provider].requests += (metrics.metrics?.api_requests ?? 0); + providerSpend[provider].successful_requests += (metrics.metrics?.successful_requests ?? 0); + providerSpend[provider].failed_requests += (metrics.metrics?.failed_requests ?? 0); + providerSpend[provider].tokens += (metrics.metrics?.total_tokens ?? 0); } catch (e) { console.error(`Error processing provider ${provider}: ${e}`); }