diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index bb6698d7308..d0a76ec9635 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -26,7 +26,13 @@ from litellm.caching import DualCache from litellm.integrations.custom_logger import CustomLogger from litellm.litellm_core_utils.litellm_logging import Logging from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler -from litellm.proxy._types import AlertType, CallInfo, UserAPIKeyAuth, WebhookEvent +from litellm.proxy._types import ( + AlertType, + CallInfo, + UserAPIKeyAuth, + VirtualKeyEvent, + WebhookEvent, +) from litellm.types.router import LiteLLM_Params from .email_templates.templates import * @@ -1788,3 +1794,46 @@ Model Info: verbose_proxy_logger.error("Error sending weekly spend report %s", e) pass + + async def send_virtual_key_event_slack( + self, + key_event: VirtualKeyEvent, + event_name: str, + ): + """ + Helper to send fallback statistics from prometheus server -> to slack + + This runs once per day and sends an overview of all the fallback statistics + """ + try: + message = f"`{event_name}`\n" + + key_event_dict = key_event.model_dump() + + # Add Created by information first + message += "*Action Done by:*\n" + for key, value in key_event_dict.items(): + if "created_by" in key: + message += f"{key}: `{value}`\n" + + # Add args sent to function in the alert + message += "\n*Arguments passed:*\n" + request_kwargs = key_event.request_kwargs + for key, value in request_kwargs.items(): + if key == "user_api_key_dict": + continue + message += f"{key}: `{value}`\n" + + await self.send_alert( + message=message, + level="High", + alert_type="fallback_reports", + alerting_metadata={}, + ) + + except Exception as e: + verbose_proxy_logger.error( + "Error sending send_virtual_key_event_slack %s", e + ) + + return diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 5e0de6fb09a..deb496f2e91 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1754,3 +1754,10 @@ class SSOUserDefinedValues(TypedDict): user_role: Optional[str] max_budget: Optional[float] budget_duration: Optional[str] + + +class VirtualKeyEvent(LiteLLMBase): + created_by_user_id: str + created_by_user_role: str + created_by_key_alias: Optional[str] + request_kwargs: dict diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 0e4696e4423..968b3ede912 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -26,6 +26,7 @@ import litellm from litellm._logging import verbose_proxy_logger from litellm.proxy._types import * from litellm.proxy.auth.user_api_key_auth import user_api_key_auth +from litellm.proxy.management_helpers.utils import management_endpoint_wrapper from litellm.proxy.utils import _duration_in_seconds router = APIRouter() @@ -37,6 +38,7 @@ router = APIRouter() dependencies=[Depends(user_api_key_auth)], response_model=GenerateKeyResponse, ) +@management_endpoint_wrapper async def generate_key_fn( data: GenerateKeyRequest, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), @@ -278,6 +280,7 @@ async def generate_key_fn( @router.post( "/key/update", tags=["key management"], dependencies=[Depends(user_api_key_auth)] ) +@management_endpoint_wrapper async def update_key_fn( request: Request, data: UpdateKeyRequest, @@ -398,6 +401,7 @@ async def update_key_fn( @router.post( "/key/delete", tags=["key management"], dependencies=[Depends(user_api_key_auth)] ) +@management_endpoint_wrapper async def delete_key_fn( data: KeyRequest, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), diff --git a/litellm/proxy/management_helpers/utils.py b/litellm/proxy/management_helpers/utils.py index 86c761a70ce..64e691196de 100644 --- a/litellm/proxy/management_helpers/utils.py +++ b/litellm/proxy/management_helpers/utils.py @@ -23,6 +23,7 @@ from litellm.proxy._types import ( # key request types; user request types; tea UpdateTeamRequest, UpdateUserRequest, UserAPIKeyAuth, + VirtualKeyEvent, ) from litellm.proxy.common_utils.http_parsing_utils import _read_request_body from litellm.proxy.utils import PrismaClient @@ -186,6 +187,56 @@ def _delete_customer_id_from_cache(kwargs): pass +async def send_management_endpoint_alert( + request_kwargs: dict, + user_api_key_dict: UserAPIKeyAuth, + function_name: str, +): + """ + Sends a slack alert when: + - A virtual key is created, updated, or deleted + - An internal user is created, updated, or deleted + - A team is created, updated, or deleted + """ + from litellm.proxy.proxy_server import premium_user, proxy_logging_obj + + if premium_user is not True: + return + + management_function_to_event_name = { + "generate_key_fn": "New Virtual Key Created", + "update_key_fn": "Virtual Key Updated", + "delete_key_fn": "Virtual Key Deleted", + # Team events + "new_team": "New Team Created", + "update_team": "Team Updated", + "delete_team": "Team Deleted", + # Internal User events + "new_user": "New Internal User Created", + "user_update": "Internal User Updated", + "delete_user": "Internal User Deleted", + } + + if ( + proxy_logging_obj is not None + and proxy_logging_obj.slack_alerting_instance is not None + ): + + # Virtual Key Events + if function_name in management_function_to_event_name: + key_event = VirtualKeyEvent( + created_by_user_id=user_api_key_dict.user_id or "Unknown", + created_by_user_role=user_api_key_dict.user_role or "Unknown", + created_by_key_alias=user_api_key_dict.key_alias, + request_kwargs=request_kwargs, + ) + + event_name = management_function_to_event_name[function_name] + await proxy_logging_obj.slack_alerting_instance.send_virtual_key_event_slack( + key_event=key_event, event_name=event_name + ) + + def management_endpoint_wrapper(func): """ This wrapper does the following: @@ -207,6 +258,13 @@ def management_endpoint_wrapper(func): user_api_key_dict: UserAPIKeyAuth = ( kwargs.get("user_api_key_dict") or UserAPIKeyAuth() ) + + await send_management_endpoint_alert( + request_kwargs=kwargs, + user_api_key_dict=user_api_key_dict, + function_name=func.__name__, + ) + _http_request: Request = kwargs.get("http_request") parent_otel_span = user_api_key_dict.parent_otel_span if parent_otel_span is not None: