Skip to content

fix(team): apply team_member_budget updates to members still on the team default - #41347

Merged
ryan-crabbe-berri merged 3 commits into
mainfrom
litellm_team_member_budget_link_default
Sep 19, 2026
Merged

ryan-crabbe-berri merged 3 commits into
mainfrom
litellm_team_member_budget_link_default

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • /team/update with team_member_budget silently skips existing members
  • Members added since v1.83.11 got a private copy of the team default
  • Their old cap kept applying while /team/info showed the new one

How it solves it:

  • /team/member_add links members to the shared team default again
  • /team/member_update still clones that row on first per-member edit
  • Explicit max_budget_in_team, allowed_models, or budget_duration keep a private row
  • test_team_update_reaches_inherited_members_but_not_overridden_ones walks the whole flow through add_new_member, _upsert_budget_and_membership, upsert_team_member_budget_table and _check_team_member_budget against an in-memory table double, so it fails on main (the inherited member gets a private row) and passes here (the inherited member is capped at the new team value while the overridden member keeps its own cap)

User Flow

Before: an admin lowers the team's default member budget and nothing changes for the people already on the team

  1. They send POST https://litellm-domain/team/new with {"team_alias": "lit7716", "team_member_budget": 100} and get back a team_id
  2. They send POST https://litellm-domain/team/member_add with two members and no per-member budget; the response lists each membership with its own budget_id and max_budget: 100
  3. Both members spend a little through POST https://litellm-domain/v1/chat/completions and get 200s
  4. The admin sends POST https://litellm-domain/team/update with {"team_id": "...", "team_member_budget": 0.000001} and gets 200
  5. GET https://litellm-domain/team/info?team_id=... still shows each member at max_budget: 100
  6. The member who never had a budget of their own sends the same chat request and gets 200 again, so the new cap is not enforced

After: the same update reaches every member who is still on the team default

  1. They send POST https://litellm-domain/team/new with {"team_alias": "lit7716", "team_member_budget": 100} and get back a team_id
  2. They send POST https://litellm-domain/team/member_add with two members and no per-member budget; the response shows both memberships sharing the team's team-lit7716-budget-... id at max_budget: 100
  3. Both members spend a little through POST https://litellm-domain/v1/chat/completions and get 200s
  4. The admin sends POST https://litellm-domain/team/update with {"team_id": "...", "team_member_budget": 0.000001} and gets 200
  5. GET https://litellm-domain/team/info?team_id=... shows the inherited member at max_budget: 1e-06; a member who was given max_budget_in_team: 50 through /team/member_update stays at 50
  6. The inherited member's next chat request returns 429 Budget has been exceeded! TeamMember=..., and the member with their own budget still gets 200

Relevant issues

Fixes #40783

Affected release

regression in v1.83.11 (#29119)

Linear ticket

Resolves LIT-7716

Pre-Submission checklist

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

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Shared setup: proxy on localhost:4000 with master_key: sk-1234, one openai/gpt-5.4-mini deployment named gpt-5.4-mini, Postgres attached. $AUTH below is -H "Authorization: Bearer sk-1234" -H "Content-Type: application/json". Every chat call is a real OpenAI request

Chat body used in every step: {"model":"gpt-5.4-mini","messages":[{"role":"user","content":"say hi"}],"max_tokens":5}

Before (113a416)

  1. Create the team and add two members with no per-member budget. Each membership gets a fresh private budget id instead of the team's team-lit7716-budget-... id
$ curl http://localhost:4000/team/new $AUTH -d '{"team_alias":"lit7716","team_member_budget":100}' | jq '{team_id, team_member_budget_id: .metadata.team_member_budget_id}'
{"team_id":"ed41e6df-039f-409e-bc4f-30d41c4b4ba5","team_member_budget_id":"team-lit7716-budget-86f8e521ef074c2bb666a346b62b9c27"}

$ curl http://localhost:4000/team/member_add $AUTH -d '{"team_id":"ed41e6df-039f-409e-bc4f-30d41c4b4ba5","member":[{"user_id":"lit7716-inherits-30629","role":"user"},{"user_id":"lit7716-override-30629","role":"user"}]}' | jq '.updated_team_memberships[] | {user_id, budget_id, max_budget: .litellm_budget_table.max_budget}'
{"user_id":"lit7716-inherits-30629","budget_id":"94d35342-baa2-4254-af11-605b2bc3d988","max_budget":100}
{"user_id":"lit7716-override-30629","budget_id":"d7962272-6c7f-4a2f-9e8b-0130c8694b20","max_budget":100}
  1. Give the second member their own budget
$ curl http://localhost:4000/team/member_update $AUTH -d '{"team_id":"ed41e6df-039f-409e-bc4f-30d41c4b4ba5","user_id":"lit7716-override-30629","max_budget_in_team":50}'
{"user_id":"lit7716-override-30629","user_email":null,"team_id":"ed41e6df-039f-409e-bc4f-30d41c4b4ba5","max_budget_in_team":50,"tpm_limit":null,"rpm_limit":null,"budget_duration":null,"allowed_models":null}
  1. One key per member, one real chat call each (both 200, "content":"Hi!"), then check spend landed
$ curl 'http://localhost:4000/team/info?team_id=ed41e6df-039f-409e-bc4f-30d41c4b4ba5' $AUTH | jq '.team_memberships[] | {user_id, budget_id, max_budget: .litellm_budget_table.max_budget, spend}'
{"user_id":"default_user_id","budget_id":"d56c1b41-d215-4f31-8993-21432b2ef011","max_budget":100,"spend":0}
{"user_id":"lit7716-inherits-30629","budget_id":"94d35342-baa2-4254-af11-605b2bc3d988","max_budget":100,"spend":2.85e-05}
{"user_id":"lit7716-override-30629","budget_id":"d7962272-6c7f-4a2f-9e8b-0130c8694b20","max_budget":50,"spend":2.85e-05}
  1. Lower the team default. The call succeeds but no membership changes
$ curl http://localhost:4000/team/update $AUTH -d '{"team_id":"ed41e6df-039f-409e-bc4f-30d41c4b4ba5","team_member_budget":0.000001}' | jq '{team_id: .data.team_id, team_member_budget_id: .data.metadata.team_member_budget_id}'
{"team_id":"ed41e6df-039f-409e-bc4f-30d41c4b4ba5","team_member_budget_id":"team-lit7716-budget-86f8e521ef074c2bb666a346b62b9c27"}

$ curl 'http://localhost:4000/team/info?team_id=ed41e6df-039f-409e-bc4f-30d41c4b4ba5' $AUTH | jq '.team_memberships[] | {user_id, budget_id, max_budget: .litellm_budget_table.max_budget, spend}'
{"user_id":"default_user_id","budget_id":"d56c1b41-d215-4f31-8993-21432b2ef011","max_budget":100,"spend":0}
{"user_id":"lit7716-inherits-30629","budget_id":"94d35342-baa2-4254-af11-605b2bc3d988","max_budget":100,"spend":2.85e-05}
{"user_id":"lit7716-override-30629","budget_id":"d7962272-6c7f-4a2f-9e8b-0130c8694b20","max_budget":50,"spend":2.85e-05}
  1. The inherited member is still allowed through, which is the bug
$ curl http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer $KEY_A' -H 'Content-Type: application/json' -d '<chat body>' -w '\nHTTP %{http_code}'
{"id":"chatcmpl-EOYcDR3rsmZby8aLxEegB2OANGy8e","created":1789521641,"model":"gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi!","role":"assistant",...}}],...}
HTTP 200

$ curl http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer $KEY_B' -H 'Content-Type: application/json' -d '<chat body>' -w '\nHTTP %{http_code}'
{"id":"chatcmpl-EOYcDFFLcom5zPRt7VpIqbBXNkm3B","created":1789521641,"model":"gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi!","role":"assistant",...}}],...}
HTTP 200

After (b4c5f6f, the merge with main)

  1. Create the team and add two members with no per-member budget. Both memberships now carry the team's own team-lit7716-budget-... id
$ curl http://localhost:4000/team/new $AUTH -d '{"team_alias":"lit7716","team_member_budget":100}' | jq '{team_id, team_member_budget_id: .metadata.team_member_budget_id}'
{"team_id":"632e355d-a34b-4109-9356-40ef1a9e4a50","team_member_budget_id":"team-lit7716-budget-185ab3196b4f447dbcf0c238b749f6e0"}

$ curl http://localhost:4000/team/member_add $AUTH -d '{"team_id":"632e355d-a34b-4109-9356-40ef1a9e4a50","member":[{"user_id":"lit7716-inherits-19386","role":"user"},{"user_id":"lit7716-override-19386","role":"user"}]}' | jq '.updated_team_memberships[] | {user_id, budget_id, max_budget: .litellm_budget_table.max_budget}'
{"user_id":"lit7716-inherits-19386","budget_id":"team-lit7716-budget-185ab3196b4f447dbcf0c238b749f6e0","max_budget":100}
{"user_id":"lit7716-override-19386","budget_id":"team-lit7716-budget-185ab3196b4f447dbcf0c238b749f6e0","max_budget":100}
  1. Give the second member their own budget. Only that membership moves off the shared row
$ curl http://localhost:4000/team/member_update $AUTH -d '{"team_id":"632e355d-a34b-4109-9356-40ef1a9e4a50","user_id":"lit7716-override-19386","max_budget_in_team":50}'
{"user_id":"lit7716-override-19386","user_email":null,"team_id":"632e355d-a34b-4109-9356-40ef1a9e4a50","max_budget_in_team":50,"tpm_limit":null,"rpm_limit":null,"budget_duration":null,"allowed_models":null,"temp_budget_increase":null,"temp_budget_expiry":null}
  1. One key per member, one real chat call each (both 200, "content":"Hi!"), then check spend landed
$ curl 'http://localhost:4000/team/info?team_id=632e355d-a34b-4109-9356-40ef1a9e4a50' $AUTH | jq '.team_memberships[] | {user_id, budget_id, max_budget: .litellm_budget_table.max_budget, spend}'
{"user_id":"default_user_id","budget_id":"team-lit7716-budget-185ab3196b4f447dbcf0c238b749f6e0","max_budget":100,"spend":0}
{"user_id":"lit7716-inherits-19386","budget_id":"team-lit7716-budget-185ab3196b4f447dbcf0c238b749f6e0","max_budget":100,"spend":2.85e-05}
{"user_id":"lit7716-override-19386","budget_id":"547ce554-0c3a-4244-a4f7-6ae4768c3d69","max_budget":50,"spend":2.85e-05}
  1. Lower the team default. The inherited member follows it, the overridden member keeps 50
$ curl http://localhost:4000/team/update $AUTH -d '{"team_id":"632e355d-a34b-4109-9356-40ef1a9e4a50","team_member_budget":0.000001}' | jq '{team_id: .data.team_id, team_member_budget_id: .data.metadata.team_member_budget_id}'
{"team_id":"632e355d-a34b-4109-9356-40ef1a9e4a50","team_member_budget_id":"team-lit7716-budget-185ab3196b4f447dbcf0c238b749f6e0"}

$ curl 'http://localhost:4000/team/info?team_id=632e355d-a34b-4109-9356-40ef1a9e4a50' $AUTH | jq '.team_memberships[] | {user_id, budget_id, max_budget: .litellm_budget_table.max_budget, spend}'
{"user_id":"default_user_id","budget_id":"team-lit7716-budget-185ab3196b4f447dbcf0c238b749f6e0","max_budget":1e-06,"spend":0}
{"user_id":"lit7716-inherits-19386","budget_id":"team-lit7716-budget-185ab3196b4f447dbcf0c238b749f6e0","max_budget":1e-06,"spend":2.85e-05}
{"user_id":"lit7716-override-19386","budget_id":"547ce554-0c3a-4244-a4f7-6ae4768c3d69","max_budget":50,"spend":2.85e-05}
  1. The inherited member is blocked, the member with their own budget still goes through
$ curl http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer $KEY_A' -H 'Content-Type: application/json' -d '<chat body>' -w '\nHTTP %{http_code}'
{"error":{"message":"Budget has been exceeded! TeamMember=lit7716-inherits-19386:632e355d-a34b-4109-9356-40ef1a9e4a50 Current cost: 2.85e-05, Max budget: 1e-06","type":"budget_exceeded","param":null,"code":"429"}}
HTTP 429

$ curl http://localhost:4000/v1/chat/completions -H 'Authorization: Bearer $KEY_B' -H 'Content-Type: application/json' -d '<chat body>' -w '\nHTTP %{http_code}'
{"id":"chatcmpl-EPc12g1bVVplFUhjEBayikQUM6pGa","created":1789773040,"model":"gpt-5.4-mini","object":"chat.completion","choices":[{"finish_reason":"length","index":0,"message":{"content":"Hi!","role":"assistant",...}}],...}
HTTP 200

The merge with main picks up the membership upsert from main, which still writes the membership row when the team default budget row is gone, so test_add_new_member_no_budget_when_default_budget_row_is_missing now checks that the row is written without a budget_id instead of not at all

Type

🐛 Bug Fix

Caveats (if any)

Medium

  • Members added on v1.83.11 through the release before this one keep their private copy
    • Nothing in the schema records whether a copy was inherited or deliberate, so this PR does not rewrite them
    • Fix them with /team/member_update per member or the bulk member budget UI, or re-add them
  • A member with only budget_duration still gets a private clone, so later team-level changes do not reach them

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

Link to Devin session: https://app.devin.ai/sessions/5d1eee9e522749968ae46d78e845be94
Open in Devin Desktop: https://app.devin.ai/desktop/session/5d1eee9e522749968ae46d78e845be94?variant=devin


Note

Medium Risk
Changes team member budget linking and spend enforcement semantics; members on shared rows now follow team default updates, though explicit per-member budgets and duration-only clones are unchanged.

Overview
Fixes a regression where /team/member_add gave each member a private copy of the team default budget, so later /team/update changes to team_member_budget did not affect them.

_resolve_member_budget_id now links memberships to the team's shared default_team_budget_id when there is no explicit per-member cap or model list and no budget_duration (after confirming the default row still exists). Cloning is kept only when a member needs a private row—explicit limits, budget_duration alone (override reset window while keeping the default's cap), or the existing window-only path when there is no team default.

Docs and tests are aligned with that model: add-member cases assert no budget create, a missing default still creates membership without a budget_id, and test_team_update_reaches_inherited_members_but_not_overridden_ones checks that a team-wide budget update caps inherited members while /team/member_update overrides stay on their own budget.

Reviewed by Cursor Bugbot for commit b4c5f6f. Bugbot is set up for automated code reviews on this repo. Configure here.

…/update applies to them

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@codspeed

codspeed Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_team_member_budget_link_default (b4c5f6f) with main (8e93031)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

The PR appears safe to merge because inherited members retain independent spend accounting while team-default changes propagate as intended and member-specific updates clone before mutation.

Summary

This PR restores inherited team-member budget behavior by linking members without explicit limits to the team’s shared default budget and retaining clone-on-write semantics for member-specific overrides.

  • Verifies that the configured default budget row exists before linking a membership.
  • Preserves private rows for explicit limits and custom budget durations.
  • Adds functional coverage showing team updates reach inherited members without changing overridden members.
  • Keeps memberships valid without a budget link when the referenced default row is missing.

Reviews (3) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..."

Comment thread tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.py Outdated
Comment thread tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.py Outdated
@codecov

codecov Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…tion end to end

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@devin-ai-integration devin-ai-integration Bot added the backport-stable P0 regression fix only (Urgent ticket): cherry-pick onto the baking rc line before the stable tag label Sep 16, 2026

@cursor cursor Bot left a comment •

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.

Stale Bugbot comment from a previous run.

…udget_link_default

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

# Conflicts:
#	litellm/proxy/management_helpers/utils.py
#	tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.py
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b4c5f6f. Configure here.

@ryan-crabbe-berri
ryan-crabbe-berri merged commit 4bb1ae1 into main Sep 19, 2026
94 of 96 checks passed
@ryan-crabbe-berri
ryan-crabbe-berri deleted the litellm_team_member_budget_link_default branch September 19, 2026 00:06
mateo-berri added a commit that referenced this pull request Sep 19, 2026
fix(team): apply team_member_budget updates to members still on the team default (backport of #41347 to rc/1.102.0)
tech-carrement pushed a commit to tech-carrement/litellm that referenced this pull request Sep 20, 2026
…eam default

Backport of BerriAI#41347 to rc/1.102.0.
Cherry-picked from merge commit 4bb1ae1 (main), originally by app/devin-ai-integration.

Conflicts: main's BerriAI#41349 (membership rows written through upsert) is not on this line, so add_new_member keeps its create call and still writes no membership row when no budget resolves. The picked tests are adapted to that and to this line's _check_team_member_budget, which loads the membership itself.
@mateo-berri mateo-berri removed the backport-stable P0 regression fix only (Urgent ticket): cherry-pick onto the baking rc line before the stable tag label Sep 26, 2026
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.

[Bug]: Updating team_member_budget silently does not apply to existing team members

3 participants