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

1"""`POST /management/v1/teams/{team_id}/members/bulk_delete` and `.../members/bulk_update`.""" 

2 

3from typing import Annotated, Final 

4 

5from fastapi import APIRouter, Depends, Header 

6 

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) 

24 

25router: Final = APIRouter(prefix=MANAGEMENT_V1_PREFIX) 

26 

27 

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. 

45 

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. 

50 

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 

61 

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 ) 

71 

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) 

81 

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 ) 

98 

99 

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. 

123 

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. 

129 

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. 

134 

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 

145 

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 ) 

155 

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) 

166 

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 )