Skip to content

Fix bugs that bypasses per-team member budget limit - #26204

Merged
Michael-RZ-Berri merged 2 commits into
litellm_internal_stagingfrom
litellm_budgetLimitFix
Apr 23, 2026
Merged

Fix bugs that bypasses per-team member budget limit#26204
Michael-RZ-Berri merged 2 commits into
litellm_internal_stagingfrom
litellm_budgetLimitFix

Conversation

@Michael-RZ-Berri

@Michael-RZ-Berri Michael-RZ-Berri commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Should address LIT-2466 and LIT-2467, both having to do with users exceeding budget limits. Three bugs in particular:

  • The Redis TTL (time to live) for the budget counts was set to a minute, if the value was not accessed within that time frame, it would get deleted and the user's budget usage would be reset when it should not. This is increased to five minutes (now stays the same), and reseeding the value after expiry now reads straight from the DB (it was previously getting it from a stale cache that didn't get updated on increment).
  • There is a code path that finds the user's budget limit, but in a case where the team limit is set after the users are added or vice versa, the path will find null if they don't have an individual limit and not enforce a limit instead of finding the team limit. This path is fixed to get the correct amount in either case.
  • The bug that led to that code path was that if the team budget limit was set, the backfill only made new rows that included the updated budget, but it didn't update missing budget values for already existing members. Which led to a null value, which led to the above. This updates the backfill to also update those nulls.

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Screenshots / Proof of Fix

The tests added (enforcing budget limits) fail before the fix and succeed after the fix. Also, here are some screenshots before and after:

Type

🐛 Bug Fix
✅ Test

Changes

Fixes to proxy auth_checks and proxy_server, with respective tests added.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


Michael Riad Zaky seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three related bugs that allowed users to exceed per-team-member budget limits: Redis counters were reseeding from a stale cache instead of the DB after TTL expiry, the auth check skipped enforcement when a member had no individual budget row (instead of falling back to the team-level default), and the backfill function wasn't healing pre-existing membership rows with a null budget_id. All three fixes are targeted and include dedicated mock-only tests that fail before the fix and pass after.

Confidence Score: 5/5

Safe to merge; all three bugs are correctly fixed with targeted changes and passing mock tests, and no new P0/P1 issues were found.

The three bug fixes are logically sound and well-isolated. The remaining open item (direct DB query in get_team_member_default_budget on the auth path) is a P2 rule concern already captured in the previous review round and is mitigated by result caching. All new tests are mock-only, non-network, and cover both the happy path and boundary conditions. Prior P1 concerns from earlier review threads are either addressed (user/org counter reseed, window-key disambiguation) or acknowledged as out-of-scope by a senior developer.

No files require special attention beyond the previously flagged direct DB query in litellm/proxy/auth/auth_checks.py.

Important Files Changed

Filename Overview
litellm/proxy/auth/auth_checks.py Adds get_team_member_default_budget (direct DB query, flagged in prior review) and correctly rewires _check_team_member_budget to fall back to the team-level default when a member has no linked budget table.
litellm/proxy/management_endpoints/team_endpoints.py Extends backfill_team_member_budget_entries with an unconditional update_many to heal pre-existing membership rows whose budget_id is NULL; new rows from create_many already carry the correct budget_id so no double-update occurs.
litellm/proxy/proxy_server.py Introduces _reseed_spend_from_db to read authoritative spend from the DB on counter-cache miss (handling key/team/team_member/user/org prefixes and short-circuiting on window-suffix keys) and wires it into _init_and_increment_spend_counter; outstanding P2 concerns from prior threads acknowledged as in-scope or out-of-scope.
tests/test_litellm/proxy/auth/test_auth_checks.py Adds two new async mock-only tests covering the team-default budget fallback path and the per-member override priority; no real network calls, good assertion coverage including cache-hit bypass of DB.
tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py Adds update_many mocks to existing tests (necessary for new code path) and a new dedicated test asserting the null-budget_id healing behavior; does not weaken prior assertions.
tests/test_litellm/proxy/test_proxy_server.py Three new mock-only tests cover DB-reseed on counter-cache miss, user/org prefix handling, and window-key short-circuit; correctly isolates module-level globals and restores them in finally blocks.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Request arrives] --> B[_check_team_member_budget]
    B --> C[get_team_membership]
    C --> D{membership.litellm_budget_table not None?}
    D -- Yes --> E[team_member_budget = membership.budget_table.max_budget]
    D -- No --> F{team.metadata has team_member_budget_id?}
    F -- No --> G[No budget limit enforced]
    F -- Yes --> H[get_team_member_default_budget cache-first DB lookup]
    H --> I[team_member_budget = default_budget.max_budget]
    E --> J[get_current_spend Redis counter first]
    I --> J
    J --> K{spend >= budget?}
    K -- Yes --> L[raise BudgetExceededError]
    K -- No --> M[Request proceeds]
Loading

Reviews (5): Last reviewed commit: "fix linting" | Re-trigger Greptile

Comment on lines +1888 to +1931
async def _reseed_spend_from_db(counter_key: str) -> float:
"""
Read the authoritative spend for a missing counter from the DB. The
counter_key prefix encodes the table to query:

spend:key:{token} -> LiteLLM_VerificationToken.spend
spend:team:{team_id} -> LiteLLM_TeamTable.spend
spend:team_member:{uid}:{tid} -> LiteLLM_TeamMembership.spend

Returns 0.0 if prisma is unavailable, the row is missing, or the
key format is unrecognized. On failure, logs and returns 0.0 rather
than raising so the caller can still record the current increment.
"""
if prisma_client is None:
return 0.0
try:
if counter_key.startswith("spend:key:"):
token = counter_key[len("spend:key:") :]
row = await prisma_client.db.litellm_verificationtoken.find_unique(
where={"token": token}
)
elif counter_key.startswith("spend:team_member:"):
suffix = counter_key[len("spend:team_member:") :]
if ":" not in suffix:
return 0.0
user_id, team_id = suffix.rsplit(":", 1)
row = await prisma_client.db.litellm_teammembership.find_unique(
where={"user_id_team_id": {"user_id": user_id, "team_id": team_id}}
)
elif counter_key.startswith("spend:team:"):
team_id = counter_key[len("spend:team:") :]
row = await prisma_client.db.litellm_teamtable.find_unique(
where={"team_id": team_id}
)
else:
return 0.0
except Exception:
verbose_proxy_logger.exception(
"Failed to reseed spend counter %s from DB", counter_key
)
return 0.0
if row is None:
return 0.0
return float(getattr(row, "spend", 0.0) or 0.0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Window-budget counters not reseeded from DB on TTL expiry

_reseed_spend_from_db handles the three base spend keys but the per-window counter variants (incremented directly in increment_spend_counters for budget_limits windows) have no DB reseed path. After the 5-minute TTL expires those counters reset to 0. A user inactive for more than 5 minutes during a budget window will have their window counter silently wiped, letting them spend up to their window cap again within the same period.

A follow-up could either extend _reseed_spend_from_db to handle window-suffix keys, or set each window counter's TTL to match its actual window duration rather than the shared SPEND_COUNTER_REDIS_TTL_SECONDS.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes the window budget can get exceeded once on each expiry (if the primary budget is not exceeded), but this was pre-existing and is currently out of scope / will go in another PR.

Comment thread litellm/proxy/proxy_server.py
@codecov

codecov Bot commented Apr 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
litellm/proxy/auth/auth_checks.py 75.00% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

Comment thread litellm/proxy/proxy_server.py
@Michael-RZ-Berri
Michael-RZ-Berri temporarily deployed to integration-postgres April 23, 2026 19:08 — with GitHub Actions Inactive
@veria-ai

veria-ai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Low: Budget enforcement fix with no new security concerns

This PR fixes a bug where per-team member budget limits were silently skipped when the membership row lacked a direct budget link. It adds a fallback path to read the team-level default budget from team.metadata["team_member_budget_id"], improves spend counter reseeding from the DB instead of stale cache, and backfills existing membership rows with NULL budget_id.

All new DB queries use Prisma ORM with parameterized access. Counter keys are constructed from server-side auth variables, not user input. The backfill update_many runs within admin-authenticated team update endpoints. No new attack surface introduced.


Status: 0 open
Risk: 1/10

Posted by Veria AI · 2026-04-23T19:10:55.100Z


cache_key = f"team_member_default_budget:{budget_id}"

cached_budget = await user_api_key_cache.async_get_cache(key=cache_key)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be stale? I see you are caching this, but I do not see any cache invalidation on update of the budget

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but it's at most stale for a minute and there are six other stale caches in this file that are similar

@Michael-RZ-Berri
Michael-RZ-Berri merged commit c81342e into litellm_internal_staging Apr 23, 2026
104 checks passed
@Michael-RZ-Berri
Michael-RZ-Berri deleted the litellm_budgetLimitFix branch April 23, 2026 21:59
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
Fix bugs that bypasses per-team member budget limit
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.

3 participants