fix(team): persist budget_duration on /team/member_add member budgets - #31443
Conversation
|
|
Greptile SummaryThis PR fixes a gap on
Confidence Score: 5/5Safe 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 No files require special attention.
|
| 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 SummaryThis PR fixes a gap where
Confidence Score: 4/5Safe to merge with awareness of one edge case: passing only The core fix is correct and well-scoped — validation fires before any DB write,
|
| 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
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
54e55d1 to
9534d2e
Compare
|
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 |
9534d2e to
58cff35
Compare
|
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
58cff35 to
fe9141a
Compare
|
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 |
Relevant issues
Resolves LIT-4052. Sibling of #25509 / LIT-4012, which fixed the same gap on
/team/member_updateLinear ticket
LIT-4052
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
Live proxy against real Postgres, running this branch. A member added with an explicit
max_budget_in_teamwhile the team runs a recurring member budget used to get a lifetime cap; nowbudget_durationis persisted and abudget_reset_atis scheduledBefore the fix,
budget_durationis silently dropped on add:After the fix, it is persisted, a duration-only budget is also honored, and an invalid duration is rejected before any DB write:
Self-join hardening
Since
budget_durationis 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 of5.0;aliceis a non-admin internal user self-joining itBefore the guard, a non-admin self-joiner could override both the cap and the reset window of their own membership, escaping the team default:
After the guard, the same call is rejected before any DB write, while the legitimate paths keep working:
Type
🐛 Bug Fix
Changes
/team/member_addcould not setbudget_durationon an individual member budget.add_new_membercreated the budget row with onlymax_budgetandallowed_models, andTeamMemberAddRequesthad nobudget_durationfield, 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 explicitmax_budget_in_teamwas supplied on addThis threads
budget_durationfromTeamMemberAddRequestthrough_process_team_membersintoadd_new_member, and pulls the member-budget resolution into a helper that writesbudget_durationplus a computedbudget_reset_at. When only abudget_durationis supplied and the team has a default member budget, the default is cloned and its reset window overridden so the member keeps the default'smax_budgetrather 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_updateBecause the new field makes member budget controls settable on add,
_validate_team_member_add_permissionsnow rejectsmax_budget_in_team,budget_duration, andallowed_modelsfor 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 budgetRegression tests in
tests/test_litellm/proxy/management_helpers/test_management_helpers_utils.pycover 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 intests/test_litellm/proxy/management_endpoints/test_team_endpoints.pycover the self-join guard rejecting each budget control and still allowing a clean self-join; all fail on the pre-fix code