diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts index 802555b9d0b..dfeecdc4fa6 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts @@ -358,6 +358,23 @@ describe("EntityUsageExport utils", () => { expect(result[0]).toHaveProperty("Total Tokens"); expect(result[0]).toHaveProperty("Prompt Tokens"); expect(result[0]).toHaveProperty("Completion Tokens"); + expect(result[0]).toHaveProperty("Cache Read Input Tokens"); + expect(result[0]).toHaveProperty("Cache Creation Input Tokens"); + }); + + it("should export exact cache token values per entity per day", () => { + const result = generateDailyData(mockSpendData, "Team", mockTeamAliasMap); + + const day1Team1 = result.find((r) => r.Date === "2025-01-01" && r["Team ID"] === "team-1"); + const day1Team2 = result.find((r) => r.Date === "2025-01-01" && r["Team ID"] === "team-2"); + const day2Team1 = result.find((r) => r.Date === "2025-01-02" && r["Team ID"] === "team-1"); + + expect(day1Team1?.["Cache Read Input Tokens"]).toBe(50); + expect(day1Team1?.["Cache Creation Input Tokens"]).toBe(30); + expect(day1Team2?.["Cache Read Input Tokens"]).toBe(100); + expect(day1Team2?.["Cache Creation Input Tokens"]).toBe(60); + expect(day2Team1?.["Cache Read Input Tokens"]).toBe(75); + expect(day2Team1?.["Cache Creation Input Tokens"]).toBe(45); }); it("should sort data by date ascending", () => { @@ -469,6 +486,8 @@ describe("EntityUsageExport utils", () => { expect(result[0]["Prompt Tokens"]).toBe(0); expect(result[0]["Completion Tokens"]).toBe(0); + expect(result[0]["Cache Read Input Tokens"]).toBe(0); + expect(result[0]["Cache Creation Input Tokens"]).toBe(0); }); }); @@ -614,6 +633,61 @@ describe("EntityUsageExport utils", () => { expect(result[0]).toHaveProperty("Total Tokens"); expect(result[0]).toHaveProperty("Prompt Tokens"); expect(result[0]).toHaveProperty("Completion Tokens"); + expect(result[0]).toHaveProperty("Cache Read Input Tokens"); + expect(result[0]).toHaveProperty("Cache Creation Input Tokens"); + }); + + it("should export and aggregate cache token values per key", () => { + const makeDay = (cacheRead: number, cacheCreation: number) => ({ + date: "2025-01-01", + breakdown: { + entities: { + "team-1": { + metrics: { + spend: 5.0, + api_requests: 50, + successful_requests: 50, + failed_requests: 0, + total_tokens: 500, + prompt_tokens: 300, + completion_tokens: 200, + cache_read_input_tokens: cacheRead, + cache_creation_input_tokens: cacheCreation, + }, + api_key_breakdown: { + key1: { + metrics: { + spend: 5.0, + api_requests: 50, + successful_requests: 50, + failed_requests: 0, + total_tokens: 500, + prompt_tokens: 300, + completion_tokens: 200, + cache_read_input_tokens: cacheRead, + cache_creation_input_tokens: cacheCreation, + }, + metadata: { + team_id: "team-1", + key_alias: "alias-1", + }, + }, + }, + }, + }, + }, + }); + + const spendDataWithCache: EntitySpendData = { + results: [makeDay(40, 25), makeDay(10, 5)], + metadata: mockSpendDataWithKeys.metadata, + }; + + const result = generateDailyWithKeysData(spendDataWithCache, "Team"); + + expect(result).toHaveLength(1); + expect(result[0]["Cache Read Input Tokens"]).toBe(50); + expect(result[0]["Cache Creation Input Tokens"]).toBe(30); }); it("should sort data by date ascending", () => { @@ -935,6 +1009,8 @@ describe("EntityUsageExport utils", () => { expect(key1Entry?.["Prompt Tokens"]).toBe(0); expect(key1Entry?.["Completion Tokens"]).toBe(0); + expect(key1Entry?.["Cache Read Input Tokens"]).toBe(0); + expect(key1Entry?.["Cache Creation Input Tokens"]).toBe(0); }); it("should handle empty api_key_breakdown", () => { @@ -1096,6 +1172,117 @@ describe("EntityUsageExport utils", () => { expect(result[0]).toHaveProperty("Successful"); expect(result[0]).toHaveProperty("Failed"); expect(result[0]).toHaveProperty("Total Tokens"); + expect(result[0]).toHaveProperty("Prompt Tokens"); + expect(result[0]).toHaveProperty("Completion Tokens"); + expect(result[0]).toHaveProperty("Cache Read Input Tokens"); + expect(result[0]).toHaveProperty("Cache Creation Input Tokens"); + }); + + it("should export prompt, completion, and cache token values summed across keys for the same model", () => { + const data: EntitySpendData = { + results: [ + { + date: "2025-03-01", + breakdown: { + entities: { + "team-1": { + metrics: { + spend: 5.0, + api_requests: 25, + successful_requests: 25, + failed_requests: 0, + total_tokens: 1050, + prompt_tokens: 750, + completion_tokens: 300, + cache_read_input_tokens: 450, + cache_creation_input_tokens: 200, + }, + api_key_breakdown: { + key1: { + metrics: { + spend: 2.0, + api_requests: 10, + successful_requests: 10, + failed_requests: 0, + total_tokens: 700, + }, + metadata: { team_id: "team-1" }, + }, + key2: { + metrics: { + spend: 3.0, + api_requests: 15, + successful_requests: 15, + failed_requests: 0, + total_tokens: 350, + }, + metadata: { team_id: "team-1" }, + }, + }, + }, + }, + models: { + "claude-sonnet-4-5": { + metrics: { + spend: 5.0, + api_requests: 25, + successful_requests: 25, + failed_requests: 0, + total_tokens: 1050, + }, + api_key_breakdown: { + key1: { + metrics: { + spend: 2.0, + api_requests: 10, + successful_requests: 10, + failed_requests: 0, + total_tokens: 700, + prompt_tokens: 500, + completion_tokens: 200, + cache_read_input_tokens: 300, + cache_creation_input_tokens: 120, + }, + metadata: { team_id: "team-1" }, + }, + key2: { + metrics: { + spend: 3.0, + api_requests: 15, + successful_requests: 15, + failed_requests: 0, + total_tokens: 350, + prompt_tokens: 250, + completion_tokens: 100, + cache_read_input_tokens: 150, + cache_creation_input_tokens: 80, + }, + metadata: { team_id: "team-1" }, + }, + }, + }, + }, + }, + }, + ], + metadata: { + total_spend: 5.0, + total_api_requests: 25, + total_successful_requests: 25, + total_failed_requests: 0, + total_tokens: 1050, + }, + }; + + const result = generateDailyWithModelsData(data, "Team"); + + expect(result).toHaveLength(1); + expect(result[0].Model).toBe("claude-sonnet-4-5"); + expect(result[0]["Total Tokens"]).toBe(1050); + expect(result[0]["Prompt Tokens"]).toBe(750); + expect(result[0]["Completion Tokens"]).toBe(300); + expect(result[0]["Cache Read Input Tokens"]).toBe(450); + expect(result[0]["Cache Creation Input Tokens"]).toBe(200); }); it("should sort data by date ascending", () => { diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts index cec8af2dc74..8ff389ded5e 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts @@ -126,6 +126,8 @@ export const generateDailyData = ( "Total Tokens": data.metrics.total_tokens, "Prompt Tokens": data.metrics.prompt_tokens || 0, "Completion Tokens": data.metrics.completion_tokens || 0, + "Cache Read Input Tokens": data.metrics.cache_read_input_tokens || 0, + "Cache Creation Input Tokens": data.metrics.cache_creation_input_tokens || 0, }); }); }); @@ -154,6 +156,8 @@ export const generateDailyWithKeysData = ( total_tokens: number; prompt_tokens: number; completion_tokens: number; + cache_read_input_tokens: number; + cache_creation_input_tokens: number; }; }; } = {}; @@ -186,6 +190,8 @@ export const generateDailyWithKeysData = ( total_tokens: keyData.metrics?.total_tokens || 0, prompt_tokens: keyData.metrics?.prompt_tokens || 0, completion_tokens: keyData.metrics?.completion_tokens || 0, + cache_read_input_tokens: keyData.metrics?.cache_read_input_tokens || 0, + cache_creation_input_tokens: keyData.metrics?.cache_creation_input_tokens || 0, }, }; } else { @@ -197,6 +203,9 @@ export const generateDailyWithKeysData = ( aggregatedData[uniqueKey].metrics.total_tokens += keyData.metrics?.total_tokens || 0; aggregatedData[uniqueKey].metrics.prompt_tokens += keyData.metrics?.prompt_tokens || 0; aggregatedData[uniqueKey].metrics.completion_tokens += keyData.metrics?.completion_tokens || 0; + aggregatedData[uniqueKey].metrics.cache_read_input_tokens += keyData.metrics?.cache_read_input_tokens || 0; + aggregatedData[uniqueKey].metrics.cache_creation_input_tokens += + keyData.metrics?.cache_creation_input_tokens || 0; } }); }); @@ -216,6 +225,8 @@ export const generateDailyWithKeysData = ( "Total Tokens": item.metrics.total_tokens, "Prompt Tokens": item.metrics.prompt_tokens, "Completion Tokens": item.metrics.completion_tokens, + "Cache Read Input Tokens": item.metrics.cache_read_input_tokens, + "Cache Creation Input Tokens": item.metrics.cache_creation_input_tokens, })); return dailyKeyBreakdown.sort((a, b) => new Date(a.Date).getTime() - new Date(b.Date).getTime()); @@ -251,6 +262,10 @@ export const generateDailyWithModelsData = ( successful: 0, failed: 0, tokens: 0, + promptTokens: 0, + completionTokens: 0, + cacheReadInputTokens: 0, + cacheCreationInputTokens: 0, }; } dailyEntityModels[entity][model].spend += keyMetrics.spend || 0; @@ -258,6 +273,10 @@ export const generateDailyWithModelsData = ( dailyEntityModels[entity][model].successful += keyMetrics.successful_requests || 0; dailyEntityModels[entity][model].failed += keyMetrics.failed_requests || 0; dailyEntityModels[entity][model].tokens += keyMetrics.total_tokens || 0; + dailyEntityModels[entity][model].promptTokens += keyMetrics.prompt_tokens || 0; + dailyEntityModels[entity][model].completionTokens += keyMetrics.completion_tokens || 0; + dailyEntityModels[entity][model].cacheReadInputTokens += keyMetrics.cache_read_input_tokens || 0; + dailyEntityModels[entity][model].cacheCreationInputTokens += keyMetrics.cache_creation_input_tokens || 0; }); }); }); @@ -276,6 +295,10 @@ export const generateDailyWithModelsData = ( Successful: metrics.successful, Failed: metrics.failed, "Total Tokens": metrics.tokens, + "Prompt Tokens": metrics.promptTokens, + "Completion Tokens": metrics.completionTokens, + "Cache Read Input Tokens": metrics.cacheReadInputTokens, + "Cache Creation Input Tokens": metrics.cacheCreationInputTokens, }); }); });