Skip to content

fix(ui): stop usage pagination from under-reporting wide date ranges - #36022

Open
devin-ai-integration[bot] wants to merge 2 commits into
litellm_internal_stagingfrom
litellm_fix_usage_wide_range_underreport
Open

fix(ui): stop usage pagination from under-reporting wide date ranges#36022
devin-ai-integration[bot] wants to merge 2 commits into
litellm_internal_stagingfrom
litellm_fix_usage_wide_range_underreport

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Usage charts and CSV under-report over wide date ranges
  • A date split across API pages appears twice, each partial
  • Export stays clickable while pages are still loading
  • A failed page silently leaves partial totals looking final

How it solves it:

  • Merge daily activity pages by date instead of concatenating
  • Disable Export until the whole range has loaded
  • Surface a page fetch failure as an error banner

User Flow

Before: an admin reviewing six months of team spend sees one day counted twice at partial amounts, and the CSV they hand to finance is short

  1. They open http://litellm-domain/ui/?page=usage and pick Team Usage
  2. They set the range to 2026-02-05 through 2026-08-05, which takes five pages to load
  3. 2026-06-25 reads $23.37 in the chart instead of $35.16, and the daily CSV carries 370 rows for 182 days because each boundary date is written twice per team with partial spend
  4. Export Data is clickable while the "Currently fetching spend data" banner is still up, so clicking it writes only the pages that had arrived
  5. If one page never comes back, the totals still look final, with nothing on screen saying the range is partial

After: the same range shows one row per day at the full amount, and Export waits for the whole range

  1. They open http://litellm-domain/ui/?page=usage and pick Team Usage
  2. They set the range to 2026-02-05 through 2026-08-05, which takes five pages to load
  3. 2026-06-25 reads $35.16 in the chart, and the daily CSV is 364 rows, one per date per team
  4. Export Data is disabled while pages are loading, and hovering it says the data on screen does not yet cover the range
  5. If a page fails, a red banner says totals cover only part of the range, and Export stays blocked

Relevant issues

Linear ticket

Resolves LIT-5045

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Shared setup: a live proxy on localhost:4000 with six months of team spend seeded into LiteLLM_DailyTeamSpend (2026-02-05 through 2026-08-05, 2 teams, 4 keys each, 3 models, 4368 rows over 182 distinct dates, SUM(spend) = 12951.12). The endpoint paginates raw rows, so a date lands on two different pages, each carrying only its share:

for p in 1 2 3 4 5; do
  curl -sG "http://localhost:4000/team/daily/activity" \
    -H "Authorization: Bearer sk-1234" \
    --data-urlencode start_date=2026-02-05 --data-urlencode end_date=2026-08-05 \
    --data-urlencode page_size=1000 --data-urlencode page=$p -o /tmp/page$p.json
done
jq -r '.results[] | select(.date == "2026-06-25") | "\(.date) \(.metrics.spend)"' /tmp/page*.json

2026-06-25 23.3699...
2026-06-25 11.7935...

Before (5277dab)

Folding those five live page payloads the way the hook did on the merge base, [...accumulatedResults, ...pageData.results]:

  1. The same date arrives as two separate entries, so the chart tooltip for 2026-06-25 reads $23.37 rather than $35.16
  2. The exported daily CSV is 370 rows over 182 dates: all three boundary dates (2026-06-25, 2026-05-14, 2026-02-20) are written twice per team with partial spend
  3. Export Data stays clickable while the loading banner is up, so exporting mid load writes only the pages that had arrived

After (45ad441)

Same five payloads through mergeDailyResults as shipped here:

  1. 182 entries for 182 dates, total spend 12951.12, matching the seeded SUM(spend) and the accumulated metadata
  2. 2026-06-25 is a single entry at 35.16, with the entities and api_key_breakdown buckets summed too
  3. 2026-05-14 is 35.16 and 2026-02-20 is 47.16, each one entry, and the exported daily CSV is 364 rows summing to 12951.12
  4. Export Data is disabled from the very first page fetch, before the pagination banner even appears, and hovering it says "Spend data is still loading, so an export would under-report. Wait for it to finish."; forcing a page fetch to throw shows the red "Fetching spend data failed, so totals cover only part of the range" banner with Export still blocked

Browser run of both revisions, with the screenshots and the screen recording, is in the Slack thread: https://berriaillm.slack.com/archives/C0BE49SAUE6/p1787012100873179?thread_ts=1787012100.873179&cid=C0BE49SAUE6 (Before captured at 5277dab, After re-run at the current tip 45ad441). The same screenshots are also in a comment on this PR

Reviewer steps to reproduce it yourself (proxy on :4000, npm run dev in ui/litellm-dashboard on :3000):

  1. Open http://localhost:4000/ui/?page=usage and pick the Team Usage view
  2. Set the date range to six months so the fetch takes several pages
  3. While the "Currently fetching spend data" banner is up, confirm Export Data is disabled and hovering it explains why
  4. Let it finish, click Export Data, pick Daily, and confirm the CSV total matches the Total Spend card
  5. To see the failure banner, block one /team/daily/activity page request in devtools and confirm the red banner appears instead of the totals looking final

Type

🐛 Bug Fix

Changes

mergeDailyActivity.ts is the new piece. It folds an incoming page into the accumulated series by date, adding metrics and recursively merging every breakdown bucket (models, model_groups, mcp_servers, providers, entities, endpoints, api_keys, and the nested api_key_breakdown) so a split date ends up identical to what a single unpaginated query would have returned:

export const mergeDailyResults = (existing, incoming) =>
  incoming.reduce((acc, day) => {
    const index = acc.findIndex((d) => d.date === day.date);
    if (index === -1) return [...acc, day];
    return acc.map((d, i) => (i === index ? mergeDay(d, day) : d));
  }, [...existing]);

usePaginatedDailyActivity runs each page through that instead of concatenating, and reports two new flags: failed when a page fetch throws, and incomplete for isFetchingMore || cancelled || failed. UsageExportHeader takes exportDisabled plus a reason surfaced as a title on the wrapper, and EntityUsage passes incomplete into it, so the export can no longer run against a half loaded range. EntityUsage and UsagePageView also render the partial-data banner as an error when failed is set, using the shared Alert component the usage pages now use.

Tests cover the merge in isolation (mergeDailyActivity.test.ts), the hook end to end over a date straddling two pages plus the failure path (usePaginatedDailyActivity.test.ts), and the blocked export (UsageExportHeader.test.tsx).

Link to Devin session: https://app.devin.ai/sessions/d0a988ce9c1542b0a5cec9f531c2e0a4


Note

Cursor Bugbot is generating a summary for commit 45ad441. Configure here.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR merges duplicate dates across paginated daily-activity responses, exposes incomplete and failed fetch state, and disables exports while later pages are loading. It also adds failure banners and focused tests, but the export guard does not cover the first-page loading window.

  • Recursively combines daily totals and breakdown buckets for dates split across pages.
  • Tracks pagination failure and incomplete state in the activity hook.
  • Disables usage export for later-page loading, cancellation, and failure.
  • Adds merge, pagination-failure, and export-button tests.

Confidence Score: 4/5

The first-page loading gap must be fixed before merging because users can still export empty or stale usage data for the currently selected range.

The merge and later-page failure handling are sound, but incomplete becomes false while the first page is pending even though the hook retains previous data and the export path has no secondary guard.

Files Needing Attention: ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts, ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/mergeDailyActivity.ts

Important Files Changed

Filename Overview
ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts Integrates date-aware merging and failure state, but omits initial loading from incomplete, leaving export enabled with empty or stale data.
ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/mergeDailyActivity.ts Correctly sums every current SpendMetrics field and recursively merges all daily breakdown buckets; it also adds comments prohibited by repository guidance.
ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx Displays partial-data failures and wires incomplete state into export disabling, inheriting the hook’s first-page loading gap.
ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx Adds disabled-button and tooltip support correctly, with no secondary guard if the supplied disabled state is false.
ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx Distinguishes failed pagination from user cancellation in the partial-data banner.
ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.test.ts Covers split dates and failed later pages but does not test export-related completeness during the first-page loading window.

Reviews (1): Last reviewed commit: "fix(ui): stop usage pagination from unde..." | Re-trigger Greptile

}, [enabled, fetchFn, argsKey]);

return { data, loading, isFetchingMore, progress, cancelled, cancel };
const incomplete = isFetchingMore || cancelled || failed;

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.

P1 Initial loading leaves export enabled

When the initial page is loading after mount or a range or filter change, incomplete remains false while the hook retains empty or previous data, so Export Data stays enabled and can download a file that does not represent the current selection.

Suggested change
const incomplete = isFetchingMore || cancelled || failed;
const incomplete = loading || isFetchingMore || cancelled || failed;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, incomplete now includes loading, so export is blocked during the first page too, with a regression test

Comment on lines +75 to +80
});

/**
* Combine daily activity pages into one series with a single entry per date.
*
* The backend paginates over raw spend rows, so a date whose rows straddle a

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 New comments violate repository guidance

This explanatory block, along with the new field comments in usePaginatedDailyActivity.ts and prop comment in UsageExportHeader.tsx, violates the repository rule against adding comments unless the user explicitly requested them, adding cleanup work before the change satisfies project conventions.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right, removed the doc block plus the field and prop comments in the hook and export header

Merge daily activity pages by date and block the CSV export until the whole range is loaded.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the litellm_fix_usage_wide_range_underreport branch from 56c9edf to fe131b8 Compare August 18, 2026 00:43
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Verified in the browser against a seeded 6 month range: pages now merge per date, and export stays blocked while incomplete.

After the fix (fe131b8): 2026-06-25 merged to $35.16, total $12,951.12

branch merged date tooltip

Daily CSV: 364 rows, 182 distinct dates, spend sum 12951.12, split dates 2026-06-25 = 35.16, 2026-05-14 = 35.16, 2026-02-20 = 47.16

Before (base 5277dab): the same date reports only $23.37

base partial date tooltip

Base daily CSV has 370 rows: each of the 3 boundary dates appears twice per team with partial spend. Export Data was also clickable mid load.

Export gating while loading and after a failed page

loading, export disabled
failed page, red banner, export blocked

Written by Devin

Also drop the explanatory comments the repo guidelines disallow.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Re-verified at 45ad441: Export Data is greyed from the first page fetch, before the pagination banner shows.

First page in flight: no banner yet, Export disabled with the reason on hover

export disabled during initial load

After all 5 pages: Export enabled, total $12,951.12, 2026-06-25 merged to $35.16

final totals with export enabled

merged 06-25 tooltip

Daily CSV: 364 rows, 182 distinct dates, spend sum 12951.12, split dates 06-25 = 35.16, 05-14 = 35.16, 02-20 = 47.16

@shivamrawat1

Copy link
Copy Markdown
Contributor

@gerptile review

@shivamrawat1

Copy link
Copy Markdown
Contributor

@BugBot review

@cursor cursor Bot left a comment

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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 45ad441. Configure here.

(acc, key) =>
a[key] === undefined && b[key] === undefined ? acc : { ...acc, [key]: (a[key] ?? 0) + (b[key] ?? 0) },
{} as SpendMetrics,
);

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.

Merge drops daily flat cost

High Severity

METRIC_KEYS omits flat_cost, so addMetrics rebuilds day and breakdown metrics without it. When a date straddles pages, chart Flat Cost and CSV Flat Cost ($) rows go to zero even though total_flat_cost in metadata still sums correctly, so wide Team Usage ranges under-report PTU flat cost on the exact split days this PR aims to fix.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 45ad441. Configure here.

failed
? "Spend data failed to load for the whole range, so an export would under-report. Reload the page first."
: "Spend data is still loading, so an export would under-report. Wait for it to finish."
}

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.

Cancelled export shows loading reason

Low Severity

incomplete is true when the user cancels, so Export stays disabled, but exportDisabledReason only special-cases failed and otherwise says spend data is still loading and to wait. After Stop, nothing is loading, so the tooltip steers users to wait for a fetch that will never finish.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 45ad441. Configure here.

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.

2 participants