Skip to content

test(e2e): budget refusals are 429 for bare keys and team caps block every team key - #33632

Merged
ryan-crabbe-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_e2e_bare_key_budget_429
Jul 17, 2026
Merged

test(e2e): budget refusals are 429 for bare keys and team caps block every team key#33632
ryan-crabbe-berri merged 4 commits into
litellm_internal_stagingfrom
litellm_e2e_bare_key_budget_429

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

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

Both proofs run against a live proxy (the docker compose stack in tests/e2e) with real paid llama-3.3-70b-versatile calls through the claude-haiku-4-5 model group.

Bare-key case, captured at 2962170. Mint a key with a tiny max_budget and no team_id or user_id, then drive /v1/chat/completions until refused. Call 1 succeeds, call 2 onward is a 429 budget_exceeded, never a 500:

$ curl -s -X POST http://localhost:4000/key/generate -H 'Authorization: Bearer sk-1234' \
    -H 'Content-Type: application/json' -d '{"max_budget": 0.000003}'
--- call 1 ---
... "usage":{"completion_tokens":16,"prompt_tokens":38,"total_tokens":54, ...
HTTP 200
--- call 2 ---
{"error":{"message":"Budget has been exceeded! Key=key (sk-...-TLw) Current cost: 3.506e-05, Max budget: 3e-06","type":"budget_exceeded","param":null,"code":"429"}}
HTTP 429

Team case, captured at 3c74580. A team with max_budget 3e-06 and two keys under it, neither with a key-level budget. Key A's first call succeeds and burns through the team cap; key A's second call and key B's very first call are both refused with the same team 429 budget_exceeded:

--- key A call 1 ---
... "usage":{"completion_tokens":16,"prompt_tokens":39,"total_tokens":55, ...
HTTP 200
--- key A call 2 ---
{"error":{"message":"Budget has been exceeded! Team=0fddb8ec-5165-46ed-abfa-adf62637f889 Current cost: 3.565e-05, Max budget: 3e-06","type":"budget_exceeded","param":null,"code":"429"}}
HTTP 429
--- key B first call ---
{"error":{"message":"Budget has been exceeded! Team=0fddb8ec-5165-46ed-abfa-adf62637f889 Current cost: 3.565e-05, Max budget: 3e-06","type":"budget_exceeded","param":null,"code":"429"}}
HTTP 429

All 6 enforcement cases pass against the same live proxy at d097d98 (6 passed in 15.33s), python -m coverage_registry.collector --strict accepts the new registry row, and basedpyright tests/e2e reports 0 errors

Type

✅ Test

Changes

Two strengthenings of tests/e2e/quota_management/budgets/test_budget_enforcement_e2e.py, both about the blocking behavior a customer sees when a budget runs out.

First, the bare-key case (quota_management.budget.key.blocks_over_limit): the existing KeyBudgetCase proved a block happens but never checked its shape. It now asserts the refusal is HTTP 429 with error type budget_exceeded; anything else, including a 500, fails the test. To support that, _assert_budget_blocks returns the blocked response instead of discarding it, so any entity case can assert on the refusal's shape; the sibling cases are behaviorally unchanged.

Second, a new TeamBudgetCase covering the previously missing plain team max_budget (new registry row quota_management.budget.team.blocks_over_limit, plus team added to the variant vocab in tests/e2e/CLAUDE.md). Prior coverage existed only in tests/otel_tests/test_e2e_budgeting.py with a single key against a mock model; nothing anywhere asserted the team-wide property. The new case creates a team with a tiny budget and two keys with no key-level budgets, drives key A until the team cap blocks it, then asserts key B's very first call is also refused; both refusals must be 429 budget_exceeded. The cap provably sits on the team, not on the key that spent. The case is deliberately focused on blocking behavior; spend-ledger read-backs belong to the spend_tracking rows, not here

QA runbook

  • tests/e2e/quota_management/budgets/test_budget_enforcement_e2e.py::test_budget_enforcement[KeyBudgetCase] - a bare key (no team_id, no user_id) carrying its own tiny max_budget is refused with a 429 budget_exceeded once spend crosses the cap (needs ANTHROPIC_API_KEY for the claude-haiku-4-5 model in the compose config)

    • Generate a bare budgeted key: curl -X POST http://localhost:4000/key/generate -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"max_budget": 0.000003}' (note there is no team_id or user_id in the body)
    • Send /v1/chat/completions requests with that key ({"model": "claude-haiku-4-5", "messages": [{"role": "user", "content": "spend"}], "max_tokens": 16}) a couple of seconds apart until one is refused
    • Expect the refusal to be HTTP 429 with body type "budget_exceeded"; any other status or error type (especially a 500) is a failure
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky
  • tests/e2e/quota_management/budgets/test_budget_enforcement_e2e.py::test_budget_enforcement[TeamBudgetCase] - a team-wide max_budget blocks every key on the team once one member burns through it (needs ANTHROPIC_API_KEY for the claude-haiku-4-5 model in the compose config)

    • Create a capped team: curl -X POST http://localhost:4000/team/new -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"team_alias": "qa-team-budget", "max_budget": 0.000003}'
    • Generate two keys on it, neither with a key budget: curl -X POST http://localhost:4000/key/generate -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json" -d '{"team_id": "<team_id>"}' (twice)
    • Send /v1/chat/completions requests with key A a couple of seconds apart until one is refused; expect HTTP 429 with type "budget_exceeded" and a message saying "Budget has been exceeded! Team=<team_id>"
    • Send one /v1/chat/completions request with key B, which has spent nothing itself, and expect the same 429 budget_exceeded on its very first call
    • Sanity check: this test makes sense to add and is not hand-wavey (e.g., assert actual expected spend instead of just spend > 0) or potentially flaky

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

@greptile-apps

greptile-apps Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds two e2e test improvements for budget enforcement: it strengthens the existing KeyBudgetCase to assert that budget refusals are HTTP 429 (not just any non-2xx), and introduces a new TeamBudgetCase proving that a team-wide max_budget blocks every member key — including one that never spent anything itself — with a 429 budget_exceeded response.

  • _assert_budget_blocks is refactored to return the blocked StreamingResponse so callers can inspect its shape; existing callers that ignored the return value are unaffected.
  • KeyBudgetCase.run() is added to override the base no-op, asserting status_code == 429 on the refusal.
  • TeamBudgetCase creates a capped team with two keyless-budget keys, drives key A to exhaustion, then asserts key B's very first call is also refused with is_budget_block(sibling) and sibling.status_code == 429, proving the cap is on the team and not the spending key.

Confidence Score: 5/5

Safe to merge — only test files are changed, no production code is touched.

The change is entirely confined to the e2e test suite and its coverage registry. Both refusals in TeamBudgetCase now check status_code == 429 explicitly, the sibling assertion combines is_budget_block with the status-code guard, and the modification to _assert_budget_blocks is additive. No existing assertions are weakened.

No files require special attention.

Important Files Changed

Filename Overview
tests/e2e/quota_management/budgets/test_budget_enforcement_e2e.py Strengthens KeyBudgetCase to assert 429 on refusal; adds TeamBudgetCase proving team-wide cap blocks all member keys with 429 budget_exceeded. Both the primary key and the sibling key have full status-code assertions.
tests/e2e/coverage_registry/quota_management.yaml New P0 registry row for quota_management.budget.team.blocks_over_limit inserted after the key row; rationale and source are accurate.
tests/e2e/CLAUDE.md Grammar doc updated to include team in the budget variant vocab; consistent with the new registry row and test.

Reviews (4): Last reviewed commit: "test(e2e): focus the team budget case on..." | Re-trigger Greptile

Comment thread tests/e2e/quota_management/budgets/test_budget_enforcement_e2e.py Outdated
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ryan-crabbe-berri ryan-crabbe-berri changed the title test(e2e): assert bare-key budget refusal is 429 and /key/info spend reaches the cap test(e2e): assert bare-key budget refusal is 429 budget_exceeded Jul 17, 2026
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

@ryan-crabbe-berri ryan-crabbe-berri changed the title test(e2e): assert bare-key budget refusal is 429 budget_exceeded test(e2e): budget refusals are 429 for bare keys and team caps block every team key Jul 17, 2026
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

Comment thread tests/e2e/quota_management/budgets/test_budget_enforcement_e2e.py
@codspeed-hq

codspeed-hq Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_e2e_bare_key_budget_429 (d097d98) with litellm_internal_staging (ecef9e6)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (5daed34) during the generation of this report, so ecef9e6 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai re review

@ryan-crabbe-berri
ryan-crabbe-berri merged commit e5a9f3f into litellm_internal_staging Jul 17, 2026
135 of 136 checks passed
@ryan-crabbe-berri
ryan-crabbe-berri deleted the litellm_e2e_bare_key_budget_429 branch July 17, 2026 18:29
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