Repository navigation
feat(proxy): server-side global Usage export beyond the top-N key cap - #43138
devin-ai-integration[bot] wants to merge 2 commits into
Conversation
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
|
|
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
yuneng-berri
left a comment
There was a problem hiding this comment.
looks like the user and user_id columns are filled in with team_ids under the cap
|
@yuneng-berri good catch, fixed in 7f2d1fc: the client-side export now groups keys by user_id for user exports. Under-cap CSV proof is in the PR body |
|
@greptileai review the current head 4ac163c (merge of main, no source changes since ccd3956) |
Resolves LIT-8617 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
4ac163c to
c5f6886
Compare
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@greptileai review the current head b67a956 (restacked on #43137, plus the user-label lookup now chunked through find_many_in) |
TLDR
Problem this solves:
How it solves it:
Intentional product change: the global export modal now labels rows by user instead of team and drops the "by user" scope (the global table already is per user), so filenames become
user_usage_*and the CSV entity columns read User / User IDUser Flow
Before: an admin with more keys than USAGE_TOP_API_KEYS_LIMIT cannot export global usage at all
api_key_limit: 2, total_api_keys: 3After: the same admin exports every key from the server
user_usage_daily_with_keys_<date>.csvfrom GET /user/daily/activity/export with all 3 keysLinear ticket
Resolves LIT-8617
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or moreAnalyze (python)fails on main too: LogInjection.ql result set over 2 GiB, reproduced on main head f4308bc)@greptileaito 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
Setup: live proxy started from this branch with
against local Postgres. Three virtual keys (
lit8617-k1..k3) each made one realanthropic/claude-haiku-4-5chat completion through POST http://localhost:4000/v1/chat/completions (real provider spend), and the daily rollup flushed three rows into the user spend tableBefore (origin/main 1e6c983, unfixed proxy on :4001 with the main dashboard on :3001, marker:
/openapi.jsonhas 0 hits for/user/daily/activity/export)Admin UI global Usage export
Aggregated response is capped
curl -s "http://localhost:4000/user/daily/activity/aggregated?start_date=2026-09-26&end_date=2026-09-26&timezone=0" -H "Authorization: Bearer $MASTER_KEY" | jq '.metadata | {api_key_limit, total_api_keys}'{ "api_key_limit": 2, "total_api_keys": 3 }andresults[].breakdown.api_keysholds only 2 of the 3 digestsServer-side export
/user/daily/activity/exportroute is registered at this commit, so the UI has nothing to fall back to and stays disabled as shown above (the Before UI capture ran the dashboard from amainworktree against the same proxy and data)After (4ac163c, this branch merged with current main, proxy on :4000 with the branch dashboard on :3000, marker:
/openapi.jsonhas 1 hit for/user/daily/activity/export)Admin UI global Usage export
user_usage_daily_with_keys_2026-09-26.csv(601 B) served by GET /user/daily/activity/export; it holds all 3 Key IDs while the Top Virtual Keys table only loaded 2Aggregated response is capped
{ "api_key_limit": 2, "total_api_keys": 3 }, unchangedServer-side export
curl -si "http://localhost:4000/user/daily/activity/export?start_date=2026-09-26&end_date=2026-09-26&timezone=0&export_type=daily_with_keys&format=csv" -H "Authorization: Bearer $MASTER_KEY"format=jsonwithexport_type=dailyreturns 200 and adatarow forlit8617-memberexport_type=daily_with_models&format=csvreturnscontent-disposition: attachment; filename="user_usage_daily_with_models_2026-09-26.csv"and the row2026-09-26,lit8617-member,lit8617-member,anthropic/claude-haiku-4-5,0.0000,1,1,0,17,12,5,0,0Reviewer fix (yuneng-berri, 7f2d1fc): under the cap the User columns held team ids
What was flagged: under
USAGE_TOP_API_KEYS_LIMITthe modal still exports client-side, and/user/daily/activity/aggregatedreturns nobreakdown.entities, so the client rebuilt entities frombreakdown.api_keysgrouped bymetadata.team_id. With the modal relabelled to users, the User and User ID columns showed team ids (or Unassigned for keys without a team). Fix commit7f2d1fca8684e213a5f9772ff56a5db4df2764b5: the client groups bymetadata.user_idwhen the entity is a user, and keeps grouping byteam_idfor every other entity. Regression tests cover both inutils.test.tsFollow-up from Greptile on that commit,
c44da4394candccd3956570: the grouping map is nowObject.create(null), so user ids that collide with object properties such asconstructoror__proto__are exported as ordinary rows instead of crashing or being dropped, with a regression test inutils.test.tsLive proof on the running proxy (
USAGE_TOP_API_KEYS_LIMIT=100, so the aggregated response is not truncated and the export stays client-side). Setup, as admin:POST /team/newwithteam_id: lit8617-teamand memberlit8617-member,POST /key/generatewithteam_id: lit8617-team,user_id: lit8617-member,key_alias: lit8617-team-key, onePOST /v1/chat/completionsonanthropic-haiku-4-5with that key (real Anthropic call).GET /user/daily/activity/aggregated?start_date=2026-09-26&end_date=2026-09-26then shows that key withmetadata: {team_id: lit8617-team, user_id: lit8617-member}and an emptybreakdown.entitiesReproduce in the Admin UI: http://localhost:3000/usage/, Global Usage, Export Data, Day-by-day breakdown by user and key, CSV, Export CSV. The raw rows below are from the two downloaded CSVs filtered to the lit8617 keys
Before (6a6de8c): the team key is attributed to
lit8617-team, and the member's team-less key to Unassigned, so one user shows up as two rows under two wrong identitiesAfter (7f2d1fc): both keys roll up under
lit8617-memberRaw rows from the two downloaded
user_usage_daily_with_keys_2026-09-26.csvfiles,grep lit8617-team-key:The plain Day-by-day breakdown by user export on the fixed head gives one row per user, for example
2026-09-26,integration-d41ac2351e0d47ac99b5717fc6225366,integration-d41ac2351e0d47ac99b5717fc6225366,0.1800,3,3,0,120,60,60,0,0Non-admin scoping
POST /user/newwithuser_role: internal_usercreatedlit8617-member,/key/generateminted$MEMBER_KEYunder it and one chat completion was made with itcurl -s "http://localhost:4000/user/daily/activity/export?start_date=2026-09-26&end_date=2026-09-26&timezone=0&export_type=daily&format=json" -H "Authorization: Bearer $MEMBER_KEY"returns 200 withmetadata.user_id: "lit8617-member"and a single row for that user&user_id=lit8617-otherreturns403 {"detail":{"error":"Non-admin users can only view their own spend data."}}user_id, so a scoped view exports the same scope it displaysLint fix on the restacked head (b67a956): user-label lookup chunked
CI
code-qualityflagged the new_resolve_export_userslookup as an unbounded Prismainfilter over every exported user id. It now goes throughfind_many_infromlitellm/repositories/chunked_in.py, the same chunking helper the rest of the repo uses, so a large export cannot exceed the Postgres parameter limit. Behaviour is unchanged; the unbounded-IN checker reports 0 new, and the export tests intest_internal_user_endpoints.pystill passLive re-verify on this head (proxy from this branch,
USAGE_TOP_API_KEYS_LIMIT=2, three userslit8617-u1..u3each with one daily row):All three keys are returned past the cap of 2 and the User column resolves emails through the chunked lookup
Taxonomy audit (current diff against the base, re-run on b67a956)
Fixed on this head: B-style unbounded query (the user-label
inlist is now chunked throughfind_many_in). Fixed earlier in this PR: A1-style grouping identity (client export grouped users by team id, now bymetadata.user_id), a prototype-key collision in the grouping map (Object.create(null)), and CSV formula injection on identifier columns (csv_safe)Judged not applicable with the reason: D2/Z7 (entity id is resolved through
_resolve_user_daily_activity_entity_id, the same helper the aggregated route uses, so admin and non-admin scoping cannot diverge); X1/E5 (noorfallback on spend, token or request counts, only on nullable labels where empty means absent); W1 (the route builds new tuples and dicts, it does not mutate the caller's request or rows); F3/O4 (sibling team export already exists from #42996 and sharesget_daily_activity_export_rows, the tag and org usage pages have no export modal); C1-C7 (new route only, no default flips or status code changes on existing routes; the modal relabel from team to user is called out under TLDR); H1-H10 (no new source comments, no em dashes in copy,X | Nonetyping, noAnyin new backend code,schema.d.tsregenerated, and the dashboard prebuilt bundle is produced by the release build, not committed); T1-T5 (integration tests assert the composed CSV rows and the 403 body, andtest_user_activity_export_returns_every_key_beyond_the_top_n_capfails on main because the route 404s)Type
🆕 New Feature
Caveats (if any)
Low
User ID: UnassignedUsercolumn is the user's email, else alias, else the user id, looked up from the user table in one extra query per export; JSON carriesuser_emailanduser_aliasas separate fieldsuser_id, no email), so itsUsercolumn falls back to the user idLink to Devin session: https://app.devin.ai/sessions/f364d9a3f7e54d91a11d089b02390304
Open in Devin Desktop: https://app.devin.ai/desktop/session/f364d9a3f7e54d91a11d089b02390304?variant=devin
Requested by: @yassin-berriai