litellm - ✅(Solved) Fix [Bug]: Team key spend incorrectly increments user personal spend, causing false BudgetExceededError on personal key calls [1 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#26239Fetched 2026-04-23 07:24:26
View on GitHub
Comments
1
Participants
1
Timeline
5
Reactions
0
Participants
Timeline (top)
subscribed ×2commented ×1cross-referenced ×1labeled ×1

Error Message

BudgetExceededError: ExceededBudget: LiteLLM Proxy: User=<user_id> over budget. Current cost: 411.92, Max budget: 200.0, Reset At: 2026-05-01 00:00:00+00:00

Root Cause

_batch_database_updates() in db_spend_update_writer.py (line 333) calls _update_user_db() unconditionally — it does not pass team_id, and _update_user_db() has no guard to skip the update when the request came from a team key.

Compare _update_team_db() (line 560–565), which correctly short-circuits when team_id is None:

# _update_team_db (correctly guarded)
async def _update_team_db(self, ..., team_id, ...):
    if team_id is None or prisma_client is None:
        return  # skip — not a team key call
# _update_user_db (no guard — the bug)
async def _update_user_db(self, ...,  # no team_id parameter at all
):
    # unconditionally enqueues spend increment for the user
    await self.spend_update_queue.add_update(
        update=SpendUpdateQueueItem(
            entity_type=Litellm_EntityType.USER,
            entity_id=_id,
            response_cost=response_cost,
        )
    )

The budget check in auth_checks.py (line 623–633) is correctly guarded:

# Correctly skips personal budget check for team key calls
if (
    (team_object is None or team_object.team_id is None)  # ← correct guard
    and user_object is not None
    and user_object.max_budget is not None
):
    user_spend = await get_current_spend(
        counter_key=f"spend:user:{user_object.user_id}",
        fallback_spend=user_object.spend or 0.0,  # ← reads polluted DB field
    )
    if user_spend >= user_budget:
        raise litellm.BudgetExceededError(...)

The mismatch between the spend write path (no guard) and the spend check path (correctly guarded) is what causes the bug. The fallback_spend=user_object.spend line reads from the DB field that has been inflated by team key costs, so any time the Redis counter is absent (e.g. after a proxy restart), the inflated DB value is used and triggers a false positive.

Fix Action

Fixed

PR fix notes

PR #26297: fix: skip personal spend update for team key calls

Description (problem / solution / changelog)

Fixes #26239

Problem

_update_user_db() was unconditionally incrementing LiteLLM_UserTable.spend on every API call, including calls made with team keys. This caused the user's personal spend row to accumulate all team key costs.

When the user later made a call with their personal key and the Redis counter was absent (e.g., after a proxy restart), the budget check fell back to the polluted DB value and raised BudgetExceededError — even though their actual personal spend was within budget.

Solution

Add a team_id guard to _update_user_db() that returns early when team_id is set, matching the pattern already used by _update_team_db():

# _update_team_db — already correctly guarded
if team_id is None or prisma_client is None:
    return

# _update_user_db — new guard (this fix)
if team_id is not None:
    return

Also passes team_id through from _batch_database_updates() to _update_user_db().

The budget check in auth_checks.py already correctly skips personal budget enforcement for team key calls. This fix aligns the spend write path with that existing behavior.

Testing

Added two unit tests in tests/test_litellm/proxy/db/test_db_spend_update_writer.py:

  • test_update_user_db_skips_for_team_key_calls — verifies no spend update is enqueued when team_id is set
  • test_update_user_db_runs_for_personal_key_calls — verifies spend update IS enqueued when team_id is None

Changed files

  • litellm/llms/zai/chat/transformation.py (modified, +43/-1)
  • litellm/proxy/db/db_spend_update_writer.py (modified, +9/-0)
  • tests/test_litellm/llms/zai/test_zai_provider.py (modified, +114/-0)
  • tests/test_litellm/proxy/db/test_db_spend_update_writer.py (modified, +67/-0)

Code Example

# _update_team_db (correctly guarded)
async def _update_team_db(self, ..., team_id, ...):
    if team_id is None or prisma_client is None:
        return  # skip — not a team key call

---

# _update_user_db (no guard — the bug)
async def _update_user_db(self, ...,  # no team_id parameter at all
):
    # unconditionally enqueues spend increment for the user
    await self.spend_update_queue.add_update(
        update=SpendUpdateQueueItem(
            entity_type=Litellm_EntityType.USER,
            entity_id=_id,
            response_cost=response_cost,
        )
    )

---

# Correctly skips personal budget check for team key calls
if (
    (team_object is None or team_object.team_id is None)  # ← correct guard
    and user_object is not None
    and user_object.max_budget is not None
):
    user_spend = await get_current_spend(
        counter_key=f"spend:user:{user_object.user_id}",
        fallback_spend=user_object.spend or 0.0,  # ← reads polluted DB field
    )
    if user_spend >= user_budget:
        raise litellm.BudgetExceededError(...)

---

async def _update_user_db(
    self,
    response_cost: Optional[float],
    user_id: Optional[str],
    prisma_client: Optional[PrismaClient],
    user_api_key_cache: DualCache,
    litellm_proxy_budget_name: Optional[str],
    end_user_id: Optional[str] = None,
    team_id: Optional[str] = None,    # new parameter
):
    if team_id is not None:
        return  # team key call — spend is tracked against the team, not the user

---

await self._update_user_db(
    response_cost=response_cost,
    user_id=user_id,
    prisma_client=prisma_client,
    user_api_key_cache=user_api_key_cache,
    litellm_proxy_budget_name=litellm_proxy_budget_name,
    end_user_id=end_user_id,
    team_id=team_id,    # pass through
)

---

BudgetExceededError: ExceededBudget: LiteLLM Proxy: User=<user_id> over budget.
  Current cost: 411.92, Max budget: 200.0, Reset At: 2026-05-01 00:00:00+00:00
RAW_BUFFERClick to expand / collapse

Check for existing issues

  • I have searched the existing issues

What happened?

When a user makes API calls using a team key (team_id is set), the spend is correctly attributed to the team (LiteLLM_TeamTable.spend). However, the same cost is also written to the user's personal spend record (LiteLLM_UserTable.spend). This means the user's personal spend DB field accumulates all team key spend in addition to personal key spend.

When the user later makes a call with their personal key (team_id = None), the personal budget check in auth_checks.py reads user_object.spend (the polluted DB field) as the fallback_spend, sees total spend ≥ personal budget, and raises BudgetExceededError — even though the user's actual personal key spend is within budget.

Steps to Reproduce

  1. Create a user with a personal budget (e.g. max_budget = $200)
  2. Add the user to a team; the user makes calls with the team key (accumulates, say, $400 of team spend)
  3. The user makes a call with their personal key
  4. BudgetExceededError is raised: "Current cost: 411.92, Max budget: 200.0" — even though personal key spend is only ~$11

Root cause

_batch_database_updates() in db_spend_update_writer.py (line 333) calls _update_user_db() unconditionally — it does not pass team_id, and _update_user_db() has no guard to skip the update when the request came from a team key.

Compare _update_team_db() (line 560–565), which correctly short-circuits when team_id is None:

# _update_team_db (correctly guarded)
async def _update_team_db(self, ..., team_id, ...):
    if team_id is None or prisma_client is None:
        return  # skip — not a team key call
# _update_user_db (no guard — the bug)
async def _update_user_db(self, ...,  # no team_id parameter at all
):
    # unconditionally enqueues spend increment for the user
    await self.spend_update_queue.add_update(
        update=SpendUpdateQueueItem(
            entity_type=Litellm_EntityType.USER,
            entity_id=_id,
            response_cost=response_cost,
        )
    )

The budget check in auth_checks.py (line 623–633) is correctly guarded:

# Correctly skips personal budget check for team key calls
if (
    (team_object is None or team_object.team_id is None)  # ← correct guard
    and user_object is not None
    and user_object.max_budget is not None
):
    user_spend = await get_current_spend(
        counter_key=f"spend:user:{user_object.user_id}",
        fallback_spend=user_object.spend or 0.0,  # ← reads polluted DB field
    )
    if user_spend >= user_budget:
        raise litellm.BudgetExceededError(...)

The mismatch between the spend write path (no guard) and the spend check path (correctly guarded) is what causes the bug. The fallback_spend=user_object.spend line reads from the DB field that has been inflated by team key costs, so any time the Redis counter is absent (e.g. after a proxy restart), the inflated DB value is used and triggers a false positive.

Proposed fix

Add team_id as a parameter to _update_user_db() and short-circuit when it is set:

async def _update_user_db(
    self,
    response_cost: Optional[float],
    user_id: Optional[str],
    prisma_client: Optional[PrismaClient],
    user_api_key_cache: DualCache,
    litellm_proxy_budget_name: Optional[str],
    end_user_id: Optional[str] = None,
    team_id: Optional[str] = None,    # new parameter
):
    if team_id is not None:
        return  # team key call — spend is tracked against the team, not the user

And update the caller in _batch_database_updates() (line 333) to pass team_id:

await self._update_user_db(
    response_cost=response_cost,
    user_id=user_id,
    prisma_client=prisma_client,
    user_api_key_cache=user_api_key_cache,
    litellm_proxy_budget_name=litellm_proxy_budget_name,
    end_user_id=end_user_id,
    team_id=team_id,    # pass through
)

Relevant log output

BudgetExceededError: ExceededBudget: LiteLLM Proxy: User=<user_id> over budget.
  Current cost: 411.92, Max budget: 200.0, Reset At: 2026-05-01 00:00:00+00:00

What part of LiteLLM is this about?

Proxy — spend tracking (db_spend_update_writer.py) and budget enforcement (auth_checks.py)

What LiteLLM version are you on?

main @ 09cd7e383e (April 2026)

extent analysis

TL;DR

Add a team_id parameter to _update_user_db() and short-circuit when it is set to prevent team key spend from being written to the user's personal spend record.

Guidance

  • Identify the _update_user_db() function in db_spend_update_writer.py and add a team_id parameter to it.
  • Update the function to short-circuit when team_id is not None, preventing team key spend from being written to the user's personal spend record.
  • Update the caller in _batch_database_updates() to pass team_id to _update_user_db().
  • Verify that the fix works by testing the scenario described in the "Steps to Reproduce" section and checking that the BudgetExceededError is no longer raised incorrectly.

Example

async def _update_user_db(
    self,
    response_cost: Optional[float],
    user_id: Optional[str],
    prisma_client: Optional[PrismaClient],
    user_api_key_cache: DualCache,
    litellm_proxy_budget_name: Optional[str],
    end_user_id: Optional[str] = None,
    team_id: Optional[str] = None,    # new parameter
):
    if team_id is not None:
        return  # team key call — spend is tracked against the team, not the user

Notes

This fix assumes that the team_id is available in the _batch_database_updates() function and can be passed to _update_user_db(). If this is not the case, additional changes may be needed to make the team_id available.

Recommendation

Apply the proposed fix to add a team_id parameter to _update_user_db() and short-circuit when it is set. This fix addresses the root cause of the issue and prevents team key spend from being written to the user's personal spend record.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

litellm - ✅(Solved) Fix [Bug]: Team key spend incorrectly increments user personal spend, causing false BudgetExceededError on personal key calls [1 pull requests, 1 comments, 1 participants]