From 268d4f766545fdf92ef76ea30a6cbd7847476661 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 14:23:27 -0700 Subject: [PATCH 1/7] fix send_email update --- litellm/proxy/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 9ca8404bf..b11c2d04e 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -1835,7 +1835,7 @@ async def send_email(sender_name, sender_email, receiver_email, subject, html): smtp_password = os.getenv("SMTP_PASSWORD") ## EMAIL SETUP ## email_message = MIMEMultipart() - email_message["From"] = f"{sender_name} <{sender_email}>" + email_message["From"] = sender_email email_message["To"] = receiver_email email_message["Subject"] = subject @@ -1843,7 +1843,6 @@ async def send_email(sender_name, sender_email, receiver_email, subject, html): email_message.attach(MIMEText(html, "html")) try: - print_verbose(f"SMTP Connection Init") # Establish a secure connection with the SMTP server with smtplib.SMTP(smtp_host, smtp_port) as server: if os.getenv("SMTP_TLS", "True") != "False": From 9bcbb6cc8b3201b91b846bd5286d06ac46ad7a0b Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 14:29:00 -0700 Subject: [PATCH 2/7] feat - send send_email_alert_using_smtp --- litellm/integrations/slack_alerting.py | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index a4ff0620a..f8c0d0b5f 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -781,6 +781,57 @@ Model Info: return False + async def send_email_alert_using_smtp(self, webhook_event: WebhookEvent) -> bool: + """ + Sends structured Email alert to an SMTP server + + Currently only implemented for budget alerts + + Returns -> True if sent, False if not. + """ + from litellm.proxy.utils import send_email + + event_name = webhook_event.event_message + recipient_email = webhook_event.user_email + user_name = webhook_event.user_id + max_budget = webhook_event.max_budget + email_html_content = "Alert from LiteLLM Server" + + if webhook_event.event == "budget_crossed": + email_html_content = f""" +

LiteLLM

+ +

Hi {user_name},
+ + 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

+ + Best,
+ The LiteLLM team
+ """ + + payload = webhook_event.model_dump_json() + email_event = { + "from": "support@alerts.litellm.ai", + "to": recipient_email, + "subject": event_name, + "html": email_html_content, + } + headers = {"Content-type": "application/json"} + + response = await send_email( + sender_name=email_event["from"], + sender_email=email_event["from"], + receiver_email=email_event["to"], + subject=email_event["subject"], + html=email_event["html"], + ) + + return False + async def send_alert( self, message: str, @@ -823,6 +874,13 @@ Model Info: ): await self.send_webhook_alert(webhook_event=user_info) + if ( + "email" in self.alerting + and alert_type == "budget_alerts" + and user_info is not None + ): + await self.send_email_alert_using_smtp(webhook_event=user_info) + if "slack" not in self.alerting: return From c3dd0a1470977fbd575f20f8d5680175a7b5fc88 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 14:37:53 -0700 Subject: [PATCH 3/7] feat check SMTP_SENDER_EMAIL + put behind enterprise license --- litellm/integrations/slack_alerting.py | 4 +--- litellm/proxy/utils.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index f8c0d0b5f..5f8f0ada6 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -815,7 +815,6 @@ Model Info: payload = webhook_event.model_dump_json() email_event = { - "from": "support@alerts.litellm.ai", "to": recipient_email, "subject": event_name, "html": email_html_content, @@ -823,8 +822,6 @@ Model Info: headers = {"Content-type": "application/json"} response = await send_email( - sender_name=email_event["from"], - sender_email=email_event["from"], receiver_email=email_event["to"], subject=email_event["subject"], html=email_event["html"], @@ -879,6 +876,7 @@ Model Info: and alert_type == "budget_alerts" and user_info is not None ): + # only send budget alerts over Email await self.send_email_alert_using_smtp(webhook_event=user_info) if "slack" not in self.alerting: diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index b11c2d04e..11470bcda 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -1819,7 +1819,7 @@ async def _cache_user_row( return -async def send_email(sender_name, sender_email, receiver_email, subject, html): +async def send_email(receiver_email, subject, html): """ smtp_host, smtp_port, @@ -1829,10 +1829,24 @@ async def send_email(sender_name, sender_email, receiver_email, subject, html): sender_email, """ ## SERVER SETUP ## + from litellm.proxy.proxy_server import premium_user + from litellm.proxy.proxy_server import CommonProxyErrors + + # Check if user is premium - This is an Enterprise only Feature + if premium_user != True: + raise Exception( + f"Trying to use Email Alerting\n {CommonProxyErrors.not_premium_user.value}" + ) + # Done Checking + smtp_host = os.getenv("SMTP_HOST") smtp_port = os.getenv("SMTP_PORT", 587) # default to port 587 smtp_username = os.getenv("SMTP_USERNAME") smtp_password = os.getenv("SMTP_PASSWORD") + sender_email = os.getenv("SMTP_SENDER_EMAIL", None) + if sender_email is None: + raise Exception("Trying to use SMTP, but SMTP_SENDER_EMAIL is not set") + ## EMAIL SETUP ## email_message = MIMEMultipart() email_message["From"] = sender_email From 4f4607a4df2528c5d3275c20932256b411aac88f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 15:22:28 -0700 Subject: [PATCH 4/7] fix - send email alert when token crossed it's budget --- litellm/proxy/proxy_server.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 7cc196ffe..565bf47fc 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1062,12 +1062,19 @@ async def user_api_key_auth( # Check 4. Token Spend is under budget if valid_token.spend is not None and valid_token.max_budget is not None: + # collect information for alerting + + if isinstance(user_id_information, list): + user_id_information = user_id_information[0] + user_email = user_id_information.get("user_email", None) call_info = CallInfo( token=valid_token.token, spend=valid_token.spend, max_budget=valid_token.max_budget, user_id=valid_token.user_id, team_id=valid_token.team_id, + user_email=user_email, + key_alias=valid_token.key_alias, ) asyncio.create_task( proxy_logging_obj.budget_alerts( From 06370d24b445ae8fed6ba31ad66657faa49cb9c7 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 15:23:36 -0700 Subject: [PATCH 5/7] feat - email alerts --- litellm/integrations/slack_alerting.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index 5f8f0ada6..d403cd565 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -663,10 +663,10 @@ class SlackAlerting(CustomLogger): # check if crossed budget if user_info.spend >= user_info.max_budget: event = "budget_crossed" - event_message += "Budget Crossed" + event_message += f"Budget Crossed\n Total Budget:`{user_info.max_budget}`" elif percent_left <= 0.05: event = "threshold_crossed" - event_message += "5% Threshold Crossed" + event_message += "5% Threshold Crossed " elif percent_left <= 0.15: event = "threshold_crossed" event_message += "15% Threshold Crossed" @@ -796,6 +796,10 @@ Model Info: user_name = webhook_event.user_id max_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() + ) if webhook_event.event == "budget_crossed": email_html_content = f""" @@ -819,7 +823,6 @@ Model Info: "subject": event_name, "html": email_html_content, } - headers = {"Content-type": "application/json"} response = await send_email( receiver_email=email_event["to"], From c43a4dff7f46ae9d7112509a4bfa7e19676f577a Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 15:32:51 -0700 Subject: [PATCH 6/7] use litellm prefix in all emails --- litellm/integrations/slack_alerting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index d403cd565..39fb2490a 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -820,7 +820,7 @@ Model Info: payload = webhook_event.model_dump_json() email_event = { "to": recipient_email, - "subject": event_name, + "subject": f"LiteLLM: {event_name}", "html": email_html_content, } From 36916852ad9d0ab04e31a9a09e8820c2314341d7 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 15:37:36 -0700 Subject: [PATCH 7/7] fix - user_id_information --- litellm/proxy/proxy_server.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 565bf47fc..9e1230c38 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -1062,11 +1062,18 @@ async def user_api_key_auth( # Check 4. Token Spend is under budget if valid_token.spend is not None and valid_token.max_budget is not None: - # collect information for alerting - if isinstance(user_id_information, list): - user_id_information = user_id_information[0] - user_email = user_id_information.get("user_email", None) + #################################### + # collect information for alerting # + #################################### + + user_email = None + # Check if the token has any user id information + if user_id_information is not None: + if isinstance(user_id_information, list): + user_id_information = user_id_information[0] + user_email = user_id_information.get("user_email", None) + call_info = CallInfo( token=valid_token.token, spend=valid_token.spend, @@ -1083,6 +1090,10 @@ async def user_api_key_auth( ) ) + #################################### + # collect information for alerting # + #################################### + if valid_token.spend >= valid_token.max_budget: raise Exception( f"ExceededTokenBudget: Current spend for token: {valid_token.spend}; Max Budget for Token: {valid_token.max_budget}"