Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/management_endpoints/customer_endpoints.py: 79%

256 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1""" 

2CUSTOMER MANAGEMENT 

3 

4All /customer management endpoints 

5 

6/customer/new 

7/customer/info 

8/customer/update 

9/customer/delete 

10""" 

11 

12#### END-USER/CUSTOMER MANAGEMENT #### 

13from collections.abc import Mapping, Sequence 

14from datetime import datetime, timedelta 

15from typing import TYPE_CHECKING, Final, Protocol, TypeVar, overload 

16 

17import fastapi 

18from fastapi import APIRouter, Depends, HTTPException, Request 

19from pydantic import BaseModel, TypeAdapter 

20 

21if TYPE_CHECKING: 21 ↛ 22line 21 didn't jump to line 22 because the condition on line 21 was never true

22 from prisma.models import LiteLLM_BudgetTable as PrismaBudgetRow 

23 from prisma.models import LiteLLM_EndUserTable as PrismaEndUserRow 

24 

25 from litellm.proxy.utils import PrismaClient 

26 

27import litellm 

28from litellm._logging import verbose_proxy_logger 

29from litellm.litellm_core_utils.duration_parser import duration_in_seconds 

30from litellm.proxy._types import * 

31from litellm.proxy.auth.user_api_key_auth import user_api_key_auth 

32from litellm.proxy.common_utils.user_api_key_cache import ( 

33 end_user_cache_key, 

34 end_user_restricted_registry_cache_key, 

35) 

36from litellm.proxy.management_endpoints.common_daily_activity import get_daily_activity 

37from litellm.proxy.management_endpoints.common_utils import validate_budget_duration 

38from litellm.proxy.management_helpers.object_permission_utils import ( 

39 _set_object_permission, 

40 handle_update_object_permission_common, 

41) 

42from litellm.proxy.utils import handle_exception_on_proxy 

43from litellm.repositories.budget_repository import BudgetRepository 

44from litellm.repositories.table_repositories import EndUserRepository 

45from litellm.types.proxy.management_endpoints.common_daily_activity import ( 

46 SpendAnalyticsPaginatedResponse, 

47) 

48from litellm.types.proxy.management_endpoints.customer_endpoints import ( 

49 BlockUsersResponse, 

50 CustomerResponse, 

51 DeleteCustomersResponse, 

52 UnblockUsersResponse, 

53) 

54 

55_RowT_co: Final = TypeVar("_RowT_co", covariant=True) 

56_STR_OBJECT_DICT: Final = TypeAdapter(dict[str, object]) 

57 

58if TYPE_CHECKING: 58 ↛ 60line 58 didn't jump to line 60 because the condition on line 58 was never true

59 

60 class _TableOps(Protocol[_RowT_co]): 

61 async def find_first( 

62 self, 

63 where: Mapping[str, object] | None = None, 

64 include: Mapping[str, bool] | None = None, 

65 ) -> _RowT_co | None: ... 

66 

67 async def find_many( 

68 self, 

69 where: Mapping[str, object] | None = None, 

70 include: Mapping[str, bool] | None = None, 

71 ) -> Sequence[_RowT_co]: ... 

72 

73 async def create( 

74 self, 

75 data: Mapping[str, object], 

76 include: Mapping[str, bool] | None = None, 

77 ) -> _RowT_co: ... 

78 

79 async def update( 

80 self, 

81 where: Mapping[str, object], 

82 data: Mapping[str, object], 

83 include: Mapping[str, bool] | None = None, 

84 ) -> _RowT_co | None: ... 

85 

86 async def upsert( 

87 self, 

88 where: Mapping[str, object], 

89 data: Mapping[str, Mapping[str, object]], 

90 ) -> _RowT_co: ... 

91 

92 async def delete_many(self, where: Mapping[str, object]) -> int: ... 

93 

94 

95@overload 

96def _typed_table(repo: EndUserRepository) -> "_TableOps[PrismaEndUserRow]": ... 96 ↛ exitline 96 didn't return from function '_typed_table' because

97@overload 

98def _typed_table(repo: BudgetRepository) -> "_TableOps[PrismaBudgetRow]": ... 98 ↛ exitline 98 didn't return from function '_typed_table' because

99def _typed_table(repo: EndUserRepository | BudgetRepository) -> object: 

100 return repo.table 

101 

102 

103router: Final = APIRouter() 

104 

105 

106async def _evict_end_user_cache_keys(cache_keys: Sequence[str]) -> None: 

107 """ 

108 Every endpoint that mutates an end-user row must call this, or a newly blocked or budgeted 

109 customer keeps being served unrestricted until the TTL expires: auth reads end users 

110 cache-first, and the cached restricted-id registry decides whether the row is read at all. 

111 """ 

112 from litellm.proxy.common_utils.auth_cache_invalidation_pubsub import ( 

113 evict_and_broadcast, 

114 ) 

115 from litellm.proxy.proxy_server import user_api_key_cache 

116 

117 await evict_and_broadcast(cache_keys=cache_keys, user_api_key_cache=user_api_key_cache) 

118 

119 

120def _end_user_cache_keys(user_ids: Sequence[str]) -> tuple[str, ...]: 

121 """The per-id entries plus the registry, which any restriction change can move ids in or out of.""" 

122 return (*(end_user_cache_key(user_id) for user_id in user_ids), end_user_restricted_registry_cache_key()) 

123 

124 

125def _to_customer_response(record: BaseModel) -> CustomerResponse: 

126 """Validate a raw end-user DB row into the typed customer response. 

127 

128 object_permission reverse relations and the budget's audit fields are 

129 dropped here by the response model's field set, so callers need no manual 

130 cleanup. 

131 """ 

132 return CustomerResponse.model_validate(record.model_dump()) 

133 

134 

135@router.post( 

136 "/end_user/block", 

137 tags=["Customer Management"], 

138 dependencies=[Depends(user_api_key_auth)], 

139 include_in_schema=False, 

140) 

141@router.post( 

142 "/customer/block", 

143 tags=["Customer Management"], 

144 dependencies=[Depends(user_api_key_auth)], 

145 response_model=BlockUsersResponse, 

146) 

147async def block_user(data: BlockUsers): 

148 """ 

149 [BETA] Reject calls with this end-user id 

150 

151 Parameters: 

152 - user_ids (List[str], required): The unique `user_id`s for the users to block 

153 

154 (any /chat/completion call with this user={end-user-id} param, will be rejected.) 

155 

156 ``` 

157 curl -X POST "http://0.0.0.0:8000/user/block" 

158 -H "Authorization: Bearer sk-1234" 

159 -d '{ 

160 "user_ids": [<user_id>, ...] 

161 }' 

162 ``` 

163 """ 

164 from litellm.proxy.proxy_server import prisma_client 

165 

166 try: 

167 records: Final = [] 

168 if prisma_client is not None: 168 ↛ 180line 168 didn't jump to line 180 because the condition on line 168 was always true

169 for id in data.user_ids: 

170 record = await _typed_table(EndUserRepository(prisma_client)).upsert( 

171 where={"user_id": id}, 

172 data={ 

173 "create": {"user_id": id, "blocked": True}, 

174 "update": {"blocked": True}, 

175 }, 

176 ) 

177 records.append(record) 

178 await _evict_end_user_cache_keys(_end_user_cache_keys(data.user_ids)) 

179 else: 

180 raise HTTPException( 

181 status_code=500, 

182 detail={"error": "Postgres DB Not connected"}, 

183 ) 

184 

185 return {"blocked_users": records} 

186 except Exception as e: 

187 verbose_proxy_logger.error("An error occurred - %s", e) 

188 raise HTTPException(status_code=500, detail={"error": str(e)}) 

189 

190 

191@router.post( 

192 "/end_user/unblock", 

193 tags=["Customer Management"], 

194 dependencies=[Depends(user_api_key_auth)], 

195 include_in_schema=False, 

196) 

197@router.post( 

198 "/customer/unblock", 

199 tags=["Customer Management"], 

200 dependencies=[Depends(user_api_key_auth)], 

201 response_model=UnblockUsersResponse, 

202) 

203async def unblock_user(data: BlockUsers): 

204 """ 

205 [BETA] Unblock calls with this user id 

206 

207 Example 

208 ``` 

209 curl -X POST "http://0.0.0.0:8000/user/unblock" 

210 -H "Authorization: Bearer sk-1234" 

211 -d '{ 

212 "user_ids": [<user_id>, ...] 

213 }' 

214 ``` 

215 """ 

216 try: 

217 from enterprise.enterprise_hooks.blocked_user_list import ( 

218 _ENTERPRISE_BlockedUserList, 

219 ) 

220 except ImportError: 

221 raise HTTPException( 

222 status_code=400, 

223 detail={ 

224 "error": "Blocked user check was never set. This call has no effect." 

225 + CommonProxyErrors.missing_enterprise_package_docker.value 

226 }, 

227 ) 

228 

229 if ( 229 ↛ 238line 229 didn't jump to line 238 because the condition on line 229 was always true

230 not any(isinstance(x, _ENTERPRISE_BlockedUserList) for x in litellm.callbacks) 

231 or litellm.blocked_user_list is None 

232 ): 

233 raise HTTPException( 

234 status_code=400, 

235 detail={"error": "Blocked user check was never set. This call has no effect."}, 

236 ) 

237 

238 if isinstance(litellm.blocked_user_list, list): 

239 for id in data.user_ids: 

240 litellm.blocked_user_list.remove(id) 

241 else: 

242 raise HTTPException( 

243 status_code=500, 

244 detail={"error": "`blocked_user_list` must be set as a list. Filepaths can't be updated."}, 

245 ) 

246 

247 return {"blocked_users": litellm.blocked_user_list} 

248 

249 

250def new_budget_request(data: NewCustomerRequest) -> BudgetNewRequest | None: 

251 """ 

252 Return a new budget object if new budget params are passed. 

253 """ 

254 budget_params: Final = BudgetNewRequest.model_fields.keys() 

255 budget_kv_pairs: Final = {} 

256 

257 # Get the actual values from the data object using getattr 

258 for field_name in budget_params: 

259 if field_name == "budget_id": 

260 continue 

261 value = getattr(data, field_name, None) 

262 if value is not None: 

263 budget_kv_pairs[field_name] = value 

264 

265 if budget_kv_pairs: 

266 budget_request: Final = BudgetNewRequest.model_validate(budget_kv_pairs) 

267 validate_budget_duration(budget_request.budget_duration) 

268 if budget_request.budget_reset_at is None and budget_request.budget_duration is not None: 268 ↛ 269line 268 didn't jump to line 269 because the condition on line 268 was never true

269 budget_request.budget_reset_at = datetime.utcnow() + timedelta( 

270 seconds=duration_in_seconds(duration=budget_request.budget_duration) 

271 ) 

272 return budget_request 

273 return None 

274 

275 

276async def _handle_customer_object_permission_update( 

277 non_default_values: dict[str, object], 

278 end_user_table_data_typed: LiteLLM_EndUserTable | None, 

279 update_end_user_table_data: dict[str, object], 

280 prisma_client: "PrismaClient", 

281) -> None: 

282 """ 

283 Handle object permission updates for customer endpoints. 

284 

285 Updates the update_end_user_table_data dict in place with the new object_permission_id. 

286 

287 Args: 

288 non_default_values: Dictionary containing the update values including object_permission 

289 end_user_table_data_typed: Existing end user table data 

290 update_end_user_table_data: Dictionary to update with new object_permission_id 

291 prisma_client: Prisma database client 

292 """ 

293 if "object_permission" in non_default_values: 

294 existing_object_permission_id: Final = ( 

295 end_user_table_data_typed.object_permission_id if end_user_table_data_typed is not None else None 

296 ) 

297 object_permission_id: Final = await handle_update_object_permission_common( 

298 data_json=non_default_values, 

299 existing_object_permission_id=existing_object_permission_id, 

300 prisma_client=prisma_client, 

301 ) 

302 if object_permission_id is not None: 302 ↛ exitline 302 didn't return from function '_handle_customer_object_permission_update' because the condition on line 302 was always true

303 update_end_user_table_data["object_permission_id"] = object_permission_id 

304 

305 

306@router.post( 

307 "/end_user/new", 

308 tags=["Customer Management"], 

309 include_in_schema=False, 

310 dependencies=[Depends(user_api_key_auth)], 

311) 

312@router.post( 

313 "/customer/new", 

314 tags=["Customer Management"], 

315 dependencies=[Depends(user_api_key_auth)], 

316 response_model=CustomerResponse, 

317) 

318async def new_end_user( 

319 data: NewCustomerRequest, 

320 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

321) -> CustomerResponse: 

322 """ 

323 Allow creating a new Customer  

324 

325 

326 Parameters: 

327 - user_id: str - The unique identifier for the user. 

328 - alias: Optional[str] - A human-friendly alias for the user. 

329 - blocked: bool - Flag to allow or disallow requests for this end-user. Default is False. 

330 - max_budget: Optional[float] - The maximum budget allocated to the user. Either 'max_budget' or 'budget_id' should be provided, not both. 

331 - budget_id: Optional[str] - The identifier for an existing budget allocated to the user. Either 'max_budget' or 'budget_id' should be provided, not both. 

332 - allowed_model_region: Optional[Union[Literal["eu"], Literal["us"]]] - Require all user requests to use models in this specific region. 

333 - default_model: Optional[str] - If no equivalent model in the allowed region, default all requests to this model. 

334 - metadata: Optional[dict] = Metadata for customer, store information for customer. Example metadata = {"data_training_opt_out": True} 

335 - budget_duration: Optional[str] - Budget is reset at the end of specified duration. If not set, budget is never reset. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). 

336 - tpm_limit: Optional[int] - [Not Implemented Yet] Specify tpm limit for a given customer (Tokens per minute) 

337 - rpm_limit: Optional[int] - [Not Implemented Yet] Specify rpm limit for a given customer (Requests per minute) 

338 - tpd_limit: Optional[int] - Specify tpd limit for a given customer (Tokens per day). Batch submissions are charged against it instead of tpm_limit/rpm_limit 

339 - model_max_budget: Optional[dict] - [Not Implemented Yet] Specify max budget for a given model. Example: {"openai/gpt-4o-mini": {"max_budget": 100.0, "budget_duration": "1d"}} 

340 - max_parallel_requests: Optional[int] - [Not Implemented Yet] Specify max parallel requests for a given customer. 

341 - soft_budget: Optional[float] - [Not Implemented Yet] Get alerts when customer crosses given budget, doesn't block requests. 

342 - spend: Optional[float] - Specify initial spend for a given customer. 

343 - budget_reset_at: Optional[str] - Specify the date and time when the budget should be reset. 

344 - object_permission: Optional[LiteLLM_ObjectPermissionBase] - Customer-specific object permissions to control access to resources. 

345 Supported fields: 

346 * mcp_servers: List[str] - List of allowed MCP server IDs 

347 * mcp_access_groups: List[str] - List of MCP access group names 

348 * mcp_tool_permissions: Dict[str, List[str]] - Map of server ID to allowed tool names (e.g., {"server_1": ["tool_a", "tool_b"]}) 

349 * vector_stores: List[str] - List of allowed vector store IDs 

350 * agents: List[str] - List of allowed agent IDs 

351 * agent_access_groups: List[str] - List of agent access group names 

352 Example: {"mcp_servers": ["server_1", "server_2"], "vector_stores": ["vector_store_1"], "agents": ["agent_1"]} 

353 IF null or {} then no object-level restrictions apply. 

354  

355  

356 - Allow specifying allowed regions  

357 - Allow specifying default model 

358 

359 Example curl: 

360 ``` 

361 curl --location 'http://0.0.0.0:4000/customer/new' \ 

362 --header 'Authorization: Bearer sk-1234' \ 

363 --header 'Content-Type: application/json' \ 

364 --data '{ 

365 "user_id" : "ishaan-jaff-3", 

366 "allowed_region": "eu", 

367 "budget_id": "free_tier", 

368 "default_model": "azure/gpt-3.5-turbo-eu" 

369 }' 

370 

371 # With object permissions 

372 curl -L -X POST 'http://localhost:4000/customer/new' \ 

373 -H 'Authorization: Bearer sk-1234' \ 

374 -H 'Content-Type: application/json' \ 

375 -d '{ 

376 "user_id": "user_1", 

377 "object_permission": { 

378 "mcp_servers": ["server_1"], 

379 "mcp_access_groups": ["public_group"], 

380 "vector_stores": ["vector_store_1"] 

381 } 

382 }' 

383 

384 # return end-user object 

385 ``` 

386 

387 NOTE: This used to be called `/end_user/new`, we will still be maintaining compatibility for /end_user/XXX for these endpoints 

388 """ 

389 """ 

390 Validation: 

391 - check if default model exists  

392 - create budget object if not already created 

393  

394 - Add user to end user table  

395 

396 Return  

397 - end-user object 

398 - currently allowed models  

399 """ 

400 from litellm.proxy.proxy_server import ( 

401 litellm_proxy_admin_name, 

402 llm_router, 

403 prisma_client, 

404 ) 

405 

406 if prisma_client is None: 406 ↛ 407line 406 didn't jump to line 407 because the condition on line 406 was never true

407 raise HTTPException( 

408 status_code=500, 

409 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

410 ) 

411 try: 

412 ## VALIDATION ## 

413 if data.default_model is not None: 

414 if llm_router is None: 414 ↛ 415line 414 didn't jump to line 415 because the condition on line 414 was never true

415 raise HTTPException( 

416 status_code=422, 

417 detail={"error": CommonProxyErrors.no_llm_router.value}, 

418 ) 

419 elif data.default_model not in llm_router.get_model_names(): 419 ↛ 427line 419 didn't jump to line 427 because the condition on line 419 was always true

420 raise HTTPException( 

421 status_code=422, 

422 detail={ 

423 "error": f"Default Model not on proxy. Configure via `/model/new` or config.yaml. Default_model={data.default_model}, proxy_model_names={set(llm_router.get_model_names())}" 

424 }, 

425 ) 

426 

427 new_end_user_obj: dict[str, object] = {} 

428 

429 ## CREATE BUDGET ## if set 

430 _new_budget: Final = new_budget_request(data) 

431 if _new_budget is not None: 

432 try: 

433 budget_record: Final = await _typed_table(BudgetRepository(prisma_client)).create( 

434 data={ 

435 **_new_budget.model_dump(exclude_unset=True), 

436 "created_by": user_api_key_dict.user_id or litellm_proxy_admin_name, 

437 "updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name, 

438 } 

439 ) 

440 except Exception as e: 

441 raise HTTPException(status_code=422, detail={"error": str(e)}) 

442 

443 new_end_user_obj["budget_id"] = budget_record.budget_id 

444 elif data.budget_id is not None: 444 ↛ 445line 444 didn't jump to line 445 because the condition on line 444 was never true

445 new_end_user_obj["budget_id"] = data.budget_id 

446 

447 _user_data: Final = _STR_OBJECT_DICT.validate_python(data.dict(exclude_none=True)) 

448 

449 for k, v in _user_data.items(): 

450 if k not in BudgetNewRequest.model_fields: 

451 new_end_user_obj[k] = v 

452 

453 ## Handle Object Permission - MCP Servers, Vector Stores etc. 

454 new_end_user_obj = _STR_OBJECT_DICT.validate_python( 

455 await _set_object_permission( 

456 data_json=new_end_user_obj, 

457 prisma_client=prisma_client, 

458 ) 

459 ) 

460 

461 # Ensure object_permission is not in the data being sent to create 

462 # It should have been converted to object_permission_id by _set_object_permission 

463 if "object_permission" in new_end_user_obj: 463 ↛ 464line 463 didn't jump to line 464 because the condition on line 463 was never true

464 verbose_proxy_logger.warning( 

465 "object_permission still in new_end_user_obj after _set_object_permission: %s", 

466 new_end_user_obj.get("object_permission"), 

467 ) 

468 new_end_user_obj.pop("object_permission", None) 

469 

470 ## WRITE TO DB ## 

471 end_user_record: Final = await _typed_table(EndUserRepository(prisma_client)).create( 

472 data=new_end_user_obj, 

473 include={"litellm_budget_table": True, "object_permission": True}, 

474 ) 

475 

476 await _evict_end_user_cache_keys(_end_user_cache_keys((data.user_id,))) 

477 

478 return _to_customer_response(end_user_record) 

479 except Exception as e: 

480 verbose_proxy_logger.exception( 

481 "litellm.proxy.management_endpoints.customer_endpoints.new_end_user(): Exception occured - %s", e 

482 ) 

483 if "Unique constraint failed on the fields: (`user_id`)" in str(e): 

484 raise ProxyException( 

485 message=f"Customer already exists, passed user_id={data.user_id}. Please pass a new user_id.", 

486 type="bad_request", 

487 code=400, 

488 param="user_id", 

489 ) 

490 raise handle_exception_on_proxy(e) 

491 

492 

493@router.get( 

494 "/customer/info", 

495 tags=["Customer Management"], 

496 dependencies=[Depends(user_api_key_auth)], 

497 response_model=CustomerResponse, 

498) 

499@router.get( 

500 "/end_user/info", 

501 tags=["Customer Management"], 

502 include_in_schema=False, 

503 dependencies=[Depends(user_api_key_auth)], 

504) 

505async def end_user_info( 

506 end_user_id: str = fastapi.Query(description="End User ID in the request parameters"), 

507) -> CustomerResponse: 

508 """ 

509 Get information about an end-user. An `end_user` is a customer (external user) of the proxy. 

510 

511 Parameters: 

512 - end_user_id (str, required): The unique identifier for the end-user 

513 

514 Example curl: 

515 ``` 

516 curl -X GET 'http://localhost:4000/customer/info?end_user_id=test-litellm-user-4' \ 

517 -H 'Authorization: Bearer sk-1234' 

518 ``` 

519 """ 

520 try: 

521 from litellm.proxy.proxy_server import prisma_client 

522 

523 if prisma_client is None: 523 ↛ 524line 523 didn't jump to line 524 because the condition on line 523 was never true

524 raise HTTPException( 

525 status_code=500, 

526 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

527 ) 

528 

529 user_info: Final = await _typed_table(EndUserRepository(prisma_client)).find_first( 

530 where={"user_id": end_user_id}, 

531 include={"litellm_budget_table": True, "object_permission": True}, 

532 ) 

533 

534 if user_info is None: 

535 raise ProxyException( 

536 message=f"End User Id={end_user_id} does not exist in db", 

537 type="not_found", 

538 code=404, 

539 param="end_user_id", 

540 ) 

541 

542 return _to_customer_response(user_info) 

543 

544 except Exception as e: 

545 verbose_proxy_logger.exception( 

546 "litellm.proxy.management_endpoints.customer_endpoints.end_user_info(): Exception occured - %s", e 

547 ) 

548 raise handle_exception_on_proxy(e) 

549 

550 

551@router.post( 

552 "/customer/update", 

553 tags=["Customer Management"], 

554 dependencies=[Depends(user_api_key_auth)], 

555 response_model=CustomerResponse, 

556) 

557@router.post( 

558 "/end_user/update", 

559 tags=["Customer Management"], 

560 include_in_schema=False, 

561 dependencies=[Depends(user_api_key_auth)], 

562) 

563async def update_end_user( 

564 data: UpdateCustomerRequest, 

565 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

566) -> CustomerResponse: 

567 """ 

568 Example curl  

569 

570 Parameters: 

571 - user_id: str 

572 - alias: Optional[str] = None # human-friendly alias 

573 - blocked: bool = False # allow/disallow requests for this end-user 

574 - max_budget: Optional[float] = None 

575 - budget_id: Optional[str] = None # give either a budget_id or max_budget 

576 - allowed_model_region: Optional[AllowedModelRegion] = ( 

577 None # require all user requests to use models in this specific region 

578 ) 

579 - default_model: Optional[str] = ( 

580 None # if no equivalent model in allowed region - default all requests to this model 

581 ) 

582 - object_permission: Optional[LiteLLM_ObjectPermissionBase] - Customer-specific object permissions to control access to resources. 

583 Supported fields: 

584 * mcp_servers: List[str] - List of allowed MCP server IDs 

585 * mcp_access_groups: List[str] - List of MCP access group names 

586 * mcp_tool_permissions: Dict[str, List[str]] - Map of server ID to allowed tool names 

587 * vector_stores: List[str] - List of allowed vector store IDs 

588 * agents: List[str] - List of allowed agent IDs 

589 * agent_access_groups: List[str] - List of agent access group names 

590 Example: {"mcp_servers": ["server_1"], "vector_stores": ["vector_store_1"]} 

591 IF null or {} then no object-level restrictions apply. 

592 

593 Example curl: 

594 ``` 

595 curl --location 'http://0.0.0.0:4000/customer/update' \ 

596 --header 'Authorization: Bearer sk-1234' \ 

597 --header 'Content-Type: application/json' \ 

598 --data '{ 

599 "user_id": "test-litellm-user-4", 

600 "budget_id": "paid_tier" 

601 }' 

602 

603 # Updating object permissions 

604 curl -L -X POST 'http://localhost:4000/customer/update' \ 

605 --header 'Authorization: Bearer sk-1234' \ 

606 --header 'Content-Type: application/json' \ 

607 --data '{ 

608 "user_id": "user_1", 

609 "object_permission": { 

610 "mcp_servers": ["server_3"], 

611 "vector_stores": ["vector_store_2", "vector_store_3"] 

612 } 

613 }' 

614 

615 See below for all params 

616 ``` 

617 """ 

618 

619 from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client 

620 

621 try: 

622 data_json: Final = _STR_OBJECT_DICT.validate_python(data.json()) 

623 # get the row from db 

624 if prisma_client is None: 624 ↛ 625line 624 didn't jump to line 625 because the condition on line 624 was never true

625 raise Exception("Not connected to DB!") 

626 

627 # get non default values for key 

628 non_default_values: Final = dict[str, object]() 

629 for k, v in data_json.items(): 

630 if v is not None and ((isinstance(v, bool) and k in data.fields_set()) or v not in ([], {}, 0)): 

631 non_default_values[k] = v 

632 

633 ## Get end user table data ## 

634 end_user_table_data: Final = await _typed_table(EndUserRepository(prisma_client)).find_first( 

635 where={"user_id": data.user_id}, include={"litellm_budget_table": True} 

636 ) 

637 

638 if end_user_table_data is None: 

639 raise ProxyException( 

640 message=f"End User Id={data.user_id} does not exist in db", 

641 type="not_found", 

642 code=404, 

643 param="user_id", 

644 ) 

645 

646 end_user_table_data_typed: Final = LiteLLM_EndUserTable.model_validate(end_user_table_data.model_dump()) 

647 

648 ## Get budget table data ## 

649 end_user_budget_table: Final = end_user_table_data_typed.litellm_budget_table 

650 

651 ## Get all params for budget table ## 

652 budget_table_data: Final = dict[str, object]() 

653 update_end_user_table_data: Final = dict[str, object]() 

654 for k, v in non_default_values.items(): 

655 # budget_id is for linking to existing budget, not for creating new budget 

656 if k == "budget_id": 

657 update_end_user_table_data[k] = v 

658 elif k in LiteLLM_BudgetTable.model_fields: 

659 budget_table_data[k] = v 

660 

661 elif k in LiteLLM_EndUserTable.model_fields: 661 ↛ 654line 661 didn't jump to line 654 because the condition on line 661 was always true

662 update_end_user_table_data[k] = v 

663 

664 ## Handle object permission updates (MCP servers, vector stores, etc.) 

665 await _handle_customer_object_permission_update( 

666 non_default_values=non_default_values, 

667 end_user_table_data_typed=end_user_table_data_typed, 

668 update_end_user_table_data=update_end_user_table_data, 

669 prisma_client=prisma_client, 

670 ) 

671 

672 ## Check if we need to create a new budget (only if budget fields are provided, not just budget_id) ## 

673 if budget_table_data: 

674 if end_user_budget_table is None: 674 ↛ 688line 674 didn't jump to line 688 because the condition on line 674 was always true

675 ## Create new budget ## 

676 budget_table_data_record = await _typed_table(BudgetRepository(prisma_client)).create( 

677 data={ 

678 **budget_table_data, 

679 "created_by": user_api_key_dict.user_id or litellm_proxy_admin_name, 

680 "updated_by": user_api_key_dict.user_id or litellm_proxy_admin_name, 

681 }, 

682 include={"end_users": True}, 

683 ) 

684 

685 update_end_user_table_data["budget_id"] = budget_table_data_record.budget_id 

686 else: 

687 ## Update existing budget ## 

688 budget_table_data_record = await _typed_table(BudgetRepository(prisma_client)).update( 

689 where={"budget_id": end_user_budget_table.budget_id}, 

690 data=budget_table_data, 

691 ) 

692 

693 ## Update user table, with update params + new budget id (if set) ## 

694 verbose_proxy_logger.debug("/customer/update: Received data = %s", data) 

695 

696 # Ensure object_permission is not in the update data 

697 # It should have been converted to object_permission_id by handle_update_object_permission_common 

698 if "object_permission" in update_end_user_table_data: 

699 verbose_proxy_logger.warning( 

700 "object_permission still in update_end_user_table_data: %s", 

701 update_end_user_table_data.get("object_permission"), 

702 ) 

703 update_end_user_table_data.pop("object_permission", None) 

704 

705 if data.user_id is not None and len(data.user_id) > 0: 

706 update_end_user_table_data["user_id"] = data.user_id 

707 verbose_proxy_logger.debug("In update customer, user_id condition block.") 

708 response: Final = await _typed_table(EndUserRepository(prisma_client)).update( 

709 where={"user_id": data.user_id}, 

710 data=update_end_user_table_data, 

711 include={"litellm_budget_table": True, "object_permission": True}, 

712 ) 

713 if response is None: 713 ↛ 714line 713 didn't jump to line 714 because the condition on line 713 was never true

714 raise ValueError(f"Failed updating customer data. User ID does not exist passed user_id={data.user_id}") 

715 verbose_proxy_logger.debug("received response from updating prisma client. response=%s", response) 

716 

717 await _evict_end_user_cache_keys(_end_user_cache_keys((data.user_id,))) 

718 

719 return _to_customer_response(response) 

720 else: 

721 raise ValueError(f"user_id is required, passed user_id = {data.user_id}") 

722 

723 # update based on remaining passed in values 

724 

725 except Exception as e: 

726 verbose_proxy_logger.exception("litellm.proxy.proxy_server.update_end_user(): Exception occured - %s", e) 

727 raise handle_exception_on_proxy(e) 

728 

729 

730@router.post( 

731 "/customer/delete", 

732 tags=["Customer Management"], 

733 dependencies=[Depends(user_api_key_auth)], 

734 response_model=DeleteCustomersResponse, 

735) 

736@router.post( 

737 "/end_user/delete", 

738 tags=["Customer Management"], 

739 include_in_schema=False, 

740 dependencies=[Depends(user_api_key_auth)], 

741) 

742async def delete_end_user( 

743 data: DeleteCustomerRequest, 

744 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

745) -> DeleteCustomersResponse: 

746 """ 

747 Delete multiple end-users. 

748 

749 Parameters: 

750 - user_ids (List[str], required): The unique `user_id`s for the users to delete 

751 

752 Example curl: 

753 ``` 

754 curl --location 'http://0.0.0.0:4000/customer/delete' \ 

755 --header 'Authorization: Bearer sk-1234' \ 

756 --header 'Content-Type: application/json' \ 

757 --data '{ 

758 "user_ids" :["ishaan-jaff-5"] 

759 }' 

760 

761 See below for all params  

762 ``` 

763 """ 

764 from litellm.proxy.proxy_server import prisma_client 

765 

766 try: 

767 if prisma_client is None: 767 ↛ 768line 767 didn't jump to line 768 because the condition on line 767 was never true

768 raise Exception("Not connected to DB!") 

769 

770 verbose_proxy_logger.debug("/customer/delete: Received data = %s", data) 

771 if data.user_ids is not None and isinstance(data.user_ids, list) and len(data.user_ids) > 0: 

772 # First check if all users exist 

773 existing_users: Final = await _typed_table(EndUserRepository(prisma_client)).find_many( 

774 where={"user_id": {"in": data.user_ids}} 

775 ) 

776 existing_user_ids: Final = {user.user_id for user in existing_users} 

777 missing_user_ids: Final = [user_id for user_id in data.user_ids if user_id not in existing_user_ids] 

778 

779 if missing_user_ids: 

780 raise ProxyException( 

781 message="End User Id(s)={} do not exist in db".format(", ".join(missing_user_ids)), 

782 type="not_found", 

783 code=404, 

784 param="user_ids", 

785 ) 

786 

787 # All users exist, proceed with deletion 

788 response: Final = await _typed_table(EndUserRepository(prisma_client)).delete_many( 

789 where={"user_id": {"in": data.user_ids}} 

790 ) 

791 verbose_proxy_logger.debug("received response from updating prisma client. response=%s", response) 

792 

793 await _evict_end_user_cache_keys(_end_user_cache_keys(data.user_ids)) 

794 

795 return DeleteCustomersResponse( 

796 deleted_customers=response, 

797 message="Successfully deleted customers with ids: " + str(data.user_ids), 

798 ) 

799 else: 

800 raise ValueError(f"user_id is required, passed user_id = {data.user_ids}") 

801 

802 # update based on remaining passed in values 

803 except Exception as e: 

804 verbose_proxy_logger.error("litellm.proxy.proxy_server.delete_end_user(): Exception occured - %s", e) 

805 raise handle_exception_on_proxy(e) 

806 

807 

808@router.get( 

809 "/customer/list", 

810 tags=["Customer Management"], 

811 dependencies=[Depends(user_api_key_auth)], 

812 response_model=list[CustomerResponse], 

813) 

814@router.get( 

815 "/end_user/list", 

816 tags=["Customer Management"], 

817 include_in_schema=False, 

818 dependencies=[Depends(user_api_key_auth)], 

819) 

820async def list_end_user( 

821 http_request: Request, 

822 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

823) -> list[CustomerResponse]: 

824 """ 

825 [Admin-only] List all available customers 

826 

827 Example curl: 

828 ``` 

829 curl --location --request GET 'http://0.0.0.0:4000/customer/list' \ 

830 --header 'Authorization: Bearer sk-1234' 

831 ``` 

832 

833 """ 

834 try: 

835 from litellm.proxy.proxy_server import prisma_client 

836 

837 if ( 837 ↛ 841line 837 didn't jump to line 841 because the condition on line 837 was never true

838 user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN 

839 and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY 

840 ): 

841 raise HTTPException( 

842 status_code=401, 

843 detail={"error": f"Admin-only endpoint. Your user role={user_api_key_dict.user_role}"}, 

844 ) 

845 

846 if prisma_client is None: 846 ↛ 847line 846 didn't jump to line 847 because the condition on line 846 was never true

847 raise HTTPException( 

848 status_code=400, 

849 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

850 ) 

851 

852 response: Final = await _typed_table(EndUserRepository(prisma_client)).find_many( 

853 include={"litellm_budget_table": True, "object_permission": True} 

854 ) 

855 

856 return [_to_customer_response(item) for item in response] 

857 

858 except Exception as e: 

859 verbose_proxy_logger.exception( 

860 "litellm.proxy.management_endpoints.customer_endpoints.list_end_user(): Exception occured - %s", e 

861 ) 

862 raise handle_exception_on_proxy(e) 

863 

864 

865@router.get( 

866 "/customer/daily/activity", 

867 tags=["Customer Management"], 

868 dependencies=[Depends(user_api_key_auth)], 

869 response_model=SpendAnalyticsPaginatedResponse, 

870) 

871@router.get( 

872 "/end_user/daily/activity", 

873 tags=["Customer Management"], 

874 include_in_schema=False, 

875 dependencies=[Depends(user_api_key_auth)], 

876) 

877async def get_customer_daily_activity( 

878 end_user_ids: str | None = None, 

879 start_date: str | None = None, 

880 end_date: str | None = None, 

881 model: str | None = None, 

882 api_key: str | None = None, 

883 page: int = 1, 

884 page_size: int = 10, 

885 exclude_end_user_ids: str | None = None, 

886 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

887): 

888 """ 

889 Get daily activity for specific organizations or all accessible organizations. 

890 """ 

891 if ( 891 ↛ 895line 891 didn't jump to line 895 because the condition on line 891 was never true

892 user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN 

893 and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY 

894 ): 

895 raise HTTPException( 

896 status_code=401, 

897 detail={"error": f"Admin-only endpoint. Your user role={user_api_key_dict.user_role}"}, 

898 ) 

899 

900 from litellm.proxy.proxy_server import prisma_client 

901 

902 if prisma_client is None: 902 ↛ 903line 902 didn't jump to line 903 because the condition on line 902 was never true

903 raise HTTPException( 

904 status_code=500, 

905 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

906 ) 

907 

908 # Parse comma-separated ids 

909 end_user_ids_list: Final = end_user_ids.split(",") if end_user_ids else None 

910 exclude_end_user_ids_list: list[str] | None = None 

911 if exclude_end_user_ids: 

912 exclude_end_user_ids_list = exclude_end_user_ids.split(",") if exclude_end_user_ids else None 

913 

914 # Fetch organization aliases for metadata 

915 where_condition: Final = dict[str, object]() 

916 if end_user_ids_list: 

917 where_condition["user_id"] = {"in": list(end_user_ids_list)} 

918 end_user_aliases: Final = await _typed_table(EndUserRepository(prisma_client)).find_many(where=where_condition) 

919 

920 # Query daily activity for organizations 

921 return await get_daily_activity( 

922 prisma_client=prisma_client, 

923 table_name="litellm_dailyenduserspend", 

924 entity_id_field="end_user_id", 

925 entity_id=end_user_ids_list, 

926 entity_metadata_field={e.user_id: {"alias": e.alias} for e in end_user_aliases}, 

927 exclude_entity_ids=exclude_end_user_ids_list, 

928 start_date=start_date, 

929 end_date=end_date, 

930 model=model, 

931 api_key=api_key, 

932 page=page, 

933 page_size=page_size, 

934 )