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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2CUSTOMER MANAGEMENT
4All /customer management endpoints
6/customer/new
7/customer/info
8/customer/update
9/customer/delete
10"""
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
17import fastapi
18from fastapi import APIRouter, Depends, HTTPException, Request
19from pydantic import BaseModel, TypeAdapter
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
25 from litellm.proxy.utils import PrismaClient
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)
55_RowT_co: Final = TypeVar("_RowT_co", covariant=True)
56_STR_OBJECT_DICT: Final = TypeAdapter(dict[str, object])
58if TYPE_CHECKING: 58 ↛ 60line 58 didn't jump to line 60 because the condition on line 58 was never true
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: ...
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]: ...
73 async def create(
74 self,
75 data: Mapping[str, object],
76 include: Mapping[str, bool] | None = None,
77 ) -> _RowT_co: ...
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: ...
86 async def upsert(
87 self,
88 where: Mapping[str, object],
89 data: Mapping[str, Mapping[str, object]],
90 ) -> _RowT_co: ...
92 async def delete_many(self, where: Mapping[str, object]) -> int: ...
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
103router: Final = APIRouter()
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
117 await evict_and_broadcast(cache_keys=cache_keys, user_api_key_cache=user_api_key_cache)
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())
125def _to_customer_response(record: BaseModel) -> CustomerResponse:
126 """Validate a raw end-user DB row into the typed customer response.
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())
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
151 Parameters:
152 - user_ids (List[str], required): The unique `user_id`s for the users to block
154 (any /chat/completion call with this user={end-user-id} param, will be rejected.)
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
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 )
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)})
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
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 )
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 )
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 )
247 return {"blocked_users": litellm.blocked_user_list}
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 = {}
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
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
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.
285 Updates the update_end_user_table_data dict in place with the new object_permission_id.
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
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
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.
356 - Allow specifying allowed regions
357 - Allow specifying default model
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 }'
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 }'
384 # return end-user object
385 ```
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
394 - Add user to end user table
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 )
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 )
427 new_end_user_obj: dict[str, object] = {}
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)})
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
447 _user_data: Final = _STR_OBJECT_DICT.validate_python(data.dict(exclude_none=True))
449 for k, v in _user_data.items():
450 if k not in BudgetNewRequest.model_fields:
451 new_end_user_obj[k] = v
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 )
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)
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 )
476 await _evict_end_user_cache_keys(_end_user_cache_keys((data.user_id,)))
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)
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.
511 Parameters:
512 - end_user_id (str, required): The unique identifier for the end-user
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
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 )
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 )
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 )
542 return _to_customer_response(user_info)
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)
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
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.
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 }'
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 }'
615 See below for all params
616 ```
617 """
619 from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client
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!")
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
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 )
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 )
646 end_user_table_data_typed: Final = LiteLLM_EndUserTable.model_validate(end_user_table_data.model_dump())
648 ## Get budget table data ##
649 end_user_budget_table: Final = end_user_table_data_typed.litellm_budget_table
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
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
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 )
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 )
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 )
693 ## Update user table, with update params + new budget id (if set) ##
694 verbose_proxy_logger.debug("/customer/update: Received data = %s", data)
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)
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)
717 await _evict_end_user_cache_keys(_end_user_cache_keys((data.user_id,)))
719 return _to_customer_response(response)
720 else:
721 raise ValueError(f"user_id is required, passed user_id = {data.user_id}")
723 # update based on remaining passed in values
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)
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.
749 Parameters:
750 - user_ids (List[str], required): The unique `user_id`s for the users to delete
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 }'
761 See below for all params
762 ```
763 """
764 from litellm.proxy.proxy_server import prisma_client
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!")
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]
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 )
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)
793 await _evict_end_user_cache_keys(_end_user_cache_keys(data.user_ids))
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}")
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)
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
827 Example curl:
828 ```
829 curl --location --request GET 'http://0.0.0.0:4000/customer/list' \
830 --header 'Authorization: Bearer sk-1234'
831 ```
833 """
834 try:
835 from litellm.proxy.proxy_server import prisma_client
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 )
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 )
852 response: Final = await _typed_table(EndUserRepository(prisma_client)).find_many(
853 include={"litellm_budget_table": True, "object_permission": True}
854 )
856 return [_to_customer_response(item) for item in response]
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)
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 )
900 from litellm.proxy.proxy_server import prisma_client
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 )
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
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)
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 )