Skip to content

feat(proxy): add /team/daily/activity/aggregated and switch the Usage team tab to it - #36562

Merged
ryan-crabbe-berri merged 9 commits into
litellm_internal_stagingfrom
litellm_team_daily_activity_aggregated
Aug 18, 2026
Merged

feat(proxy): add /team/daily/activity/aggregated and switch the Usage team tab to it#36562
ryan-crabbe-berri merged 9 commits into
litellm_internal_stagingfrom
litellm_team_daily_activity_aggregated

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Team Usage chart drew the same day as two partial bars
  • Big teams painted newest days first, backfilling in page batches
  • A failed page fetch silently left partial totals looking final
  • Client had to drain and reassemble up to N pages per view

How it solves it:

  • New GET /team/daily/activity/aggregated returns the whole range in one response
  • A small companion rollup query fills the per-team breakdown
  • Usage UI team tab calls it first, falls back to page draining on error
  • Both team endpoints share one permission scope resolver

User Flow

Before: an admin of a busy team opens the usage page and the daily chart shows duplicate and shifting bars while data trickles in

  1. They open http://localhost:4000/ui/?page=new_usage, pick Team Usage and a 30d range
  2. The browser fires GET /team/daily/activity?start_date=...&end_date=...&page_size=1000&page=1, then page=2, page=3, ... with a "Currently fetching spend data" banner
  3. The chart first shows only the newest days, then backfills in bursts as pages land
  4. When a day's records straddle two pages, that date appears as two shorter adjacent bars, so a 10 day range renders 12 bars and per-day spend reads wrong

After: the same admin opens the same page and gets every day at once, one bar per day

  1. They open http://localhost:4000/ui/?page=new_usage, pick Team Usage and a 30d range
  2. The browser fires exactly one GET /team/daily/activity/aggregated?start_date=...&end_date=...&timezone=...&exclude_team_ids=litellm-dashboard and no page=N requests
  3. The chart renders all days immediately, one bar per date, and Spend Per Team shows each team's spend, requests, and tokens
  4. Selecting a specific team fires one more aggregated request with team_ids=... and totals match the seeded spend exactly
  5. A team member without admin rights hitting the same URL still sees only their own keys' spend, and naming a team they don't belong to still returns 404

Relevant issues

Linear ticket

Resolves LIT-2066

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

Live proxy on 127.0.0.1:4153, throwaway Postgres seeded with 2,344 LiteLLM_DailyTeamSpend rows across 10 days and 2 teams: team "Users" $16.70 (1,500 rows on the newest day, 800 the day before), team "Platform" $1.00, grand total $17.70. Captured at a4a15ea

Before (old endpoint, unchanged): page 1 of the paginated route only contains the newest day, so anything that reads one page under-reports

curl -s -G "http://127.0.0.1:4153/team/daily/activity" -H "Authorization: Bearer sk-1234" \
  --data-urlencode "start_date=2026-07-12" --data-urlencode "end_date=2026-08-11" \
  --data-urlencode "page_size=1000" --data-urlencode "page=1"
# -> results: 1 date (2026-08-11 only), total_pages: 3, has_more: true, total_spend: 2.138 (true total 17.70)

After (new endpoint): the whole range in one response with per-team breakdown

curl -s -G "http://127.0.0.1:4153/team/daily/activity/aggregated" -H "Authorization: Bearer sk-1234" \
  --data-urlencode "start_date=2026-07-12" --data-urlencode "end_date=2026-08-11" \
  --data-urlencode "exclude_team_ids=litellm-dashboard" --data-urlencode "timezone=420"
# -> 10 results rows, one per date 2026-08-02..2026-08-11
# -> total_pages: 1, has_more: false, total_spend: 17.7 (exact seeded sum)
# -> each day's breakdown.entities carries both teams with team_alias and a per-key
#    split that reconciles to the entity spend; models/api_keys/providers still populated

Non-admin team member key (their key has $0.10 of the seed): HTTP 200, total_spend 0.10, their key hash is the only api_key anywhere in the response, and requesting another team's id returns 404 "User does not belong to Team"

UI before at be5e900, 12 bars for 10 dates (2026-08-09 and 08-10 each drawn twice at the page boundary):

before duplicate bars

UI after at a4a15ea, one aggregated request, 10 bars for 10 dates, Spend Per Team populated:

after all days

after team filtered

Type

🆕 New Feature
🐛 Bug Fix

Caveats (if any)

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

…age UI

The Team Usage tab drained row-paginated pages client side, which painted
newest days first and drew duplicate bars when a day's rows straddled a
page boundary. Serve the whole range in one SQL GROUPING SETS pass instead:
the aggregated query gains optional per-entity rollup levels (entity as the
most-significant GROUPING bit) so breakdown.entities keeps per-team spend,
aliases, and per-key splits. The endpoint shares the paginated route's
scoping via _resolve_team_daily_activity_scope, accepts the timezone the UI
already sends, and the api_key filter now takes a list so non-admin member
scoping works. The dashboard tries the aggregated endpoint first and falls
back to page draining on failure.
@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds an aggregated team daily-activity endpoint and switches Team Usage to prefer its complete-range response, with paginated fallback

  • Adds a companion per-team rollup query while preserving the primary grouping-set query
  • Shares permission scoping between team activity endpoints and adds date-range validation
  • Updates dashboard networking, fetching behavior, generated API types, and regression coverage

Confidence Score: 5/5

The PR appears safe to merge

No blocking failure remains

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/common_daily_activity.py Adds a separate entity-rollup query and folds its results into each day without changing the primary grouping bitmask
litellm/proxy/management_endpoints/team_endpoints.py Adds the aggregated route, shared team permission scoping, and bounded date validation
ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts Prefers an optional single-shot aggregate fetch and falls back to the existing paginated flow
ui/litellm-dashboard/src/components/networking.tsx Adds the dashboard client wrapper for aggregated team daily activity
ui/litellm-dashboard/src/lib/http/schema.d.ts Regenerates OpenAPI types for the new endpoint

Reviews (4): Last reviewed commit: "refactor(proxy): fetch entity rollups wi..." | Re-trigger Greptile

Comment thread litellm/proxy/management_endpoints/common_daily_activity.py Outdated
Comment thread litellm/proxy/management_endpoints/team_endpoints.py
@veria-ai

veria-ai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.50000% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...roxy/management_endpoints/common_daily_activity.py 85.07% 10 Missing ⚠️
...tellm/proxy/management_endpoints/team_endpoints.py 90.90% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

…ctivity

The aggregated endpoint has no pagination bounding its work, so validate
start_date and end_date as real dates and cap the span at 400 days. The
dashboard's widest presets fit well inside the cap, and an over-cap range
falls back to the paginated flow. Also trim implementation comments that
restated the grouping-set code.
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai removed the redundant implementation comments and added date validation plus a 400 day range cap on the aggregated endpoint, please re-review

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

…of extending the main one

The entity-as-extra-GROUPING-bit approach made the bitmask layout
mode-dependent: the same constant meant (date) for normal rows and
(date, entity) for entity rows, disambiguated by masking. Split it out:
the shared WHERE builder feeds both the untouched main query and a small
per-entity rollup query keyed by GROUPING(api_key), run concurrently, and
a fold writes breakdown.entities onto the built response.
@codspeed-hq

codspeed-hq Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_team_daily_activity_aggregated (2c482a7) with litellm_internal_staging (8159f24)

Open in CodSpeed

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai reworked the entity rollups into a separate companion query so the main GROUPING SETS query and its bitmask are untouched, please re-review

…hapes

The type-discipline ceiling for LIT002 ratcheted down on staging, so the new
aggregated endpoint had to stop hand-rolling collections the codebase already
builds elsewhere. Funnel the `{"error": ...}` detail through one construction
site, turn the range validator into an error-as-value, reuse a single
entity-metadata lookup for both breakdown paths, and widen
get_api_key_metadata to any set so callers stop copying frozensets.
@ryan-crabbe-berri
ryan-crabbe-berri merged commit 9ec0145 into litellm_internal_staging Aug 18, 2026
72 of 73 checks passed
@ryan-crabbe-berri
ryan-crabbe-berri deleted the litellm_team_daily_activity_aggregated branch August 18, 2026 18:30
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