Repository navigation
fix(usage): rank per-model top keys server-side over all keys - #43137
devin-ai-integration[bot] wants to merge 3 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! |
|
@greptileai addressed both threads: closed rows no longer refetch, and a database-backed per-model ranking test was added |
|
@greptileai merged main and added a database-backed per-model ranking regression test at 949123b, please re-review |
Resolves LIT-8618 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
949123b to
317cc1e
Compare
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
@greptileai please re-review at 4c199bb (main merged in, integration test now uses distinct underlying models) |
TLDR
Problem this solves:
How it solves it:
GET /user|team/daily/activity/aggregated/model_top_keysranks keys per model over all keysUser Flow
Before: an admin opens a model row in Usage and the Top Virtual Keys card misses the key that spent the most on it
claude-sonnet-4-5rowlit8618-heavy-a(1 request) is shown,lit8618-claude-only(2 requests, the model's biggest spender) is absentAfter: the same row lists the real top spenders for that model
claude-sonnet-4-5rowlit8618-claude-only(2 requests, 60 tokens) first andlit8618-heavy-a(1 request, 30 tokens) second, in both the Models and Model Groups viewsLinear ticket
Resolves LIT-8618
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 more@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Setup shared by both legs: local Postgres, proxy started with
PYTHONPATH=<checkout> USAGE_TOP_API_KEYS_LIMIT=2 LITELLM_LOCAL_MODEL_COST_MAP=True python litellm/proxy/proxy_cli.py --config ~/lit8618/config.yaml --port 4000 --use_v2_migration_resolver, modelsgpt-4.1-mini(openai) andclaude-sonnet-4-5(anthropic), three keys created throughPOST /key/generatewith aliaseslit8618-heavy-a,lit8618-heavy-b,lit8618-claude-only, and real traffic sent throughPOST /v1/chat/completionsonce, so the sameLiteLLM_DailyUserSpendrows back both legs:Global top 2 keys by total spend are
heavy-a($0.001626) andheavy-b($0.00126).claude-only($0.00066) is outside the cap although it is the top key onclaude-sonnet-4-5Before (1e6c983, origin/main)
Aggregated model breakdown drops the model's top key
grep -c model_top_keys litellm/proxy/management_endpoints/internal_user_endpoints.pyprints0curl -s -H "Authorization: Bearer sk-1234" "http://localhost:4000/user/daily/activity/aggregated?start_date=2026-09-19&end_date=2026-09-27"then reading.results[0].breakdown.models["anthropic/claude-sonnet-4-5"].api_key_breakdowngives one key only:lit8618-heavy-a.lit8618-claude-onlyis missing, so the dashboard ranks from one keyPer-model route does not exist
curl -s -o /dev/null -w "HTTP %{http_code}\n" -H "Authorization: Bearer sk-1234" "http://localhost:4000/user/daily/activity/aggregated/model_top_keys?start_date=2026-09-19&end_date=2026-09-27&model=anthropic/claude-sonnet-4-5&group_by=model"Admin UI
claude-sonnet-4-5row listslit8618-heavy-aonlyAfter (949123b)
Aggregated model breakdown drops the model's top key
grep -c model_top_keys litellm/proxy/management_endpoints/internal_user_endpoints.pyprints3{'9a87394760': 0.00033}), by design: the dashboard no longer readsapi_key_breakdownfor the Top Virtual Keys card on model rowsPer-model route does not exist
curl -s -H "Authorization: Bearer sk-1234" "http://localhost:4000/user/daily/activity/aggregated/model_top_keys?start_date=2026-09-19&end_date=2026-09-27&model=anthropic/claude-sonnet-4-5&group_by=model"returns(key_alias, spend, api_requests):model=claude-sonnet-4-5&group_by=model_groupreturns the same two keys in the same orderAdmin UI
claude-sonnet-4-5row listslit8618-claude-only(2 requests, 60 tokens) first, thenlit8618-heavy-a(1 request, 30 tokens)After, re-run on 4c199bb (main merged in)
Same proxy,
USAGE_TOP_API_KEYS_LIMIT=2, fresh keyslit8618v2-heavy-a(3 requests onopenai/gpt-4.1-mini, 1 onopenai/gpt-4.1),lit8618v2-heavy-b(3 ongpt-4.1-mini) andlit8618v2-b-only(2 ongpt-4.1). The database also holds older keys that outspend all three, so the global top two contain none of themcurl -s -H "Authorization: Bearer $KEY" "http://localhost:4000/user/daily/activity/aggregated?start_date=2026-09-26&end_date=2026-09-28" | jq '.results[0].breakdown.models["openai/gpt-4.1"].api_key_breakdown | map_values(.metrics.spend)'prints{}: the capped aggregate has no key at all for this model, so the old client-side card would be emptycurl -s -H "Authorization: Bearer $KEY" "http://localhost:4000/user/daily/activity/aggregated/model_top_keys?start_date=2026-09-26&end_date=2026-09-28&model=openai/gpt-4.1&group_by=model" | jq '[.api_keys[] | [.key_alias, .spend, .api_requests]]'prints[["lit8618v2-b-only",0.12,2],["lit8618v2-heavy-a",0.06,1]]model=<public model group name>&group_by=model_groupprints the same two keys in the same orderRegression test
tests/integration/spend/test_team_daily_activity_aggregated.py::test_model_top_keys_ranks_keys_by_spend_on_the_model_not_total_spenddrives the real proxy against Postgres: a key that spends more in total but less on the queried model ranks second on/team/daily/activity/aggregated/model_top_keys, with exact spend values. The two scenario models now use distinct underlyingmodelvalues (openai/gpt-4o-minivsopenai/gpt-4o), since the defaultgroup_by=modelfilters the underlying model column, not the public aliastests/test_litellm/proxy/management_endpoints/test_common_daily_activity.py::test_model_top_api_keys_ranks_a_key_the_global_cap_droppedseedsUSAGE_TOP_API_KEYS_LIMIT + 2keys into a real Postgres, asserts the aggregated breakdown drops the outlier and the per-model query ranks it first with exact spend, request and token values. Mutation check: adding a global top-Napi_key IN (...)filter to the per-model query fails it with[('key-101', 0.1)] == [('key-outlier', 0.5), ('key-101', 0.1)]Type
🐛 Bug Fix
Caveats (if any)
Low
litellm/proxy/_experimental/out/is not rebuilt here; the weeklychore: update Next.js build artifactsjob owns itui/andtests/test_litellm/without apaths:filter; X4userId || undefinedand emptyteam_idsmatch sibling calls and the backend's empty-list-means-nothing contract; D1 the new route sits in every route group its/aggregatedsibling already does; no new module-levelproxy_serverimport, cyclic-import check cleanFinal Attestation
Link to Devin session: https://app.devin.ai/sessions/8dbd3ca55e154740a7e27c87544f9082
Open in Devin Desktop: https://app.devin.ai/desktop/session/8dbd3ca55e154740a7e27c87544f9082?variant=devin
Requested by: @yassin-berriai