mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
refactor(proxy): split team member budget upsert for PLR0915
Extract helpers from _upsert_budget_and_membership and team_member_update so each stays under Ruff max-statements (no noqa). Made-with: Cursor
This commit is contained in:
parent
352d59ea2d
commit
e726e910df
2 changed files with 389 additions and 230 deletions
|
|
@ -345,6 +345,198 @@ def _set_object_metadata_field(
|
|||
object_data.metadata[field_name] = value
|
||||
|
||||
|
||||
def _budget_disconnect_all_unset(
|
||||
max_budget: Optional[float],
|
||||
tpm_limit: Optional[int],
|
||||
rpm_limit: Optional[int],
|
||||
allowed_models: Optional[List[str]],
|
||||
budget_duration: Optional[str],
|
||||
budget_duration_explicit: bool,
|
||||
) -> bool:
|
||||
return (
|
||||
max_budget is None
|
||||
and tpm_limit is None
|
||||
and rpm_limit is None
|
||||
and allowed_models is None
|
||||
and budget_duration is None
|
||||
and not budget_duration_explicit
|
||||
)
|
||||
|
||||
|
||||
async def _disconnect_team_member_budget(
|
||||
tx, *, team_id: str, user_id: str
|
||||
) -> None:
|
||||
await tx.litellm_teammembership.update(
|
||||
where={"user_id_team_id": {"user_id": user_id, "team_id": team_id}},
|
||||
data={"litellm_budget_table": {"disconnect": True}},
|
||||
)
|
||||
|
||||
|
||||
def _non_none_budget_limit_fields(
|
||||
max_budget: Optional[float],
|
||||
tpm_limit: Optional[int],
|
||||
rpm_limit: Optional[int],
|
||||
allowed_models: Optional[List[str]],
|
||||
) -> Dict[str, Any]:
|
||||
out: Dict[str, Any] = {}
|
||||
if max_budget is not None:
|
||||
out["max_budget"] = max_budget
|
||||
if tpm_limit is not None:
|
||||
out["tpm_limit"] = tpm_limit
|
||||
if rpm_limit is not None:
|
||||
out["rpm_limit"] = rpm_limit
|
||||
if allowed_models is not None:
|
||||
out["allowed_models"] = allowed_models
|
||||
return out
|
||||
|
||||
|
||||
def _apply_budget_duration_to_update_dict(
|
||||
update_data: Dict[str, Any],
|
||||
budget_duration: Optional[str],
|
||||
budget_duration_explicit: bool,
|
||||
) -> None:
|
||||
if budget_duration_explicit:
|
||||
if budget_duration is not None:
|
||||
update_data["budget_duration"] = budget_duration
|
||||
update_data["budget_reset_at"] = get_budget_reset_time(
|
||||
budget_duration=budget_duration
|
||||
)
|
||||
else:
|
||||
update_data["budget_duration"] = None
|
||||
update_data["budget_reset_at"] = None
|
||||
elif budget_duration is not None:
|
||||
update_data["budget_duration"] = budget_duration
|
||||
update_data["budget_reset_at"] = get_budget_reset_time(
|
||||
budget_duration=budget_duration
|
||||
)
|
||||
|
||||
|
||||
async def _update_existing_member_budget_in_place(
|
||||
tx,
|
||||
*,
|
||||
existing_budget_id: str,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
max_budget: Optional[float],
|
||||
tpm_limit: Optional[int],
|
||||
rpm_limit: Optional[int],
|
||||
allowed_models: Optional[List[str]],
|
||||
budget_duration: Optional[str],
|
||||
budget_duration_explicit: bool,
|
||||
) -> None:
|
||||
update_data: Dict[str, Any] = {
|
||||
"updated_by": user_api_key_dict.user_id or "",
|
||||
**_non_none_budget_limit_fields(
|
||||
max_budget, tpm_limit, rpm_limit, allowed_models
|
||||
),
|
||||
}
|
||||
_apply_budget_duration_to_update_dict(
|
||||
update_data, budget_duration, budget_duration_explicit
|
||||
)
|
||||
await tx.litellm_budgettable.update(
|
||||
where={"budget_id": existing_budget_id},
|
||||
data=update_data,
|
||||
)
|
||||
|
||||
|
||||
async def _clone_shared_default_into_create_data(
|
||||
tx, create_data: Dict[str, Any], *, existing_budget_id: str
|
||||
) -> None:
|
||||
default_budget_row = await tx.litellm_budgettable.find_unique(
|
||||
where={"budget_id": existing_budget_id}
|
||||
)
|
||||
if default_budget_row is None:
|
||||
return
|
||||
default_budget_dict = default_budget_row.model_dump()
|
||||
for field in (
|
||||
"max_budget",
|
||||
"soft_budget",
|
||||
"max_parallel_requests",
|
||||
"tpm_limit",
|
||||
"rpm_limit",
|
||||
"model_max_budget",
|
||||
"budget_duration",
|
||||
"allowed_models",
|
||||
):
|
||||
value = default_budget_dict.get(field)
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, list) and len(value) == 0:
|
||||
continue
|
||||
create_data[field] = value
|
||||
|
||||
|
||||
def _merge_limit_overrides_into_create_data(
|
||||
create_data: Dict[str, Any],
|
||||
*,
|
||||
max_budget: Optional[float],
|
||||
tpm_limit: Optional[int],
|
||||
rpm_limit: Optional[int],
|
||||
allowed_models: Optional[List[str]],
|
||||
) -> None:
|
||||
if max_budget is not None:
|
||||
create_data["max_budget"] = max_budget
|
||||
if tpm_limit is not None:
|
||||
create_data["tpm_limit"] = tpm_limit
|
||||
if rpm_limit is not None:
|
||||
create_data["rpm_limit"] = rpm_limit
|
||||
if allowed_models is not None:
|
||||
create_data["allowed_models"] = allowed_models
|
||||
|
||||
|
||||
def _merge_duration_into_create_data(
|
||||
create_data: Dict[str, Any],
|
||||
budget_duration: Optional[str],
|
||||
budget_duration_explicit: bool,
|
||||
) -> None:
|
||||
if budget_duration_explicit:
|
||||
if budget_duration is not None:
|
||||
create_data["budget_duration"] = budget_duration
|
||||
else:
|
||||
create_data.pop("budget_duration", None)
|
||||
create_data.pop("budget_reset_at", None)
|
||||
elif budget_duration is not None:
|
||||
create_data["budget_duration"] = budget_duration
|
||||
|
||||
|
||||
def _set_create_data_budget_reset_from_duration(create_data: Dict[str, Any]) -> None:
|
||||
bd = create_data.get("budget_duration")
|
||||
if bd is not None:
|
||||
create_data["budget_reset_at"] = get_budget_reset_time(budget_duration=bd)
|
||||
else:
|
||||
create_data.pop("budget_reset_at", None)
|
||||
|
||||
|
||||
async def _create_private_budget_and_link_membership(
|
||||
tx, *, team_id: str, user_id: str, create_data: Dict[str, Any]
|
||||
) -> None:
|
||||
new_budget = await tx.litellm_budgettable.create(
|
||||
data=create_data,
|
||||
include={"team_membership": True},
|
||||
)
|
||||
await tx.litellm_teammembership.upsert(
|
||||
where={
|
||||
"user_id_team_id": {
|
||||
"user_id": user_id,
|
||||
"team_id": team_id,
|
||||
}
|
||||
},
|
||||
data={
|
||||
"create": {
|
||||
"user_id": user_id,
|
||||
"team_id": team_id,
|
||||
"litellm_budget_table": {
|
||||
"connect": {"budget_id": new_budget.budget_id},
|
||||
},
|
||||
},
|
||||
"update": {
|
||||
"litellm_budget_table": {
|
||||
"connect": {"budget_id": new_budget.budget_id},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _upsert_budget_and_membership(
|
||||
tx,
|
||||
*,
|
||||
|
|
@ -389,19 +581,15 @@ async def _upsert_budget_and_membership(
|
|||
|
||||
If any of these values exist, a budget is updated or created and linked to the team membership.
|
||||
"""
|
||||
if (
|
||||
max_budget is None
|
||||
and tpm_limit is None
|
||||
and rpm_limit is None
|
||||
and allowed_models is None
|
||||
and budget_duration is None
|
||||
and not budget_duration_explicit
|
||||
if _budget_disconnect_all_unset(
|
||||
max_budget,
|
||||
tpm_limit,
|
||||
rpm_limit,
|
||||
allowed_models,
|
||||
budget_duration,
|
||||
budget_duration_explicit,
|
||||
):
|
||||
# disconnect the budget since all limits are None
|
||||
await tx.litellm_teammembership.update(
|
||||
where={"user_id_team_id": {"user_id": user_id, "team_id": team_id}},
|
||||
data={"litellm_budget_table": {"disconnect": True}},
|
||||
)
|
||||
await _disconnect_team_member_budget(tx, team_id=team_id, user_id=user_id)
|
||||
return
|
||||
|
||||
is_shared_default = (
|
||||
|
|
@ -411,121 +599,41 @@ async def _upsert_budget_and_membership(
|
|||
)
|
||||
|
||||
if existing_budget_id is not None and not is_shared_default:
|
||||
# Update the existing budget in-place to preserve fields not being changed.
|
||||
# Only write fields that the caller explicitly provided (non-None).
|
||||
update_data: Dict[str, Any] = {
|
||||
"updated_by": user_api_key_dict.user_id or "",
|
||||
}
|
||||
if max_budget is not None:
|
||||
update_data["max_budget"] = max_budget
|
||||
if tpm_limit is not None:
|
||||
update_data["tpm_limit"] = tpm_limit
|
||||
if rpm_limit is not None:
|
||||
update_data["rpm_limit"] = rpm_limit
|
||||
if allowed_models is not None:
|
||||
update_data["allowed_models"] = allowed_models
|
||||
if budget_duration_explicit:
|
||||
if budget_duration is not None:
|
||||
update_data["budget_duration"] = budget_duration
|
||||
update_data["budget_reset_at"] = get_budget_reset_time(
|
||||
budget_duration=budget_duration
|
||||
)
|
||||
else:
|
||||
update_data["budget_duration"] = None
|
||||
update_data["budget_reset_at"] = None
|
||||
elif budget_duration is not None:
|
||||
update_data["budget_duration"] = budget_duration
|
||||
update_data["budget_reset_at"] = get_budget_reset_time(
|
||||
budget_duration=budget_duration
|
||||
)
|
||||
await tx.litellm_budgettable.update(
|
||||
where={"budget_id": existing_budget_id},
|
||||
data=update_data,
|
||||
await _update_existing_member_budget_in_place(
|
||||
tx,
|
||||
existing_budget_id=existing_budget_id,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
max_budget=max_budget,
|
||||
tpm_limit=tpm_limit,
|
||||
rpm_limit=rpm_limit,
|
||||
allowed_models=allowed_models,
|
||||
budget_duration=budget_duration,
|
||||
budget_duration_explicit=budget_duration_explicit,
|
||||
)
|
||||
return
|
||||
|
||||
# Either there is no existing budget, OR the membership is still pointing
|
||||
# at the team's shared default member budget. In both cases we create a
|
||||
# NEW private budget for this user and (re)link the membership to it.
|
||||
create_data: Dict[str, Any] = {
|
||||
"created_by": user_api_key_dict.user_id or "",
|
||||
"updated_by": user_api_key_dict.user_id or "",
|
||||
}
|
||||
|
||||
# If we're forking off the shared default, seed the new row with the
|
||||
# default's values so fields the caller did not change carry over.
|
||||
if is_shared_default:
|
||||
default_budget_row = await tx.litellm_budgettable.find_unique(
|
||||
where={"budget_id": existing_budget_id}
|
||||
assert existing_budget_id is not None
|
||||
await _clone_shared_default_into_create_data(
|
||||
tx, create_data, existing_budget_id=existing_budget_id
|
||||
)
|
||||
if default_budget_row is not None:
|
||||
default_budget_dict = default_budget_row.model_dump()
|
||||
for field in (
|
||||
"max_budget",
|
||||
"soft_budget",
|
||||
"max_parallel_requests",
|
||||
"tpm_limit",
|
||||
"rpm_limit",
|
||||
"model_max_budget",
|
||||
"budget_duration",
|
||||
"allowed_models",
|
||||
):
|
||||
value = default_budget_dict.get(field)
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, list) and len(value) == 0:
|
||||
continue
|
||||
create_data[field] = value
|
||||
|
||||
# Caller-provided values take precedence over the cloned defaults.
|
||||
if max_budget is not None:
|
||||
create_data["max_budget"] = max_budget
|
||||
if tpm_limit is not None:
|
||||
create_data["tpm_limit"] = tpm_limit
|
||||
if rpm_limit is not None:
|
||||
create_data["rpm_limit"] = rpm_limit
|
||||
if allowed_models is not None:
|
||||
create_data["allowed_models"] = allowed_models
|
||||
if budget_duration_explicit:
|
||||
if budget_duration is not None:
|
||||
create_data["budget_duration"] = budget_duration
|
||||
else:
|
||||
create_data.pop("budget_duration", None)
|
||||
create_data.pop("budget_reset_at", None)
|
||||
elif budget_duration is not None:
|
||||
create_data["budget_duration"] = budget_duration
|
||||
|
||||
bd = create_data.get("budget_duration")
|
||||
if bd is not None:
|
||||
create_data["budget_reset_at"] = get_budget_reset_time(budget_duration=bd)
|
||||
else:
|
||||
create_data.pop("budget_reset_at", None)
|
||||
|
||||
new_budget = await tx.litellm_budgettable.create(
|
||||
data=create_data,
|
||||
include={"team_membership": True},
|
||||
_merge_limit_overrides_into_create_data(
|
||||
create_data,
|
||||
max_budget=max_budget,
|
||||
tpm_limit=tpm_limit,
|
||||
rpm_limit=rpm_limit,
|
||||
allowed_models=allowed_models,
|
||||
)
|
||||
await tx.litellm_teammembership.upsert(
|
||||
where={
|
||||
"user_id_team_id": {
|
||||
"user_id": user_id,
|
||||
"team_id": team_id,
|
||||
}
|
||||
},
|
||||
data={
|
||||
"create": {
|
||||
"user_id": user_id,
|
||||
"team_id": team_id,
|
||||
"litellm_budget_table": {
|
||||
"connect": {"budget_id": new_budget.budget_id},
|
||||
},
|
||||
},
|
||||
"update": {
|
||||
"litellm_budget_table": {
|
||||
"connect": {"budget_id": new_budget.budget_id},
|
||||
},
|
||||
},
|
||||
},
|
||||
_merge_duration_into_create_data(
|
||||
create_data, budget_duration, budget_duration_explicit
|
||||
)
|
||||
_set_create_data_budget_reset_from_duration(create_data)
|
||||
await _create_private_budget_and_link_membership(
|
||||
tx, team_id=team_id, user_id=user_id, create_data=create_data
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2614,6 +2614,147 @@ async def team_member_delete(
|
|||
return existing_team_row
|
||||
|
||||
|
||||
def _validate_team_member_update_request(
|
||||
data: TeamMemberUpdateRequest, premium_user: bool
|
||||
) -> None:
|
||||
if data.team_id is None:
|
||||
raise HTTPException(status_code=400, detail={"error": "No team id passed in"})
|
||||
|
||||
if data.role == "admin" and not premium_user:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Assigning team admins is a premium feature. You must be a LiteLLM Enterprise user to use this feature. If you have a license please set `LITELLM_LICENSE` in your env. Get a 7 day trial key here: https://www.litellm.ai/#trial. Pricing: https://www.litellm.ai/#pricing",
|
||||
)
|
||||
if data.user_id is None and data.user_email is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": "Either user_id or user_email needs to be passed in"},
|
||||
)
|
||||
|
||||
|
||||
async def _team_member_update_fetch_team_or_raise(
|
||||
prisma_client: PrismaClient, team_id: str
|
||||
) -> LiteLLM_TeamTable:
|
||||
_existing_team_row = await prisma_client.db.litellm_teamtable.find_unique(
|
||||
where={"team_id": team_id}
|
||||
)
|
||||
if _existing_team_row is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": "Team id={} does not exist in db".format(team_id)},
|
||||
)
|
||||
return LiteLLM_TeamTable(**_existing_team_row.model_dump())
|
||||
|
||||
|
||||
async def _team_member_update_require_authorized(
|
||||
user_api_key_dict: UserAPIKeyAuth, existing_team_row: LiteLLM_TeamTable
|
||||
) -> None:
|
||||
if (
|
||||
user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
|
||||
and not _is_user_team_admin(
|
||||
user_api_key_dict=user_api_key_dict, team_obj=existing_team_row
|
||||
)
|
||||
and not await _is_user_org_admin_for_team(
|
||||
user_api_key_dict=user_api_key_dict, team_obj=existing_team_row
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Call not allowed. User not proxy admin OR team admin. route={}, team_id={}".format(
|
||||
"/team/member_delete", existing_team_row.team_id
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _resolve_team_member_user_id_from_request(
|
||||
data: TeamMemberUpdateRequest, returned_team_info: TeamInfoResponseObject
|
||||
) -> str:
|
||||
if data.user_id is not None:
|
||||
return data.user_id
|
||||
assert data.user_email is not None
|
||||
for member in returned_team_info["team_info"].members_with_roles:
|
||||
if member.user_email is not None and member.user_email == data.user_email:
|
||||
if member.user_id is None:
|
||||
break
|
||||
return member.user_id
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": "User id doesn't exist in team table. Data={}".format(data)},
|
||||
)
|
||||
|
||||
|
||||
def _identified_member_budget_id(
|
||||
team_memberships: List[LiteLLM_TeamMembership], received_user_id: str
|
||||
) -> Optional[str]:
|
||||
for tm in team_memberships:
|
||||
if tm.user_id == received_user_id:
|
||||
return tm.budget_id
|
||||
return None
|
||||
|
||||
|
||||
def _team_default_member_budget_id(
|
||||
team_table: TeamInfoResponseObjectTeamTable,
|
||||
) -> Optional[str]:
|
||||
if team_table.metadata is None:
|
||||
return None
|
||||
raw_default_budget_id = team_table.metadata.get("team_member_budget_id")
|
||||
if isinstance(raw_default_budget_id, str):
|
||||
return raw_default_budget_id
|
||||
return None
|
||||
|
||||
|
||||
async def _effective_member_budget_duration(
|
||||
data: TeamMemberUpdateRequest,
|
||||
prisma_client: PrismaClient,
|
||||
team_default_budget_id: Optional[str],
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
budget_duration_explicit = "budget_duration" in data.model_fields_set
|
||||
if budget_duration_explicit:
|
||||
return budget_duration_explicit, data.budget_duration
|
||||
if team_default_budget_id:
|
||||
_team_budget_row = await prisma_client.db.litellm_budgettable.find_unique(
|
||||
where={"budget_id": team_default_budget_id}
|
||||
)
|
||||
return (
|
||||
budget_duration_explicit,
|
||||
_team_budget_row.budget_duration if _team_budget_row else None,
|
||||
)
|
||||
return budget_duration_explicit, None
|
||||
|
||||
|
||||
async def _team_member_update_apply_role_if_requested(
|
||||
prisma_client: PrismaClient,
|
||||
*,
|
||||
data: TeamMemberUpdateRequest,
|
||||
team_table: TeamInfoResponseObjectTeamTable,
|
||||
received_user_id: str,
|
||||
) -> None:
|
||||
if data.role is None:
|
||||
return
|
||||
team_members: List[Member] = []
|
||||
for member in team_table.members_with_roles:
|
||||
if member.user_id == received_user_id:
|
||||
team_members.append(
|
||||
Member(
|
||||
user_id=member.user_id,
|
||||
role=data.role,
|
||||
user_email=data.user_email or member.user_email,
|
||||
)
|
||||
)
|
||||
else:
|
||||
team_members.append(member)
|
||||
|
||||
team_table.members_with_roles = team_members
|
||||
|
||||
_db_team_members: List[dict] = [m.model_dump() for m in team_members]
|
||||
await prisma_client.db.litellm_teamtable.update(
|
||||
where={"team_id": data.team_id},
|
||||
data={"members_with_roles": json.dumps(_db_team_members)}, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/team/member_update",
|
||||
tags=["team management"],
|
||||
|
|
@ -2636,51 +2777,13 @@ async def team_member_update(
|
|||
if prisma_client is None:
|
||||
raise HTTPException(status_code=500, detail={"error": "No db connected"})
|
||||
|
||||
if data.team_id is None:
|
||||
raise HTTPException(status_code=400, detail={"error": "No team id passed in"})
|
||||
|
||||
if data.role == "admin" and not premium_user:
|
||||
# exactly the same text your proxy throws for add:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Assigning team admins is a premium feature. You must be a LiteLLM Enterprise user to use this feature. If you have a license please set `LITELLM_LICENSE` in your env. Get a 7 day trial key here: https://www.litellm.ai/#trial. Pricing: https://www.litellm.ai/#pricing",
|
||||
)
|
||||
if data.user_id is None and data.user_email is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": "Either user_id or user_email needs to be passed in"},
|
||||
)
|
||||
|
||||
_existing_team_row = await prisma_client.db.litellm_teamtable.find_unique(
|
||||
where={"team_id": data.team_id}
|
||||
_validate_team_member_update_request(data, premium_user)
|
||||
existing_team_row = await _team_member_update_fetch_team_or_raise(
|
||||
prisma_client, data.team_id
|
||||
)
|
||||
await _team_member_update_require_authorized(
|
||||
user_api_key_dict, existing_team_row
|
||||
)
|
||||
|
||||
if _existing_team_row is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": "Team id={} does not exist in db".format(data.team_id)},
|
||||
)
|
||||
existing_team_row = LiteLLM_TeamTable(**_existing_team_row.model_dump())
|
||||
|
||||
## CHECK IF USER IS PROXY ADMIN OR TEAM ADMIN OR ORG ADMIN
|
||||
|
||||
if (
|
||||
user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
|
||||
and not _is_user_team_admin(
|
||||
user_api_key_dict=user_api_key_dict, team_obj=existing_team_row
|
||||
)
|
||||
and not await _is_user_org_admin_for_team(
|
||||
user_api_key_dict=user_api_key_dict, team_obj=existing_team_row
|
||||
)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Call not allowed. User not proxy admin OR team admin. route={}, team_id={}".format(
|
||||
"/team/member_delete", existing_team_row.team_id
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
returned_team_info: TeamInfoResponseObject = await team_info(
|
||||
http_request=http_request,
|
||||
|
|
@ -2689,55 +2792,19 @@ async def team_member_update(
|
|||
)
|
||||
|
||||
team_table = returned_team_info["team_info"]
|
||||
|
||||
## get user id
|
||||
received_user_id: Optional[str] = None
|
||||
if data.user_id is not None:
|
||||
received_user_id = data.user_id
|
||||
elif data.user_email is not None:
|
||||
for member in returned_team_info["team_info"].members_with_roles:
|
||||
if member.user_email is not None and member.user_email == data.user_email:
|
||||
received_user_id = member.user_id
|
||||
break
|
||||
|
||||
if received_user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "User id doesn't exist in team table. Data={}".format(data)
|
||||
},
|
||||
received_user_id = _resolve_team_member_user_id_from_request(
|
||||
data, returned_team_info
|
||||
)
|
||||
identified_budget_id = _identified_member_budget_id(
|
||||
returned_team_info["team_memberships"], received_user_id
|
||||
)
|
||||
team_default_budget_id = _team_default_member_budget_id(team_table)
|
||||
budget_duration_explicit, effective_budget_duration = (
|
||||
await _effective_member_budget_duration(
|
||||
data, prisma_client, team_default_budget_id
|
||||
)
|
||||
## find the relevant team membership
|
||||
identified_budget_id: Optional[str] = None
|
||||
for tm in returned_team_info["team_memberships"]:
|
||||
if tm.user_id == received_user_id:
|
||||
identified_budget_id = tm.budget_id
|
||||
break
|
||||
)
|
||||
|
||||
team_default_budget_id: Optional[str] = None
|
||||
if team_table.metadata is not None:
|
||||
raw_default_budget_id = team_table.metadata.get("team_member_budget_id")
|
||||
if isinstance(raw_default_budget_id, str):
|
||||
team_default_budget_id = raw_default_budget_id
|
||||
|
||||
### resolve effective budget_duration
|
||||
# - Explicit value (including null) takes precedence
|
||||
# - If omitted, inherit the team's configured team_member_budget_duration
|
||||
budget_duration_explicit = "budget_duration" in data.model_fields_set
|
||||
if budget_duration_explicit:
|
||||
effective_budget_duration = data.budget_duration
|
||||
else:
|
||||
if team_default_budget_id:
|
||||
_team_budget_row = await prisma_client.db.litellm_budgettable.find_unique(
|
||||
where={"budget_id": team_default_budget_id}
|
||||
)
|
||||
effective_budget_duration = (
|
||||
_team_budget_row.budget_duration if _team_budget_row else None
|
||||
)
|
||||
else:
|
||||
effective_budget_duration = None
|
||||
|
||||
### upsert new budget
|
||||
async with prisma_client.db.tx() as tx:
|
||||
await _upsert_budget_and_membership(
|
||||
tx=tx,
|
||||
|
|
@ -2754,28 +2821,12 @@ async def team_member_update(
|
|||
budget_duration_explicit=budget_duration_explicit,
|
||||
)
|
||||
|
||||
### update team member role
|
||||
if data.role is not None:
|
||||
team_members: List[Member] = []
|
||||
for member in team_table.members_with_roles:
|
||||
if member.user_id == received_user_id:
|
||||
team_members.append(
|
||||
Member(
|
||||
user_id=member.user_id,
|
||||
role=data.role,
|
||||
user_email=data.user_email or member.user_email,
|
||||
)
|
||||
)
|
||||
else:
|
||||
team_members.append(member)
|
||||
|
||||
team_table.members_with_roles = team_members
|
||||
|
||||
_db_team_members: List[dict] = [m.model_dump() for m in team_members]
|
||||
await prisma_client.db.litellm_teamtable.update(
|
||||
where={"team_id": data.team_id},
|
||||
data={"members_with_roles": json.dumps(_db_team_members)}, # type: ignore
|
||||
)
|
||||
await _team_member_update_apply_role_if_requested(
|
||||
prisma_client,
|
||||
data=data,
|
||||
team_table=team_table,
|
||||
received_user_id=received_user_id,
|
||||
)
|
||||
|
||||
return TeamMemberUpdateResponse(
|
||||
team_id=data.team_id,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue