Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
2e4066b
tests: add e2e tests for spend, budgets and llms
mubashir1osmani Jun 18, 2026
2c24bd6
style: make chained comparison of status_code clearer
mateo-berri Jun 18, 2026
8786e30
remove e2e_tests folder
mubashir1osmani Jun 19, 2026
f616056
test: add spend tracking tests
mubashir1osmani Jun 19, 2026
156ca9f
test: multi-window budgets coverage
mubashir1osmani Jun 19, 2026
76affd0
fix: p0 issues, added types and shared functions for each test suite
mubashir1osmani Jun 19, 2026
bf27579
chore: add config.yml
mubashir1osmani Jun 19, 2026
5782b74
test: passthrough endpoints stream/non-stream e2e
mubashir1osmani Jun 19, 2026
c727d96
style: carry clearer status_code comparison into renamed e2e dir
mubashir1osmani Jun 19, 2026
a143d74
fix: rename cost breakdown function
mubashir1osmani Jun 19, 2026
6843ca0
fix: pydantic validation for budget info, dont allow explicit type cast
mubashir1osmani Jun 19, 2026
ff7c3c5
refactor: migrate to gateway client
mubashir1osmani Jun 19, 2026
61e259c
test: add custom pricing tests
mubashir1osmani Jun 19, 2026
1033279
chore: change master key
mubashir1osmani Jun 20, 2026
dcecafa
test(e2e): address greptile review feedback
mateo-berri Jun 20, 2026
3908c4e
test(e2e): drop misleading skip comment on require_successful_call
mateo-berri Jun 20, 2026
59855b5
test(e2e): assert budget-isolation invariant before success check
mateo-berri Jun 20, 2026
0c57b54
fix(e2e): guard spend-log truncate on skip and stop returning unrelat…
cursoragent Jun 20, 2026
87d1f5b
fix(e2e): run case init() inside try so partial-init failures tear down
mateo-berri Jun 20, 2026
d80c4f4
test(e2e): mark known pricing-leak isolation test xfail(strict)
mateo-berri Jun 20, 2026
d5f0299
refactor(e2e): make suite pass its shipped strict basedpyright config
mateo-berri Jun 20, 2026
0fd91d5
fix(tests/e2e): gate spend-log truncate on e2e marker, not test direc…
cursoragent Jun 20, 2026
7951221
test(e2e): run harness unit tests without a live proxy
mateo-berri Jun 20, 2026
7761ee1
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
mateo-berri Jun 20, 2026
6a86932
test(e2e): clean up gateway config comment debris
mateo-berri Jun 20, 2026
95aec20
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
mateo-berri Jun 20, 2026
74d5d1c
fix: add new tests, split gateway
mubashir1osmani Jun 20, 2026
385b4ff
test(e2e): type the redis spend-counter probe for strict basedpyright
mateo-berri Jun 21, 2026
c7ff2cc
test(e2e): xfail the known team multi-window failure and isolate memb…
mateo-berri Jun 21, 2026
9846882
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
mateo-berri Jun 21, 2026
7ed2a95
test(e2e): set fast budget-reset cadence in gateway config so staging…
mubashir1osmani Jun 22, 2026
4b692c7
test(e2e): surface real /spend/tags errors instead of masking them as…
mubashir1osmani Jun 22, 2026
90bc446
test(e2e): drop flaky test_tag_spend_matches_sum_of_tagged_logs
mubashir1osmani Jun 24, 2026
44389ee
test(e2e): widen budget-reset wait windows to de-flake wall-clock-ali…
mubashir1osmani Jun 24, 2026
7528b02
test(e2e): let the spend-counter reseed test reach a cluster-mode TLS…
mubashir1osmani Jun 24, 2026
0822042
test(e2e): take the direct-GET spend-counter path on E2E_REDIS_CLUSTER
mubashir1osmani Jun 24, 2026
edd1fd0
test(e2e): use REDIS_HOST/REDIS_PORT and drop the unused redis knobs
mubashir1osmani Jun 24, 2026
fa2c782
test(e2e): replace the vacuous failure-row test with per-model attrib…
mubashir1osmani Jun 24, 2026
54a4246
test(e2e): assert /spend/logs returns the key's spend without 5xx
mubashir1osmani Jun 24, 2026
2353625
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
mateo-berri Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions tests/e2e/budgets/BUDGET_CODE_MATRIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Budget Code Matrix

What LiteLLM actually implements for budgets: every entity that can carry a dollar
budget, how the limit is enforced, and where in the code it happens. This is the
"what we support" reference; the companion `BUDGET_TEST_COVERAGE_MATRIX.md` maps
each row to its tests and the e2e gaps.

Over-budget surfaces as a `budget_exceeded` error (the live suite
`tests/otel_tests/test_e2e_budgeting.py` asserts `type == "budget_exceeded"`,
`code == "429"`); the underlying `BudgetExceededError` is defined in
`litellm/exceptions.py` (`status_code=400`). Enforcement runs in `common_checks()`
/ `auth_checks.py` at auth time, plus pre-call reservation in
`budget_reservation.py`.

Legend for "Enforced": **block** = request rejected; **filter** = router skips the
deployment; **alert** = notify only, request proceeds.

---

## 1. Per-entity dollar budgets

| Entity | Budget stored | Hard `max_budget` | Soft budget | Per-window | Model budget | Reset by `budget_duration` |
|--------|---------------|-------------------|-------------|------------|--------------|----------------------------|
| API key | `LiteLLM_VerificationToken` (direct cols + `budget_id` FK) | block (`_virtual_key_max_budget_check`) | alert (`_virtual_key_soft_budget_check`) + 80% alert | block (`_virtual_key_multi_budget_check`) | block (`model_max_budget_limiter.is_key_within_model_budget`) | keys reset job |
| Internal user | `LiteLLM_UserTable` (direct cols) | block (`common_checks`, only when not on a team) | - | - | via `model_max_budget` json | users reset job |
| Team | `LiteLLM_TeamTable` (direct cols) | block (`_team_max_budget_check`) | alert (`_team_soft_budget_check`) | block (`_team_multi_budget_check`) | via `model_max_budget` | teams reset job |
| Team member | `LiteLLM_TeamMembership` -> `LiteLLM_BudgetTable` | block (`_check_team_member_budget`) | - | - | - | budget-table reset job |
| End-user / customer | `LiteLLM_EndUserTable` -> `LiteLLM_BudgetTable` | block (`_check_end_user_budget`) | - | - | block (`is_end_user_within_model_budget`) | budget-table reset job |
| Organization | `LiteLLM_OrganizationTable` -> `LiteLLM_BudgetTable` | block (`_organization_max_budget_check`) | - | - | via budget-table | budget-table reset job |
| Tag | `LiteLLM_TagTable` -> `LiteLLM_BudgetTable` | block (`_tag_max_budget_check`) | - | - | via budget-table | budget-table reset job |
| Project | `LiteLLM_ProjectTable` -> `LiteLLM_BudgetTable` | block (`_project_max_budget_check`) | alert (`_project_soft_budget_check`) | - | - | budget-table reset job |
| Provider (router) | config `provider_budget_config` (in-memory) | filter (`router_strategy/budget_limiter`) | - | yes (time window) | - | window TTL |
| Global proxy | `litellm.max_budget` (config) | block (`_global_proxy_budget_check`) | - | - | - | - |

Notes / flags from the code:
- **User budget only enforced off-team**: `common_checks` skips the personal-user
budget when the key belongs to a team (team budget governs instead).
- **Comparison operators are inconsistent**: key/user use `>=`, team/end-user main
budget use `>`. Spend exactly at `max_budget` blocks a key but not a team.
- **Provider budgets are filter-only**: an over-budget provider is removed from
routing; if all are over budget the router raises
`no_deployments_with_provider_budget_routing` (not a per-entity block).
- **Enforcement timing differs by entity**: key / user / org / team-member / tag /
model enforce off real-time reservation counters (block within ~2 calls);
**end-user** enforcement reads `EndUserTable.spend`, which only updates on the
`proxy_batch_write_at` flush, so it lags by that interval (verified live).

## 2. Budget mechanisms

| Mechanism | What it does | Code |
|-----------|--------------|------|
| Pre-call reservation | Estimates max request cost, atomically reserves against redis spend counters for key/team/user/end_user/tag/team_member/org before the call; blocks if a counter would exceed | `spend_tracking/budget_reservation.py` |
| Post-call reconciliation | Adjusts the reservation to the actual cost once known | `reconcile_budget_reservation` |
| Read-time enforcement | Auth-time check of current spend vs `max_budget` | `auth_checks.common_checks` + per-entity `_*_max_budget_check` |
| Soft budget / alerts | At `soft_budget` (or 80% of max) fire Slack/email alert, do not block | `_virtual_key_soft_budget_check`, `_team_soft_budget_check`, `budget_alerts` |
| Multi-window budgets | `budget_limits` list of `{budget_duration, max_budget}`; each window enforced + reset independently | `_virtual_key_multi_budget_check`, `reset_budget_windows` |
| Model-level budgets | `model_max_budget` dict (per model: `budget_limit` + `time_period`) on key/user/end_user | `hooks/model_max_budget_limiter.py` |
| Reset by duration | Job zeros `spend`, recomputes `budget_reset_at = now + duration_in_seconds(budget_duration)`, invalidates redis counters | `common_utils/reset_budget_job.py`, `duration_parser.duration_in_seconds` |
| Zero-cost bypass | Models with no configured price bypass budget reservation | `budget_reservation` zero-cost path |

## 3. Budget management surface (endpoints)

| Action | Endpoint | Handler |
|--------|----------|---------|
| Create budget | `POST /budget/new` | `new_budget` |
| Update budget | `POST /budget/update` | `update_budget` |
| Budget info | `POST /budget/info` (`{"budgets": [id]}`) | `info_budget` |
| Budget settings | `GET /budget/settings` | `budget_settings` |
| List budgets | `GET /budget/list` | `list_budget` |
| Delete budget | `POST /budget/delete` (`{"id": id}`) | `delete_budget` |
| Set on key | `POST /key/generate`, `/key/update` (`max_budget`, `soft_budget`, `budget_duration`, `model_max_budget`, `budget_id`) | key mgmt |
| Set on user | `POST /user/new` (`max_budget`, `budget_duration`) | internal user |
| Set on team | `POST /team/new` (`max_budget`, `soft_budget`, `team_member_budget`) | team |
| Set on team member | `POST /team/member_add` (`max_budget_in_team`) | team |
| Set on org | `POST /organization/new` (`max_budget`, `soft_budget`, `model_max_budget`) | org |
| Set on customer | `POST /customer/new`, `/customer/update` (`max_budget`, `budget_id`) | customer |
| Set on tag | `POST /tag/new`, `/tag/update` (`max_budget`) | tag mgmt |
| Read budget+spend | `/key/info`, `/user/info`, `/team/info`, `/organization/info`, `/customer/info`, `/budget/info` | per-entity info |

Endpoint method/shape gotchas verified live: `/organization/delete` is **DELETE**
with `{"organization_ids": [id]}`; `/budget/info` takes `{"budgets": [id]}`;
`model_max_budget` entries use `{"budget_limit", "time_period"}`.

## 4. Config knobs

| Setting | Effect |
|---------|--------|
| `litellm.max_budget` | proxy-wide hard cap (global proxy budget) |
| `max_internal_user_budget` / `default_max_internal_user_budget` | default `max_budget` for internal users |
| `internal_user_budget_duration` | default reset duration for internal users |
| `max_end_user_budget` / `max_end_user_budget_id` | default budget for end-users |
| `default_team_params` | default `max_budget` / `budget_duration` / limits for teams |
| `provider_budget_config` (router) | per-provider spend caps + windows |
78 changes: 78 additions & 0 deletions tests/e2e/budgets/BUDGET_TEST_COVERAGE_MATRIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Budget Test Coverage Matrix

Maps every row of `BUDGET_CODE_MATRIX.md` (what LiteLLM implements) to its tests
and level, then marks the live e2e coverage this suite adds.

Levels: `unit` mocked (`AsyncMock` on `get_current_spend`/prisma); `router` live
router with fake deployments; `live-e2e` real proxy, real key/team, real requests
until blocked. Status: `covered` / `partial` / `gap`.

Pre-existing live coverage outside this suite:
- `tests/otel_tests/test_e2e_budgeting.py` - key + team enforcement, budget update.
- `tests/local_testing/test_router_budget_limiter.py` - provider / tag / deployment
budgets at the router.

This suite (`tests/e2e/budgets/`) adds the missing live coverage and runs
on the shared lifecycle (every entity it creates is deleted on teardown).

---

## Per-entity enforcement

| Entity | Unit | Pre-existing live | This suite (live) | Status |
|--------|------|-------------------|-------------------|--------|
| API key | `test_budget_reservation.py`, `test_max_budget_limiter.py` | `otel_tests` | `test_budget_enforcement_e2e::test_key_budget_blocks` | **covered** |
| Team | `test_team_budget_limits.py` | `otel_tests` | (org test builds a team) | **covered** |
| Internal user | auth unit tests | - | `test_internal_user_budget_blocks` | **covered (new)** |
| Team member | `test_team_member_budget.py` | - | `test_team_member_budget_blocks` | **covered (new)** |
| End-user / customer | `test_custom_auth_end_user_budget.py` | - | `test_end_user_budget_blocks` | **covered (new)** |
| Organization | `test_organization_budget_enforcement.py` (flagged weak) | - | `test_organization_budget_blocks` | **covered (new)** |
| Tag (proxy-level) | - | router only | `test_tag_budget_e2e::test_tag_budget_blocks_tagged_requests` | **covered (new)** |
| Model-level (`model_max_budget`) | `test_unit_test_max_model_budget_limiter.py` | - | `test_model_max_budget_e2e::test_model_max_budget_isolates_per_model` | **covered (new)** |
| Provider (router) | `test_budget_limiter_hotpath.py` | `test_router_budget_limiter.py` | - | **covered** (router) |
| Global proxy (`litellm.max_budget`) | unit | - | - | **gap** (needs a config-level cap; not key-settable) |

## Budget mechanisms

| Mechanism | Unit | This suite (live) | Status |
|-----------|------|-------------------|--------|
| Pre-call reservation | `test_budget_reservation.py` | exercised by every enforcement test | **partial** |
| Soft budget / alerts | `SlackAlerting/test_budget_alert_types.py` | `test_soft_budget_e2e::test_soft_budget_does_not_block` | **covered (new)** (block-vs-alert; the alert side-effect itself stays unit) |
| Budget CRUD | `test_budget_endpoints.py` | `test_budget_crud_e2e` (roundtrip + delete) | **covered (new)** |
| Reset scheduling | `test_proxy_budget_reset.py` | `test_budget_crud_e2e::test_budget_duration_schedules_reset_on_key` | **covered (new)** (scheduling; actual zeroing is time-dependent -> unit) |
| Multi-window budgets | `test_multi_budget_windows.py` | - | **gap** (window setup is fiddly; left to unit for now) |
| Read budget+spend | `test_spend_management_endpoints.py` | `/key/info` asserted in CRUD + enforcement | **partial** |

## Remaining gaps (intentionally not live-tested)

- **Global proxy budget** (`litellm.max_budget`): set via proxy config, not a
per-key API, so it needs a dedicated proxy boot with that config rather than a
runtime-created entity. Out of scope for the per-entity suite.
- **Multi-window budgets**: the `budget_limits` list shape and per-window reset are
covered by `test_multi_budget_windows.py` (unit); a live version would need to
wait out a short window to see the reset, which is time-dependent.
- **Soft-budget alert delivery**: whether the Slack/email actually fires is not
observable from the proxy API; unit tests own that. The live test pins the
load-bearing behavior (soft does not block).
- **Reset zeroing after the window elapses**: time-dependent; unit tests own the
reset-job logic. The live test pins that `budget_reset_at` is scheduled.

## This suite's files

| File | Covers |
|------|--------|
| `test_budget_enforcement_e2e.py` | key / internal-user / end-user / organization / team-member hard enforcement |
| `test_model_max_budget_e2e.py` | per-model caps isolate by model |
| `test_soft_budget_e2e.py` | soft budget alerts but does not block |
| `test_tag_budget_e2e.py` | proxy-level tag budget blocks tagged requests, spares others |
| `test_budget_crud_e2e.py` | `/budget/*` CRUD roundtrip + delete + `budget_reset_at` scheduling |

## Pattern + timing

Create the entity with a tiny `max_budget`, drive spend until a `budget_exceeded`
block. The enforcement helper is two-phase: a fast warmup (key/user/org/member/tag/
model block within ~2 calls off real-time counters), then a poll across the ~60s
batch-write window (end-user enforcement reads table spend that lags). Skip on a
non-budget error (provider down / key missing); fail if the budget is never
enforced. Chat tests use `gpt-5.5` (the model with a working key on the reference
proxy); swap the literal if your proxy differs.
Loading
Loading