diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index a4ff0620a..39fb2490a 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" @@ -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 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""" +
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 = {
+ "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_alert(
self,
message: str,
@@ -823,6 +874,14 @@ 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
+ ):
+ # only send budget alerts over Email
+ await self.send_email_alert_using_smtp(webhook_event=user_info)
+
if "slack" not in self.alerting:
return
diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py
index 7cc196ffe..9e1230c38 100644
--- a/litellm/proxy/proxy_server.py
+++ b/litellm/proxy/proxy_server.py
@@ -1062,12 +1062,26 @@ 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 #
+ ####################################
+
+ 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,
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(
@@ -1076,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}"
diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py
index 9ca8404bf..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,13 +1829,27 @@ 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"] = f"{sender_name} <{sender_email}>"
+ email_message["From"] = sender_email
email_message["To"] = receiver_email
email_message["Subject"] = subject
@@ -1843,7 +1857,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":