test(e2e): user budget across keys and team member budget isolation - #33745
Conversation
…solation Gap-fill for the budgets e2e suite against two Quota Management behavior contracts that had incomplete coverage. A per-user max_budget must be enforced across all of that user's keys, not only the key that spent it down. The prior internal-user test drove a single key, so it never proved the budget is shared. The new test gives one internal user a tiny max_budget and two keys carrying no budget of their own, drives the first key to a budget_exceeded block, then shows the untouched second key is rejected the same way and the user's recorded spend has crossed the cap. Per-team-member budgets must be enforced independently between members. The prior team-member test used one member, so it never proved isolation. The new test puts two members on one team with a large team budget, caps the tight member at a tiny per-team budget and drives it past the block, then shows the roomy member still serves on the same team with its calls attributed to its own user id in the spend logs while the tight member stays blocked. Adds a /user/info read-back helper, two coverage-registry rows, and the two new assertion tokens to the naming grammar so the collector --strict passes. Resolves LIT-4548
|
|
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
osv-scan is red here but it is base drift, not from this change: it fails identically on every recent open PR (a CVE published after the base branch's last lockfile bump). This PR touches only |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR adds two new live e2e tests that close gaps in the quota management budget suite: one proves that a per-user
Confidence Score: 5/5Test-only PR; no production code is touched. Both tests are well-scoped and clean up after themselves. All five changed files are under tests/e2e/. The previous concern about the second key looping up to 40 times is directly addressed by _expect_prompt_block, which bounds the retry to 6 calls — small enough that the second key could never block itself through key-scoped spend accumulation since it carries no per-key budget. Both new tests include the required dual-sided assertions. No logic gaps or structural issues found. No files require special attention.
|
| Filename | Overview |
|---|---|
| tests/e2e/quota_management/budgets/test_user_budget_across_keys_e2e.py | New e2e test for shared user budget enforcement across keys. _expect_prompt_block (6 bounded attempts) replaces the previous 40-call loop on the second key, cleanly separating shared-budget signals from key-scoped accumulation. |
| tests/e2e/quota_management/budgets/test_team_member_budget_isolation_e2e.py | New e2e test proving per-team-member budget isolation. Two-member fixture with fixture-scoped teardown; tight member blocked first, then roomy member served 3 calls, spend attribution asserted via poll_logs_for_key. Logic is sound. |
| tests/e2e/quota_management/budgets/budget_client.py | Adds UserInfoParams, UserInfoRow, UserInfoResponse models and a user_info helper that reads back recorded spend. Consistent with existing GET-param helpers; silently returns None on transport failure. |
| tests/e2e/coverage_registry/quota_management.yaml | Two new registry rows added for enforced_across_keys and isolates_per_member; both match the @covers markers in the new test files. |
| tests/e2e/CLAUDE.md | Doc-only: two new assertion tokens (isolates_per_member, enforced_across_keys) appended to the naming grammar to keep the vocabulary list in sync with the new tests. |
Reviews (2): Last reviewed commit: "test(e2e): block the second key promptly..." | Re-trigger Greptile
…udget Drive only the first key to exhaust the shared user budget, then require the second key (which has no budget of its own) to be rejected within a small bounded number of calls rather than driving it to accumulate its own spend. The block can then only come from the shared user budget, not the second key's own usage.
|
@greptileai please review the current head c27515c. It addresses the 4/5 concern on the second-key blocking strategy: the second key is no longer driven through a 40-call loop. Since the shared user budget is already exhausted when the first key blocks, the untouched second key (which carries no budget of its own) is now required to block within a small bounded number of calls via |
62207ac
into
litellm_internal_staging
Relevant issues
Linear ticket
Resolves LIT-4548
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Screenshots / Proof of Fix
Both new tests pass on a live docker-compose proxy stack against the real OpenAI API with real spend, captured at commit
c27515c5b7Gate checks, same commit
Independent e2e verification (Devin)
Devin independently checked out
c27515c5b7, stood up thetests/e2edocker-compose stack, sanity-checked the OpenAIgpt-5.5key (HTTP 200), and ran both new tests to2 passed in 20.73sagainst real spend. Terminal recording of the run:Full-resolution recording: https://raw.githubusercontent.com/yassin-berriai/litellm-pr-media/main/lit4548/e2e-run.mp4
Type
✅ Test
Changes
Gap-fill for the budgets e2e suite (
tests/e2e/quota_management/budgets/) against two of the Quota Management behavior contracts whose prior coverage recorded the right state but never proved the enforced behavior the contract promisesA per-user (internal user)
max_budgetis meant to be enforced across all of that user's keys, not only the key that happened to spend it down. The existing internal-user test drove a single key, so a key-scoped-only budget would have passed it.test_user_budget_across_keys_e2e.pygives one internal user a tinymax_budgetand two keys, neither carrying a budget of its own, drives the first key to abudget_exceededblock, then asserts the untouched second key is rejected the same way (nothing but the shared user budget can block it) and that the user's recorded spend has crossed the capIndividual team-member budgets are meant to be enforced independently; one member exhausting their per-team budget must not block a different member on the same team. The existing team-member test used one member, so it never proved isolation.
test_team_member_budget_isolation_e2e.pyputs two members on one team with a large team budget, caps the tight member at a tiny per-team budget and drives it past abudget_exceededblock, then asserts the roomy member still serves on the same team with its calls attributed to its own user id and the team id in the spend logs, while the tight member stays blocked after the peer spendsSupporting changes: a
/user/inforead-back helper on the budgets client so the R4 test can assert recorded user spend; two new rows intests/e2e/coverage_registry/quota_management.yaml, which is what the collector--strictmatches the@coversmarkers against; and the two new assertion tokens (isolates_per_member,enforced_across_keys) added to the naming grammar intests/e2e/CLAUDE.mdto keep the documented vocabulary in sync (the grammar itself is doc hygiene, not a--strictgate)The R4 test uses a tiny
max_budgetso a single billed call exhausts it; the load-bearing assertion is the second untouched key being blocked (a key with no budget of its own can only be blocked by the shared user budget), driven to block promptly rather than by accumulating its own spend, with the recorded user spend crossing the cap as the corroborating recorded-state checkEach test asserts both sides of the contract per
tests/e2e/CONTRIBUTING.md: the recorded state (user spend crossed the cap; roomy rows attributed to the right user and team) and the enforced gateway behavior (budget_exceededon the blocked principal, a served 200 on the isolated one)QA runbook
Bring up the stack from
tests/e2e/with a.envholding a workingOPENAI_API_KEY:docker compose up -dthencurl -fs http://localhost:4000/health/liveliness. Both tests drivegpt-5.5(real OpenAI, real spend)max_budgetblocks a second key it owns that carries no budget of its owncurl -X POST http://localhost:4000/user/new -H "Authorization: Bearer sk-1234" -d '{"max_budget": 0.000003}'curl -X POST http://localhost:4000/key/generate -H "Authorization: Bearer sk-1234" -d '{"user_id": "<uid>"}'(twice)/v1/chat/completionscalls on the first key until one returns abudget_exceededblock/v1/chat/completionscall on the untouched second key and expect it rejected withbudget_exceededtooGET /user/info?user_id=<uid>and expectuser_info.spend >= 0.000003curl -X POST http://localhost:4000/team/new -H "Authorization: Bearer sk-1234" -d '{"max_budget": 100}'/team/member_add, capping the tight member atmax_budget_in_team: 0.000003and the roomy member at100team_id+user_id), drive the tight member's key until it returnsbudget_exceeded/v1/chat/completionscalls on the roomy member's key and expect 200sbudget_exceeded; poll/spend/logsand expect the roomy calls attributed to the roomy user id and the team idFinal Attestation