diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index 608e0113cf0..620320f6749 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -14,6 +14,7 @@ from pydantic import BaseModel from enum import Enum from datetime import datetime as dt, timedelta, timezone from litellm.integrations.custom_logger import CustomLogger +from litellm.proxy._types import WebhookEvent import random from typing import TypedDict from openai import APIError @@ -30,6 +31,13 @@ class OutageModel(TypedDict): major_alert_sent: bool last_updated_at: float +# we use this for the email header, please send a test email if you change this. verify it looks good on email +LITELLM_LOGO_URL = "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" +EMAIL_LOGO_URL = os.getenv( + "SMTP_SENDER_LOGO", "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" +) +EMAIL_SUPPORT_CONTACT = os.getenv("EMAIL_SUPPORT_CONTACT", "support@berri.ai") + class LiteLLMBase(BaseModel): """ @@ -57,12 +65,6 @@ class SlackAlertingArgs(LiteLLMBase): max_outage_alert_list_size: int = 10 # prevent memory leak -class WebhookEvent(CallInfo): - event: Literal["budget_crossed", "threshold_crossed", "projected_limit_exceeded"] - event_group: Literal["user", "key", "team", "proxy"] - event_message: str # human-readable description of event - - class DeploymentMetrics(LiteLLMBase): """ Metrics per deployment, stored in cache @@ -664,6 +666,9 @@ class SlackAlerting(CustomLogger): _id = user_info.token # percent of max_budget left to spend + if user_info.max_budget is None: + return + if user_info.max_budget > 0: percent_left = ( user_info.max_budget - user_info.spend @@ -946,6 +951,103 @@ Model Info: return False + async def send_key_created_email(self, webhook_event: WebhookEvent) -> bool: + from litellm.proxy.utils import send_email + + if self.alerting is None or "email" not in self.alerting: + # do nothing if user does not want email alerts + return False + + # make sure this is a premium user + from litellm.proxy.proxy_server import premium_user + from litellm.proxy.proxy_server import CommonProxyErrors, prisma_client + + if premium_user != True: + raise Exception( + f"Trying to use Email Alerting on key creation\n {CommonProxyErrors.not_premium_user.value}" + ) + + event_name = webhook_event.event_message + recipient_email = webhook_event.user_email + recipient_user_id = webhook_event.user_id + if ( + recipient_email is None + and recipient_user_id is not None + and prisma_client is not None + ): + user_row = await prisma_client.db.litellm_usertable.find_unique( + where={"user_id": recipient_user_id} + ) + + if user_row is not None: + recipient_email = user_row.user_email + + key_name = webhook_event.key_alias + key_token = webhook_event.token + key_budget = webhook_event.max_budget + + email_html_content = "Alert from LiteLLM Server" + if recipient_email is None: + verbose_proxy_logger.error( + "Trying to send email alert to no recipient", extra=webhook_event.dict() + ) + email_html_content = f""" + LiteLLM Logo + +

Hi {recipient_email},
+ + I'm happy to provide you with an OpenAI Proxy API Key, loaded with ${key_budget} per month.

+ + + Key:

{key_token}

+
+ +

Usage Example

+ + Detailed Documentation on Usage with OpenAI Python SDK, Langchain, LlamaIndex, Curl + +
+
+            import openai
+            client = openai.OpenAI(
+                api_key="{key_token}",
+                base_url={os.getenv("PROXY_BASE_URL", "http://0.0.0.0:4000")}
+            )
+
+            response = client.chat.completions.create(
+                model="gpt-3.5-turbo", # model to send to the proxy
+                messages = [
+                    {{
+                        "role": "user",
+                        "content": "this is a test request, write a short poem"
+                    }}
+                ]
+            )
+
+            
+ + + If you have any questions, please send an email to {EMAIL_SUPPORT_CONTACT}

+ + Best,
+ The LiteLLM team
+ """ + + payload = webhook_event.model_dump_json() + email_event = { + "to": recipient_email, + "subject": f"LiteLLM: {event_name}", + "html": email_html_content, + } + + response = await send_email( + receiver_email=email_event["to"], + subject=email_event["subject"], + html=email_event["html"], + ) + + return False + async def send_email_alert_using_smtp(self, webhook_event: WebhookEvent) -> bool: """ Sends structured Email alert to an SMTP server @@ -968,15 +1070,15 @@ Model Info: if webhook_event.event == "budget_crossed": email_html_content = f""" -

LiteLLM

+ LiteLLM Logo

Hi {user_name},
- Your LLM API usage this month has reached your account's monthly budget of ${max_budget}

+ Your LLM API usage this month has reached your account's monthly budget of ${max_budget}

API requests will be rejected until either (a) you increase your monthly budget or (b) your monthly usage resets at the beginning of the next calendar month.

- If you have any questions, please send an email to support@berri.ai

+ If you have any questions, please send an email to {EMAIL_SUPPORT_CONTACT}

Best,
The LiteLLM team
diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index b2c38794314..e8b3e657205 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1096,7 +1096,7 @@ class CallInfo(LiteLLMBase): """Used for slack budget alerting""" spend: float - max_budget: float + max_budget: Optional[float] = None token: str = Field(description="Hashed value of that key") user_id: Optional[str] = None team_id: Optional[str] = None @@ -1104,3 +1104,11 @@ class CallInfo(LiteLLMBase): key_alias: Optional[str] = None projected_exceeded_date: Optional[str] = None projected_spend: Optional[float] = None + + +class WebhookEvent(CallInfo): + event: Literal[ + "budget_crossed", "threshold_crossed", "projected_limit_exceeded", "key_created" + ] + event_group: Literal["user", "key", "team", "proxy"] + event_message: str # human-readable description of event diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index fb7b65072e7..d558107c3f4 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -5402,6 +5402,25 @@ async def generate_key_fn( response["soft_budget"] = ( data.soft_budget ) # include the user-input soft budget in the response + event = WebhookEvent( + event="key_created", + event_group="key", + event_message=f"API Key Created", + token=response.get("token", ""), + spend=response.get("spend", 0.0), + max_budget=response.get("max_budget", 0.0), + user_id=response.get("user_id", None), + team_id=response.get("team_id", "Default Team"), + key_alias=response.get("key_alias", None), + ) + + # If user configured email alerting - send an Email letting their end-user know the key was created + asyncio.create_task( + proxy_logging_obj.slack_alerting_instance.send_key_created_email( + webhook_event=event, + ) + ) + return GenerateKeyResponse(**response) except Exception as e: traceback.print_exc() diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index fd2001b04e5..b710165cb44 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -12,6 +12,7 @@ from litellm.proxy._types import ( LiteLLM_TeamTable, Member, CallInfo, + WebhookEvent, AlertType, ) from litellm.caching import DualCache, RedisCache