# 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 |
