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 dcf248cee65..e748e4586e5 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,17 @@ 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..12ea830c4d1 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageAggregations.test.ts @@ -0,0 +1,36 @@ +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 be9f44d0786..86847ade1aa 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 @@ -37,14 +37,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; }); }); @@ -71,11 +71,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; }); }); @@ -115,15 +115,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; }); }); @@ -134,7 +134,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); @@ -148,7 +148,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 { @@ -181,15 +181,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; }); }); @@ -200,7 +200,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); @@ -223,11 +223,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}`); }