Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

vi.mock("papaparse", () => ({
default: {
unparse: vi.fn((data: any[]) => "mocked-csv-data"),

Check warning on line 28 in ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
},
}));

Expand Down Expand Up @@ -358,6 +358,23 @@
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", () => {
Expand Down Expand Up @@ -469,6 +486,8 @@

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);
});
});

Expand Down Expand Up @@ -614,6 +633,61 @@
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", () => {
Expand Down Expand Up @@ -935,6 +1009,8 @@

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", () => {
Expand Down Expand Up @@ -1096,6 +1172,117 @@
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", () => {
Expand Down Expand Up @@ -1725,7 +1912,7 @@
blobType = options.type;
}
}
} as any;

Check warning on line 1915 in ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

handleExportCSV(mockSpendData, "daily", "Team", "team", mockTeamAliasMap);

Expand Down Expand Up @@ -1792,7 +1979,7 @@
blobType = options.type;
}
}
} as any;

Check warning on line 1982 in ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const mockDateRange: DateRangePickerValue = {
from: new Date("2025-01-01"),
Expand All @@ -1817,7 +2004,7 @@
jsonString = parts[0] as string;
}
}
} as any;

Check warning on line 2007 in ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const mockDateRange: DateRangePickerValue = {
from: new Date("2025-01-01"),
Expand Down Expand Up @@ -1850,7 +2037,7 @@
entities: {},
api_keys: {
...Object.fromEntries(
Object.values(day.breakdown.entities as Record<string, any>).flatMap((e: any) =>

Check warning on line 2040 in ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 2040 in ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
Object.entries(e.api_key_breakdown || {}),
),
),
Expand Down
23 changes: 23 additions & 0 deletions ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@

// When breakdown.entities is empty (aggregated endpoint), reconstruct entities
// from breakdown.api_keys by grouping on metadata.team_id.
const aggregateApiKeysIntoEntities = (breakdown: Record<string, any>): Record<string, any> => {

Check warning on line 30 in ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

Check warning on line 30 in ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
const apiKeys = breakdown.api_keys;
if (!apiKeys || Object.keys(apiKeys).length === 0) return {};

const grouped: Record<string, any> = {};

Check warning on line 34 in ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

for (const [keyId, keyData] of Object.entries<any>(apiKeys)) {

Check warning on line 36 in ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
const teamId = keyData?.metadata?.team_id || "Unassigned";
if (!grouped[teamId]) {
grouped[teamId] = {
Expand Down Expand Up @@ -126,6 +126,8 @@
"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,
});
});
});
Expand Down Expand Up @@ -154,6 +156,8 @@
total_tokens: number;
prompt_tokens: number;
completion_tokens: number;
cache_read_input_tokens: number;
cache_creation_input_tokens: number;
};
};
} = {};
Expand Down Expand Up @@ -186,6 +190,8 @@
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 {
Expand All @@ -197,6 +203,9 @@
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;
}
});
});
Expand All @@ -216,6 +225,8 @@
"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());
Expand Down Expand Up @@ -251,13 +262,21 @@
successful: 0,
failed: 0,
tokens: 0,
promptTokens: 0,
completionTokens: 0,
cacheReadInputTokens: 0,
cacheCreationInputTokens: 0,
};
}
dailyEntityModels[entity][model].spend += keyMetrics.spend || 0;
dailyEntityModels[entity][model].requests += keyMetrics.api_requests || 0;
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;
});
});
});
Expand All @@ -276,6 +295,10 @@
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,
});
});
});
Expand Down
Loading