Skip to content

fix(team): persist budget_duration on /team/member_add member budgets - #31443

Merged
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_team_member_add_budget_duration
Jun 26, 2026
Merged

fix(team): persist budget_duration on /team/member_add member budgets#31443
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_team_member_add_budget_duration

Conversation

@yassin-berriai

@yassin-berriai yassin-berriai commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Resolves LIT-4052. Sibling of #25509 / LIT-4012, which fixed the same gap on /team/member_update

Linear ticket

LIT-4052

Pre-Submission checklist

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

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • 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

Live proxy against real Postgres, running this branch. A member added with an explicit max_budget_in_team while the team runs a recurring member budget used to get a lifetime cap; now budget_duration is persisted and a budget_reset_at is scheduled

Before the fix, budget_duration is silently dropped on add:

# POST /team/member_add {"team_id":"before-team","member":{"role":"user","user_id":"before-user"},"max_budget_in_team":10.0,"budget_duration":"30d"}
# GET /team/info?team_id=before-team
max_budget     : 10.0
budget_duration: None
budget_reset_at: None

After the fix, it is persisted, a duration-only budget is also honored, and an invalid duration is rejected before any DB write:

# POST /team/member_add {"team_id":"after-team","member":{"role":"user","user_id":"after-user"},"max_budget_in_team":10.0,"budget_duration":"30d"}
# POST /team/member_add {"team_id":"after-team","member":{"role":"user","user_id":"after-user2"},"budget_duration":"7d"}
# POST /team/member_add {... ,"budget_duration":"not-a-duration"}  -> HTTP 400

# GET /team/info?team_id=after-team
user after-user  | max_budget 10.0 | budget_duration 30d | budget_reset_at 2026-07-01T00:00:00Z
user after-user2 | max_budget None | budget_duration 7d  | budget_reset_at 2026-06-29T00:00:00Z

Self-join hardening

Since budget_duration is now honored on add, the available-team self-join bypass had to be tightened so a non-admin cannot set their own per-member budget controls. The proxy below runs with an available team (public-team-4052) that has a default member budget of 5.0; alice is a non-admin internal user self-joining it

Before the guard, a non-admin self-joiner could override both the cap and the reset window of their own membership, escaping the team default:

# POST /team/member_add (Bearer <alice non-admin key>)
#   {"team_id":"public-team-4052","member":{"role":"user","user_id":"alice-4052"},"max_budget_in_team":1000.0,"budget_duration":"1h"}
HTTP 200

# member budget rows for the team
 default_user_id |    5 |      |
 alice-4052      | 1000 | 1h   | 2026-06-26 13:00:00

After the guard, the same call is rejected before any DB write, while the legitimate paths keep working:

# self-join WITH budget controls          -> HTTP 403
{"detail":{"error":"Available-team self-join cannot set per-member budget or model controls (max_budget_in_team, budget_duration, allowed_models); these are admin-only."}}

# self-join with budget_duration alone     -> HTTP 403 (same error)

# clean self-join (no budget controls)     -> HTTP 200, inherits the team default
 alice-4052 | 5 |

# admin adds a member with budget_duration -> HTTP 200, persisted
 bob-4052 | 25 | 30d | 2026-07-01 00:00:00

Type

🐛 Bug Fix

Changes

/team/member_add could not set budget_duration on an individual member budget. add_new_member created the budget row with only max_budget and allowed_models, and TeamMemberAddRequest had no budget_duration field, so a member added with an explicit per-member budget while the team ran a recurring member budget got a lifetime cap rather than a recurring allowance. The no-explicit-budget path was unaffected because it clones the team's default member budget, which already carries the duration; the gap only showed when an explicit max_budget_in_team was supplied on add

This threads budget_duration from TeamMemberAddRequest through _process_team_members into add_new_member, and pulls the member-budget resolution into a helper that writes budget_duration plus a computed budget_reset_at. When only a budget_duration is supplied and the team has a default member budget, the default is cloned and its reset window overridden so the member keeps the default's max_budget rather than becoming uncapped; a duration with no team default creates a window-only budget. An invalid duration is rejected with a 400 before any DB write, matching /team/member_update

Because the new field makes member budget controls settable on add, _validate_team_member_add_permissions now rejects max_budget_in_team, budget_duration, and allowed_models for non-admin callers using the available-team self-join bypass, before any DB write. The bypass only grants the ability to join, so a self-joiner can no longer set their own cap, reset window, or model scope past the team default; proxy admins, team admins, and org admins are unaffected, and a clean self-join still inherits the team default member budget

Regression tests in tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.py cover the duration being written with a future reset time, the duration-only case, and the clone-with-override case that keeps the team default's max_budget. Tests in tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py cover the self-join guard rejecting each budget control and still allowing a clean self-join; all fail on the pre-fix code

@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.
You have signed the CLA already but the status is still pending? Let us recheck it.

@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a gap on /team/member_add where budget_duration was silently dropped when setting a per-member budget, causing the member to receive a lifetime cap instead of a recurring allowance. It also hardens the available-team self-join path so non-admin callers cannot set their own per-member budget or model controls via the bypass.

  • Budget resolution refactor: Inline budget logic in add_new_member is extracted into _resolve_member_budget_id. When only budget_duration is supplied and the team has a default member budget, the function now clones the default (preserving its max_budget) and overrides the reset window, which addresses the previously flagged silent-cap-loss scenario and is verified by the new test_add_new_member_budget_duration_only_clones_default_max_budget test.
  • Self-join guard: _validate_team_member_add_permissions now rejects max_budget_in_team, budget_duration, and allowed_models for non-admin callers using the available-team bypass, before any DB write. Proxy admins, team admins, and org admins are unaffected.
  • Input validation: _validate_budget_duration is called early in team_member_add to reject malformed or non-positive durations with a 400 before any DB write, matching the behavior already present on /team/member_update.

Confidence Score: 5/5

Safe to merge — the change is well-scoped, all new logic paths are covered by mock-only unit tests, and the previously identified gap in the budget-duration-only clone path is correctly resolved in this version.

The core budget resolution logic in _resolve_member_budget_id correctly handles all four cases: explicit limit with duration, explicit limit without duration, duration-only with a team default (clones default and overrides reset window, preserving max_budget), and duration-only with no team default. The self-join guard is positioned before any DB write and does not affect admin callers. Input validation via _validate_budget_duration mirrors the existing pattern on team_member_update. Tests cover the regression scenarios and confirm the guard allows clean self-joins.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/management_helpers/utils.py Adds budget_duration parameter to add_new_member; extracts budget resolution into _resolve_member_budget_id which correctly clones the team default (preserving its max_budget) when only a budget_duration override is given, and creates a fresh row with budget_reset_at when explicit limits are present.
litellm/proxy/management_endpoints/team_endpoints.py Threads budget_duration through _process_team_members to add_new_member; adds _validate_budget_duration call at the API entry point; hardens _validate_team_member_add_permissions to reject budget/model controls for available-team self-joiners before any DB write.
litellm/proxy/_types.py Adds budget_duration: Optional[str] field to TeamMemberAddRequest, mirroring the field already present on TeamMemberUpdateRequest.
tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.py Adds three targeted regression tests: duration written with future reset time, duration-only fresh budget, and duration-only clone that keeps the team default's max_budget; all mock-only, no network calls.
tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py Adds parametrized tests covering the self-join guard for each budget control and a passing clean-join test; also updates an existing mock call to include the new budget_duration=None argument.
ui/litellm-dashboard/src/lib/http/schema.d.ts Adds `budget_duration?: string

Reviews (5): Last reviewed commit: "fix(team): persist budget_duration on /t..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a gap where budget_duration passed to /team/member_add was silently discarded — members added with an explicit max_budget_in_team always got a lifetime cap rather than a recurring allowance. It mirrors the fix already applied to /team/member_update.

  • budget_duration is added to TeamMemberAddRequest, threaded through _process_team_members into add_new_member, and written to the budget row alongside a computed budget_reset_at; validation via the existing _validate_budget_duration guard is added before any DB write.
  • The budget-creation condition in add_new_member is widened so that a budget_duration supplied without max_budget_in_team still creates a budget row instead of silently falling through to the default-budget clone; two new mock-only regression tests cover both cases.

Confidence Score: 4/5

Safe to merge with awareness of one edge case: passing only budget_duration without max_budget_in_team bypasses the team's default member budget, potentially giving the added member an uncapped spending window.

The core fix is correct and well-scoped — validation fires before any DB write, budget_duration and budget_reset_at are written consistently, and the new tests are properly mocked. The one edge case to watch is the interaction between an explicit budget_duration-only request and a team that has a default_team_budget_id with a max_budget; in that combination the team default is bypassed and the member gets an uncapped budget, which isn't tested and may surprise callers.

litellm/proxy/management_helpers/utils.py — specifically the budget-creation branch where only budget_duration is provided alongside a non-null default_team_budget_id.

Important Files Changed

Filename Overview
litellm/proxy/_types.py Adds budget_duration field to TeamMemberAddRequest with correct type and default.
litellm/proxy/management_endpoints/team_endpoints.py Calls _validate_budget_duration before any DB work and threads budget_duration into both the single-member and bulk-member paths of _process_team_members; the validation placement and call sites are correct.
litellm/proxy/management_helpers/utils.py Expands add_new_member to accept budget_duration, persist it to the budget row, and compute budget_reset_at; also widens the condition to create a budget when only budget_duration is supplied (bypassing the default-budget clone path).
tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.py Adds two focused mock-only regression tests: one for budget_duration + max_budget, one for budget_duration alone; both verify the budget row and reset timestamp are written correctly.

Reviews (2): Last reviewed commit: "fix(team): persist budget_duration on /t..." | Re-trigger Greptile

Comment thread litellm/proxy/management_helpers/utils.py Outdated
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment thread litellm/proxy/management_helpers/utils.py Outdated
@veria-ai

veria-ai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

@yassin-berriai
yassin-berriai force-pushed the litellm_team_member_add_budget_duration branch from 54e55d1 to 9534d2e Compare June 26, 2026 11:32
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

Addressed the P1 from the last review: when only a budget_duration is supplied and the team has a default member budget, add_new_member now clones the default (keeping its max_budget) and overrides just the reset window, instead of creating an uncapped duration-only row. This matches the merge-into-existing behavior of /team/member_update. Added a regression test for that case, and refactored the budget resolution into a helper to keep complexity within the C901 budget. @greptileai

@yassin-berriai
yassin-berriai force-pushed the litellm_team_member_add_budget_duration branch from 9534d2e to 58cff35 Compare June 26, 2026 11:44
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

Fixed the proxy-endpoints failure (an existing _process_team_members test asserted the exact add_new_member kwargs; updated it for the new budget_duration arg) and covered the allowed_models branch of the new helper for patch coverage. @greptileai

/team/member_add could not set budget_duration on an individual member
budget. add_new_member created the budget row with only max_budget and
allowed_models, and TeamMemberAddRequest had no budget_duration field, so
a member added with an explicit per-member budget while the team ran a
recurring member budget got a lifetime cap instead of a recurring
allowance.

Thread budget_duration from TeamMemberAddRequest through
_process_team_members into add_new_member, and pull the member-budget
resolution into a helper that writes budget_duration plus a computed
budget_reset_at. When only a budget_duration is supplied and the team has
a default member budget, the default is cloned and its reset window
overridden so the member keeps the default's max_budget rather than
becoming uncapped; a duration with no team default creates a window-only
budget. Invalid durations are rejected with a 400 before any DB write,
symmetric with /team/member_update.

The available-team self-join bypass only grants the ability to join, so
reject per-member budget and model controls (max_budget_in_team,
budget_duration, allowed_models) for non-admin self-join callers in
_validate_team_member_add_permissions, before any DB write. Otherwise a
self-joining non-admin could set their own cap, reset window, or model
scope past the team default; admins, team admins, and org admins are
unaffected and a clean self-join still inherits the team default budget.

Resolves LIT-4052
@yassin-berriai
yassin-berriai force-pushed the litellm_team_member_add_budget_duration branch from 58cff35 to fe9141a Compare June 26, 2026 12:03
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

Addressed the Veria HIGH (member budget reset override on available-team self-join). The available-team self-join bypass now rejects per-member budget and model controls (max_budget_in_team, budget_duration, allowed_models) for non-admin callers in _validate_team_member_add_permissions, before any DB write, so a self-joiner can no longer shorten their reset window or widen their cap/model scope past the team default. Admins, team admins, and org admins are unaffected, and a clean self-join still inherits the team default. Added parametrized regression tests for the guard plus a clean-join allow test, and verified before/after on a live proxy against real Postgres (self-join with budget controls: 200 then 403). @greptileai

@yassin-berriai
yassin-berriai enabled auto-merge (squash) June 26, 2026 12:11
@yassin-berriai
yassin-berriai merged commit f55d13e into litellm_internal_staging Jun 26, 2026
124 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_team_member_add_budget_duration branch June 26, 2026 19:59
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