mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
fix(proxy): skip members already on the team before resolving a per-member budget
A mixed /team/member_add list that names an existing member used to run add_new_member for them, which created or cloned a budget that the empty upsert update branch never linked to their membership row. Filter the requested members against the freshly locked roster first so budgets and membership rows are only written for members who are actually new Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
b6f4ad190e
commit
61d4c5b9b5
2 changed files with 79 additions and 26 deletions
|
|
@ -2904,10 +2904,15 @@ async def _process_team_members(
|
|||
if member_allowed_models is None and team_default_member_models:
|
||||
member_allowed_models = team_default_member_models
|
||||
|
||||
if isinstance(data.member, Member):
|
||||
requested_members: Final[Sequence[Member]] = (
|
||||
(data.member,) if isinstance(data.member, Member) else tuple(data.member)
|
||||
)
|
||||
for m in requested_members:
|
||||
if _member_already_in_team(m, complete_team_data):
|
||||
continue
|
||||
try:
|
||||
updated_user, updated_tm = await add_new_member(
|
||||
new_member=data.member,
|
||||
new_member=m,
|
||||
max_budget_in_team=data.max_budget_in_team,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
|
|
@ -2921,34 +2926,11 @@ async def _process_team_members(
|
|||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": f"Unable to add user - {data.member}, to team - {data.team_id}, for reason - {e}"},
|
||||
detail={"error": f"Unable to add user - {m}, to team - {data.team_id}, for reason - {e}"},
|
||||
)
|
||||
updated_users.append(updated_user)
|
||||
if updated_tm is not None:
|
||||
updated_team_memberships.append(updated_tm)
|
||||
elif isinstance(data.member, list):
|
||||
for m in data.member:
|
||||
try:
|
||||
updated_user, updated_tm = await add_new_member(
|
||||
new_member=m,
|
||||
max_budget_in_team=data.max_budget_in_team,
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
litellm_proxy_admin_name=litellm_proxy_admin_name,
|
||||
team_id=data.team_id,
|
||||
default_team_budget_id=default_team_budget_id,
|
||||
allowed_models=member_allowed_models,
|
||||
budget_duration=data.budget_duration,
|
||||
tx=tx,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail={"error": f"Unable to add user - {m}, to team - {data.team_id}, for reason - {e}"},
|
||||
)
|
||||
updated_users.append(updated_user)
|
||||
if updated_tm is not None:
|
||||
updated_team_memberships.append(updated_tm)
|
||||
|
||||
return updated_users, updated_team_memberships
|
||||
|
||||
|
|
|
|||
|
|
@ -1794,6 +1794,7 @@ async def test_process_team_members_single_member():
|
|||
mock_team = MagicMock(spec=LiteLLM_TeamTable)
|
||||
mock_team.metadata = {"team_member_budget_id": "budget-123"}
|
||||
mock_team.default_team_member_models = None
|
||||
mock_team.members_with_roles = []
|
||||
|
||||
# Mock user and membership objects
|
||||
mock_user = MagicMock(spec=LiteLLM_UserTable)
|
||||
|
|
@ -1854,6 +1855,7 @@ async def test_process_team_members_multiple_members():
|
|||
mock_team = MagicMock(spec=LiteLLM_TeamTable)
|
||||
mock_team.metadata = None
|
||||
mock_team.default_team_member_models = None
|
||||
mock_team.members_with_roles = []
|
||||
|
||||
# Create multiple members as dictionaries (they will be converted to Member objects)
|
||||
members = [
|
||||
|
|
@ -2114,6 +2116,75 @@ async def test_add_team_members_runs_member_writes_on_the_lock_holding_transacti
|
|||
assert [tm.budget_id for tm in updated_team_memberships] == ["budget-pool"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_team_members_skips_budget_and_membership_writes_for_members_already_on_the_roster():
|
||||
"""
|
||||
Regression pin for orphaned budgets on a mixed /team/member_add list.
|
||||
|
||||
A list naming one member already on the team and one new member must only create a
|
||||
budget and membership row for the new member. Running add_new_member for the existing
|
||||
member would create a per-member budget that nothing links to, since their membership
|
||||
row (and the budget it already carries) is left untouched.
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.team_endpoints import (
|
||||
_add_team_members_to_team,
|
||||
)
|
||||
|
||||
added_user = MagicMock()
|
||||
added_user.user_id = "bob"
|
||||
added_user.model_dump.return_value = {"user_id": "bob", "teams": ["team-mixed"]}
|
||||
created_budget = MagicMock()
|
||||
created_budget.budget_id = "budget-bob"
|
||||
membership = MagicMock()
|
||||
membership.model_dump.return_value = {
|
||||
"team_id": "team-mixed",
|
||||
"user_id": "bob",
|
||||
"budget_id": "budget-bob",
|
||||
"litellm_budget_table": None,
|
||||
}
|
||||
|
||||
tx = MagicMock()
|
||||
tx.query_raw = AsyncMock(
|
||||
return_value=[{"members_with_roles": [{"user_id": "alice", "user_email": None, "role": "user"}]}]
|
||||
)
|
||||
tx.litellm_teamtable.update = AsyncMock(
|
||||
return_value=LiteLLM_TeamTable(team_id="team-mixed", members_with_roles=[])
|
||||
)
|
||||
tx.litellm_usertable.upsert = AsyncMock(return_value=added_user)
|
||||
tx.litellm_usertable.update_many = AsyncMock()
|
||||
tx.litellm_budgettable.create = AsyncMock(return_value=created_budget)
|
||||
tx.litellm_teammembership.upsert = AsyncMock(return_value=membership)
|
||||
|
||||
tx_cm = MagicMock()
|
||||
tx_cm.__aenter__ = AsyncMock(return_value=tx)
|
||||
tx_cm.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
prisma_client = MagicMock()
|
||||
prisma_client.tx = MagicMock(return_value=tx_cm)
|
||||
|
||||
_, updated_users, updated_team_memberships = await _add_team_members_to_team(
|
||||
data=TeamMemberAddRequest(
|
||||
team_id="team-mixed",
|
||||
member=[Member(user_id="alice", role="user"), Member(user_id="bob", role="user")],
|
||||
max_budget_in_team=50.0,
|
||||
),
|
||||
complete_team_data=LiteLLM_TeamTable(team_id="team-mixed", members_with_roles=[]),
|
||||
prisma_client=cast(object, prisma_client),
|
||||
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
||||
litellm_proxy_admin_name="admin",
|
||||
)
|
||||
|
||||
tx.litellm_budgettable.create.assert_awaited_once()
|
||||
tx.litellm_teammembership.upsert.assert_awaited_once()
|
||||
assert tx.litellm_teammembership.upsert.call_args.kwargs["where"] == {
|
||||
"user_id_team_id": {"user_id": "bob", "team_id": "team-mixed"}
|
||||
}
|
||||
assert [user.user_id for user in updated_users] == ["bob"]
|
||||
assert [tm.user_id for tm in updated_team_memberships] == ["bob"]
|
||||
written_ids = [m["user_id"] for m in json.loads(tx.litellm_teamtable.update.call_args.kwargs["data"]["members_with_roles"])]
|
||||
assert written_ids == ["alice", "bob"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_team_members_writes_nothing_when_the_team_is_deleted_mid_request():
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue