Skip to content

fix(ui): CSV export empty on Global Usage page#23819

Merged
ryan-crabbe merged 1 commit intolitellm_ryan_march_16from
litellm_fix-csv-export
Mar 17, 2026
Merged

fix(ui): CSV export empty on Global Usage page#23819
ryan-crabbe merged 1 commit intolitellm_ryan_march_16from
litellm_fix-csv-export

Conversation

@ryan-crabbe
Copy link
Copy Markdown
Contributor

Type

🐛 Bug Fix

Changes

Aggregated endpoint returns empty breakdown.entities; fall back to grouping breakdown.api_keys by team_id.

  • Global Usage page CSV exports were always empty because the aggregated endpoint (/user/daily/activity/aggregated) returns breakdown.entities = {} and the SQL GROUP BY collapses the entity dimension for performance
  • Added resolveEntities() fallback that reconstructs entities from breakdown.api_keys by grouping on metadata.team_id
  • All 3 export types (by team, by team+key, by team+model) now produce data
Screenshot 2026-03-16 at 10 41 15 PM Screenshot 2026-03-16 at 10 41 24 PM

Aggregated endpoint returns empty breakdown.entities; fall back to
grouping breakdown.api_keys by team_id.
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Mar 17, 2026 5:41am

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR fixes an empty CSV export on the Global Usage page by adding a client-side fallback (resolveEntities) that reconstructs team entities from breakdown.api_keys when the aggregated endpoint (/user/daily/activity/aggregated) returns an empty breakdown.entities. The fix is applied consistently to all three export paths (by team, by team+key, by team+model).

Key changes:

  • New METRIC_KEYS constant mirrors SpendMetrics from the backend, ensuring all numeric fields are aggregated in the fallback.
  • aggregateApiKeysIntoEntities groups api_keys by metadata.team_id, producing entities with the same shape the existing export functions already expect (metrics + api_key_breakdown).
  • resolveEntities is a thin wrapper that short-circuits to the real entities when they exist, keeping the normal (non-aggregated) path zero-cost.
  • Comprehensive unit tests cover the happy path, edge cases (missing team_id"Unassigned", empty api_keys), and all four generator functions under the aggregated response shape.
  • Minor: resolveEntities(day.breakdown) is invoked twice per day inside generateDailyWithModelsData — once to build dailyEntityModels and again to look up entityData. Caching the result would avoid redundant aggregation on the fallback path.

Confidence Score: 4/5

  • Safe to merge — fix is client-side only, well-tested, and does not alter the normal (populated entities) code path.
  • The fallback logic is straightforward and deterministic; it only activates when entities is empty so existing behaviour is completely unchanged. All four export types now have passing tests under the aggregated-response fixture. The one redundant resolveEntities call is a minor style issue and does not affect correctness.
  • No files require special attention.

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts Adds resolveEntities + aggregateApiKeysIntoEntities fallback that groups breakdown.api_keys by metadata.team_id when breakdown.entities is empty. Applied correctly to all four export functions. Minor: resolveEntities is called twice per day in generateDailyWithModelsData, re-running aggregation unnecessarily.
ui/litellm-dashboard/src/components/EntityUsageExport/utils.test.ts Comprehensive new test block covering resolveEntities (empty entities, populated entities, Unassigned fallback, missing api_keys, preserved api_key_breakdown) and all four export generators with aggregated data. Test for generateDailyWithModelsData only checks row existence, not spend values, which could mask model double-counting if the fixture is extended.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Export triggered] --> B[generateExportData]
    B --> C{exportScope}
    C -->|daily| D[generateDailyData]
    C -->|daily_with_keys| E[generateDailyWithKeysData]
    C -->|daily_with_models| F[generateDailyWithModelsData]

    D --> G[resolveEntities per day]
    E --> G
    F --> G

    G --> H{breakdown.entities populated?}
    H -->|Yes - normal path| I[Return breakdown.entities directly]
    H -->|No - aggregated endpoint| J[aggregateApiKeysIntoEntities]

    J --> K[Group breakdown.api_keys\nby metadata.team_id]
    K --> L[Accumulate METRIC_KEYS\nper team group]
    L --> M[Preserve api_key_breakdown\nper team]
    M --> N[Return reconstructed entities map]

    I --> O[Iterate entities → build rows]
    N --> O
    O --> P[CSV / JSON export]
Loading

Last reviewed commit: c098eca

Comment on lines 275 to +276
Object.entries(dailyEntityModels).forEach(([entity, models]) => {
const entityData = day.breakdown.entities?.[entity];
const entityData = resolveEntities(day.breakdown)[entity];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant resolveEntities call

resolveEntities(day.breakdown) is called a second time here (line 276), after already being called in the outer forEach at line 248. When the fallback path is taken, this re-runs the entire aggregateApiKeysIntoEntities aggregation for every entity in the second loop. Caching the result in a variable would prevent the redundant work.

Suggested change
Object.entries(dailyEntityModels).forEach(([entity, models]) => {
const entityData = day.breakdown.entities?.[entity];
const entityData = resolveEntities(day.breakdown)[entity];
const resolvedEntities = resolveEntities(day.breakdown);
Object.entries(resolvedEntities).forEach(([entity, entityData]: [string, any]) => {

Then replace both usages of resolveEntities(day.breakdown) in this function body with resolvedEntities.

Comment on lines +1672 to +1679
expect(result[0]).toHaveProperty("Team");
});
});

describe("generateDailyWithKeysData with aggregated data", () => {
it("should produce rows from api_keys when entities is empty", () => {
const result = generateDailyWithKeysData(aggregatedSpendData, "Team");
expect(result.length).toBeGreaterThan(0);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Weak assertion for model data

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 underlying generateDailyWithModelsData logic adds all API key metrics to every model in breakdown.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 to aggregatedSpendData.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:

expect(result.find(r => r.Model === "gpt-4")).toBeDefined();
// Verify spend is not double-counted when multiple models exist

@ryan-crabbe ryan-crabbe merged commit cde28aa into litellm_ryan_march_16 Mar 17, 2026
5 checks passed
@ishaan-berri ishaan-berri deleted the litellm_fix-csv-export branch March 26, 2026 22:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant