-
-
Notifications
You must be signed in to change notification settings - Fork 7.4k
fix(ui): CSV export empty on Global Usage page #23819
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,14 +17,57 @@ const extractTeamIdFromApiKeyBreakdown = (apiKeyBreakdown: Record<string, any> | | |||||||||||
| return null; | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| // Mirrors backend SpendMetrics fields (litellm/types/activity_tracking.py). | ||||||||||||
| // If the backend adds a field, add it here too. | ||||||||||||
| const METRIC_KEYS = [ | ||||||||||||
| "spend", "api_requests", "successful_requests", "failed_requests", | ||||||||||||
| "total_tokens", "prompt_tokens", "completion_tokens", | ||||||||||||
| "cache_read_input_tokens", "cache_creation_input_tokens", | ||||||||||||
| ] as const; | ||||||||||||
|
|
||||||||||||
| // 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> => { | ||||||||||||
| const apiKeys = breakdown.api_keys; | ||||||||||||
| if (!apiKeys || Object.keys(apiKeys).length === 0) return {}; | ||||||||||||
|
|
||||||||||||
| const grouped: Record<string, any> = {}; | ||||||||||||
|
|
||||||||||||
| for (const [keyId, keyData] of Object.entries<any>(apiKeys)) { | ||||||||||||
| const teamId = keyData?.metadata?.team_id || "Unassigned"; | ||||||||||||
| if (!grouped[teamId]) { | ||||||||||||
| grouped[teamId] = { | ||||||||||||
| metrics: Object.fromEntries(METRIC_KEYS.map((k) => [k, 0])), | ||||||||||||
| api_key_breakdown: {}, | ||||||||||||
| }; | ||||||||||||
| } | ||||||||||||
| const m = grouped[teamId].metrics; | ||||||||||||
| const km = keyData?.metrics || {}; | ||||||||||||
| for (const k of METRIC_KEYS) { | ||||||||||||
| m[k] += km[k] || 0; | ||||||||||||
| } | ||||||||||||
| grouped[teamId].api_key_breakdown[keyId] = keyData; | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return grouped; | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| // Returns breakdown.entities if populated, otherwise falls back to | ||||||||||||
| // reconstructing entities from breakdown.api_keys. | ||||||||||||
| export const resolveEntities = (breakdown: Record<string, any>): Record<string, any> => { | ||||||||||||
| const entities = breakdown.entities; | ||||||||||||
| if (entities && Object.keys(entities).length > 0) return entities; | ||||||||||||
| return aggregateApiKeysIntoEntities(breakdown); | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| export const getEntityBreakdown = ( | ||||||||||||
| spendData: EntitySpendData, | ||||||||||||
| teamAliasMap: Record<string, string> = {}, | ||||||||||||
| ): EntityBreakdown[] => { | ||||||||||||
| const entitySpend: { [key: string]: EntityBreakdown } = {}; | ||||||||||||
|
|
||||||||||||
| spendData.results.forEach((day) => { | ||||||||||||
| Object.entries(day.breakdown.entities || {}).forEach(([entity, data]: [string, any]) => { | ||||||||||||
| Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => { | ||||||||||||
| // Extract team_id from api_key_breakdown metadata (not data.metadata which is empty) | ||||||||||||
| const teamId = extractTeamIdFromApiKeyBreakdown(data.api_key_breakdown) || entity; | ||||||||||||
| // Extract key_alias from the first API key that has one | ||||||||||||
|
|
@@ -80,7 +123,7 @@ export const generateDailyData = ( | |||||||||||
| const dailyBreakdown: any[] = []; | ||||||||||||
|
|
||||||||||||
| spendData.results.forEach((day) => { | ||||||||||||
| Object.entries(day.breakdown.entities || {}).forEach(([entity, data]: [string, any]) => { | ||||||||||||
| Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => { | ||||||||||||
| // Extract team_id from api_key_breakdown metadata (not data.metadata which is empty) | ||||||||||||
| const teamId = extractTeamIdFromApiKeyBreakdown(data.api_key_breakdown); | ||||||||||||
| const teamAlias = teamId ? teamAliasMap[teamId] || null : null; | ||||||||||||
|
|
@@ -129,7 +172,7 @@ export const generateDailyWithKeysData = ( | |||||||||||
| } = {}; | ||||||||||||
|
|
||||||||||||
| spendData.results.forEach((day) => { | ||||||||||||
| Object.entries(day.breakdown.entities || {}).forEach(([entity, data]: [string, any]) => { | ||||||||||||
| Object.entries(resolveEntities(day.breakdown)).forEach(([entity, data]: [string, any]) => { | ||||||||||||
| const apiKeyBreakdown = data.api_key_breakdown || {}; | ||||||||||||
|
|
||||||||||||
| // Iterate through each API key in the breakdown | ||||||||||||
|
|
@@ -202,7 +245,7 @@ export const generateDailyWithModelsData = ( | |||||||||||
| spendData.results.forEach((day) => { | ||||||||||||
| const dailyEntityModels: { [key: string]: { [key: string]: any } } = {}; | ||||||||||||
|
|
||||||||||||
| Object.entries(day.breakdown.entities || {}).forEach(([entity, entityData]: [string, any]) => { | ||||||||||||
| Object.entries(resolveEntities(day.breakdown)).forEach(([entity, entityData]: [string, any]) => { | ||||||||||||
| if (!dailyEntityModels[entity]) { | ||||||||||||
| dailyEntityModels[entity] = {}; | ||||||||||||
| } | ||||||||||||
|
|
@@ -230,7 +273,7 @@ export const generateDailyWithModelsData = ( | |||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| Object.entries(dailyEntityModels).forEach(([entity, models]) => { | ||||||||||||
| const entityData = day.breakdown.entities?.[entity]; | ||||||||||||
| const entityData = resolveEntities(day.breakdown)[entity]; | ||||||||||||
|
Comment on lines
275
to
+276
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Then replace both usages of |
||||||||||||
| // Extract team_id from api_key_breakdown metadata (not entityData.metadata which is empty) | ||||||||||||
| const teamId = extractTeamIdFromApiKeyBreakdown(entityData?.api_key_breakdown); | ||||||||||||
| const teamAlias = teamId ? teamAliasMap[teamId] || null : null; | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test only verifies that rows are produced and that the
"Model"property exists — it does not verify actual spend or request values per model. The underlyinggenerateDailyWithModelsDatalogic adds all API key metrics to every model inbreakdown.models. With only one model in the test fixture ("gpt-4") the multiplication factor is 1, so values happen to be correct by coincidence. If a second model were added toaggregatedSpendData.breakdown.models, each model row would carry the full team spend rather than its own share, and this test would not catch it.Consider adding assertions on the spend values per model to catch regressions if the metric attribution logic changes: