[Fix] Align user and org budget spend checks with atomic counter pattern - #26182
Conversation
Brings user personal budget and organization budget enforcement in line with the existing key and team patterns, which already read spend from the atomic cross-pod Redis counter.
Greptile SummaryThis PR extends the atomic Redis counter budget-enforcement pattern (already used for API keys and teams) to user personal budgets and organization budgets. It also activates The previously flagged P1 concern — Confidence Score: 5/5Safe to merge — no P0/P1 issues remain; the prior P1 (team-key exemption in MaxBudgetLimiter) has been addressed. All critical and major issues are resolved. The only remaining comment is a P2 clarification note about counter semantics (personal vs. total spend) that matches prior DB behaviour and is not a regression. No files require special attention;
|
| Filename | Overview |
|---|---|
| litellm/proxy/proxy_server.py | Adds org_id param to increment_spend_counters and two new counter increments (spend:user:{user_id}, spend:org:{org_id}). Logic mirrors existing key/team patterns; counter placement is after request so auth-time read uses the previous request's value — consistent with the rest of the counters. |
| litellm/proxy/auth/auth_checks.py | User personal-budget check now reads from spend:user:{user_id} Redis counter (Redis-first with DB fallback) and uses >= comparison, consistent with key/team enforcement. Org budget check similarly updated to read from spend:org:{org_id} counter. |
| litellm/proxy/hooks/max_budget_limiter.py | Replaces stale cache-key lookup (effectively always a no-op) with direct user_max_budget/user_id read from user_api_key_dict plus a Redis counter lookup. Adds the team-key exemption (team_id is not None → return) to match common_checks section 4.1, resolving the prior P1 concern. |
| litellm/proxy/hooks/proxy_track_cost_callback.py | Single-line addition to thread org_id through to increment_spend_counters. Change is minimal and correct. |
Sequence Diagram
sequenceDiagram
participant Client
participant Auth as auth_checks
participant Redis as Redis spend counters
participant Hook as MaxBudgetLimiter
participant CB as CostCallback
participant DB as Database
Client->>Auth: API request
Auth->>Redis: get_current_spend(spend:user:ID)
Redis-->>Auth: user spend or DB fallback
Auth->>Auth: over personal budget? raise BudgetExceededError
Auth->>Redis: get_current_spend(spend:org:ID)
Redis-->>Auth: org spend or DB fallback
Auth->>Auth: over org budget? raise BudgetExceededError
Auth-->>Hook: async_pre_call_hook
Hook->>Hook: team key? skip check and return
Hook->>Redis: get_current_spend(spend:user:ID)
Redis-->>Hook: user spend
Hook->>Hook: over budget? raise HTTP 429
Hook-->>Client: proceed
Client->>CB: success callback
CB->>Redis: increment spend:key:TOKEN
CB->>Redis: increment spend:team:TEAM
CB->>Redis: increment spend:user:USER
CB->>Redis: increment spend:org:ORG
CB->>DB: update_database (async)
Reviews (2): Last reviewed commit: "fix: skip personal budget check in MaxBu..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…unter_alignment [Fix] Align user and org budget spend checks with atomic counter pattern
Summary
common_checksnow read spend from thespend:user:{user_id}Redis counter viaget_current_spend, matching the existing pattern used for key and team budget checks. The comparison was also corrected from<to>=to be consistent with how key budget enforcement works._organization_max_budget_checknow read spend from aspend:org:{org_id}Redis counter instead of the DB-loadedorg_table.spendfield.increment_spend_countersgains anorg_idparameter and atomically increments two new counters (spend:user:{user_id},spend:org:{org_id}) after each request, keeping them in sync with the existing key/team counters._PROXY_MaxBudgetLimiterpre-call hook is updated to readuser_max_budgetanduser_iddirectly fromuser_api_key_dict(set at auth time) and look up current spend from thespend:user:{user_id}counter, replacing a stale cache lookup path that was effectively inoperative.Testing
uv run pytest tests/test_litellm/proxy/auth/test_auth_checks.py tests/test_litellm/proxy/auth/test_organization_budget_enforcement.py— 78 passeduv run pytest tests/test_litellm/proxy/auth/test_multi_budget_windows.py tests/test_litellm/proxy/auth/test_team_member_budget.py— 10 passedType
🐛 Bug Fix
✅ Test