Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/management_endpoints/management_v1/teams.py: 78%
41 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""`POST /management/v1/teams/{team_id}/members/bulk_delete` and `.../members/bulk_update`."""
3from typing import Annotated, Final
5from fastapi import APIRouter, Depends, Header
7from litellm._logging import verbose_proxy_logger
8from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth
9from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
10from litellm.proxy.list_api.common import PROBLEM_TYPE_BASE, ManagementProblem, reject_unknown_query_params
11from litellm.proxy.management_endpoints.management_v1.common import MANAGEMENT_V1_PREFIX
12from litellm.proxy.management_helpers.bulk_team_member_budgets import bulk_update_team_member_budgets
13from litellm.proxy.management_helpers.bulk_user_deletion import bulk_remove_team_members
14from litellm.proxy.management_helpers.utils import (
15 management_endpoint_wrapper, # pyright: ignore[reportUnknownVariableType] # legacy decorator is untyped
16)
17from litellm.types.proxy.management_endpoints.management_v1 import ProblemDetail
18from litellm.types.proxy.management_endpoints.team_endpoints import (
19 BulkTeamMemberBudgetUpdateRequest,
20 BulkTeamMemberBudgetUpdateResponse,
21 BulkTeamMemberDeleteRequest,
22 BulkTeamMemberDeleteResponse,
23)
25router: Final = APIRouter(prefix=MANAGEMENT_V1_PREFIX)
28@router.post(
29 "/teams/{team_id}/members/bulk_delete",
30 tags=["team management"], # mutable-ok: FastAPI types `tags` as list[str], not Sequence
31 dependencies=(Depends(user_api_key_auth), Depends(reject_unknown_query_params)),
32 response_model=BulkTeamMemberDeleteResponse,
33)
34@management_endpoint_wrapper
35async def bulk_delete_team_members_action(
36 team_id: str,
37 data: BulkTeamMemberDeleteRequest,
38 user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
39) -> BulkTeamMemberDeleteResponse:
40 """
41 Remove up to 500 members from one team in one call. Same authorization as
42 `/team/member_delete`: proxy admins, the team's admins, and admins of the team's
43 organization. Each member is named by exactly one of `user_id` or `user_email`;
44 unknown body fields are a 422 and an unknown team is a 404.
46 `data` holds one result per requested member, in request order. A row is
47 `success: false` with an `error` when it names nobody on the team or repeats an
48 earlier row. The roster is rewritten once, under the team's advisory lock, so a
49 concurrent member_add is never overwritten from a stale read.
51 Example curl:
52 ```
53 curl --location 'http://0.0.0.0:4000/management/v1/teams/team-1/members/bulk_delete' \
54 --header 'Authorization: Bearer sk-1234' \
55 --header 'Content-Type: application/json' \
56 --data '{"members": [{"user_id": "user-1"}, {"user_email": "user-2@example.com"}]}'
57 ```
58 """
59 try:
60 from litellm.proxy.proxy_server import prisma_client, proxy_logging_obj, user_api_key_cache
62 if prisma_client is None: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 raise ManagementProblem(
64 ProblemDetail(
65 type=f"{PROBLEM_TYPE_BASE}database-not-connected",
66 title="Database not connected",
67 status=503,
68 detail=CommonProxyErrors.db_not_connected_error.value,
69 )
70 )
72 results: Final = await bulk_remove_team_members(
73 team_id=team_id,
74 data=data,
75 user_api_key_dict=user_api_key_dict,
76 prisma_client=prisma_client,
77 user_api_key_cache=user_api_key_cache,
78 proxy_logging_obj=proxy_logging_obj,
79 )
80 return BulkTeamMemberDeleteResponse(data=results)
82 except ManagementProblem:
83 raise
84 except Exception as e: # noqa: BLE001 # a driver error answers as a problem document, not the OpenAI error shape
85 verbose_proxy_logger.exception(
86 "litellm.proxy.management_endpoints.management_v1.teams.bulk_delete_team_members_action(): "
87 "Exception occured - %s",
88 e,
89 )
90 raise ManagementProblem(
91 ProblemDetail(
92 type=f"{PROBLEM_TYPE_BASE}internal-server-error",
93 title="Internal server error",
94 status=500,
95 detail="Failed to remove team members.",
96 )
97 )
100@router.post(
101 "/teams/{team_id}/members/bulk_update",
102 tags=["team management"], # mutable-ok: FastAPI types `tags` as list[str], not Sequence
103 dependencies=(Depends(user_api_key_auth), Depends(reject_unknown_query_params)),
104 response_model=BulkTeamMemberBudgetUpdateResponse,
105)
106@management_endpoint_wrapper
107async def bulk_update_team_member_budgets_action(
108 team_id: str,
109 data: BulkTeamMemberBudgetUpdateRequest,
110 user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
111 litellm_changed_by: Annotated[
112 str | None,
113 Header(
114 description="The litellm-changed-by header enables tracking of actions performed by authorized users on behalf of other users, providing an audit trail for accountability",
115 ),
116 ] = None,
117) -> BulkTeamMemberBudgetUpdateResponse:
118 """
119 Set per-member limits for up to 500 members of one team in one call. Same
120 authorization and member addressing as `/team/member_update`: proxy admins, the team's
121 admins, and admins of the team's organization, with each member named by exactly one of
122 `user_id` or `user_email`. Unknown body fields are a 422 and an unknown team is a 404.
124 Each row is a merge patch of that member's limits: a field left out is untouched, a
125 field sent as null is cleared, and clearing the last limit drops the member back to the
126 team default. A budget row shared by several memberships, the team default included, is
127 copied for the member being patched rather than written in place, so one member's new
128 cap never lands on anybody else.
130 `data` holds one result per requested member, in request order, carrying the limits in
131 force after the write. A row is `success: false` with an `error` when it names nobody on
132 the team or repeats an earlier row. Roles are not part of this route; `/team/member_update`
133 still owns them.
135 Example curl:
136 ```
137 curl --location 'http://0.0.0.0:4000/management/v1/teams/team-1/members/bulk_update' \
138 --header 'Authorization: Bearer sk-1234' \
139 --header 'Content-Type: application/json' \
140 --data '{"members": [{"user_id": "user-1", "max_budget_in_team": 10}, {"user_email": "user-2@example.com", "max_budget_in_team": 10, "budget_duration": "30d"}]}'
141 ```
142 """
143 try:
144 from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client, user_api_key_cache
146 if prisma_client is None: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true
147 raise ManagementProblem(
148 ProblemDetail(
149 type=f"{PROBLEM_TYPE_BASE}database-not-connected",
150 title="Database not connected",
151 status=503,
152 detail=CommonProxyErrors.db_not_connected_error.value,
153 )
154 )
156 results: Final = await bulk_update_team_member_budgets(
157 team_id=team_id,
158 data=data,
159 user_api_key_dict=user_api_key_dict,
160 prisma_client=prisma_client,
161 user_api_key_cache=user_api_key_cache,
162 litellm_proxy_admin_name=litellm_proxy_admin_name,
163 litellm_changed_by=litellm_changed_by,
164 )
165 return BulkTeamMemberBudgetUpdateResponse(data=results)
167 except ManagementProblem:
168 raise
169 except Exception as e: # noqa: BLE001 # a driver error answers as a problem document, not the OpenAI error shape
170 verbose_proxy_logger.exception(
171 "litellm.proxy.management_endpoints.management_v1.teams.bulk_update_team_member_budgets_action(): "
172 "Exception occured - %s",
173 e,
174 )
175 raise ManagementProblem(
176 ProblemDetail(
177 type=f"{PROBLEM_TYPE_BASE}internal-server-error",
178 title="Internal server error",
179 status=500,
180 detail="Failed to update team member budgets.",
181 )
182 )