From fa49833cdfad46f3db282d9da4b8faba32ecf1f4 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 17:30:06 -0700 Subject: [PATCH 1/7] feat - send email on new key created --- litellm/integrations/slack_alerting.py | 104 +++++++++++++++++++++++-- litellm/proxy/_types.py | 8 ++ 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index 39fb2490a2e..469b319a725 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 @@ -39,12 +40,6 @@ class SlackAlertingArgs(LiteLLMBase): budget_alert_ttl: int = 24 * 60 * 60 # 24 hours -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 @@ -781,6 +776,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 + + 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 + ): + # try looking up this info in DB + from litellm.proxy.proxy_server import prisma_client + + 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

+ +

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

+ +
+
+            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"
+                    }}
+                ]
+            )
+
+            
+ + Detailed Documentation on Usage with OpenAI Python SDK, Langchain, LlamaIndex, Curl + + 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_email_alert_using_smtp(self, webhook_event: WebhookEvent) -> bool: """ Sends structured Email alert to an SMTP server diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 6081d8fbace..0d52ad0ecef 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1101,3 +1101,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 From 41879ae002098c7f35d2539c35b628bb0539dfd4 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 17:33:15 -0700 Subject: [PATCH 2/7] feat - email alerts on /key/generate --- litellm/proxy/proxy_server.py | 19 +++++++++++++++++++ litellm/proxy/utils.py | 1 + 2 files changed, 20 insertions(+) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9e1230c3895..64c17b30053 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -5409,6 +5409,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", None), + spend=response.get("spend", 0.0), + max_budget=response.get("max_budget", "Unlimited"), + 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 11470bcdaed..7eec25437db 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, ) from litellm.caching import DualCache, RedisCache from litellm.router import Deployment, ModelInfo, LiteLLM_Params From dba35536e95029c7ac089d34c27a80cb38b46b28 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 17:54:38 -0700 Subject: [PATCH 3/7] feat - resize logo --- litellm/integrations/slack_alerting.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index 469b319a725..4f63a4ab05f 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -18,6 +18,10 @@ from litellm.proxy._types import WebhookEvent import random +# 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" + + class LiteLLMBase(BaseModel): """ Implements default functions, all pydantic objects should have. @@ -785,7 +789,7 @@ Model Info: # make sure this is a premium user from litellm.proxy.proxy_server import premium_user - from litellm.proxy.proxy_server import CommonProxyErrors + from litellm.proxy.proxy_server import CommonProxyErrors, prisma_client if premium_user != True: raise Exception( @@ -800,9 +804,6 @@ Model Info: and recipient_user_id is not None and prisma_client is not None ): - # try looking up this info in DB - from litellm.proxy.proxy_server import prisma_client - user_row = await prisma_client.db.litellm_usertable.find_unique( where={"user_id": recipient_user_id} ) @@ -820,7 +821,7 @@ Model Info: "Trying to send email alert to no recipient", extra=webhook_event.dict() ) email_html_content = f""" -

LiteLLM

+ LiteLLM Logo

Hi {recipient_email},
@@ -830,6 +831,8 @@ Model Info:

Usage Example

+ Detailed Documentation on Usage with OpenAI Python SDK, Langchain, LlamaIndex, Curl +
 
             import openai
@@ -850,7 +853,6 @@ Model Info:
 
             
- Detailed Documentation on Usage with OpenAI Python SDK, Langchain, LlamaIndex, Curl If you have any questions, please send an email to support@berri.ai

@@ -895,7 +897,7 @@ Model Info: if webhook_event.event == "budget_crossed": email_html_content = f""" -

LiteLLM

+ LiteLLM Logo

Hi {user_name},
From fb7fe2b87cbf913d97c63816a6590b162a33e22f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 17:59:15 -0700 Subject: [PATCH 4/7] feat - send send_key_created_email --- litellm/integrations/slack_alerting.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index 4f63a4ab05f..0a76c6a6c97 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -827,7 +827,9 @@ Model Info: I'm happy to provide you with an OpenAI Proxy API Key, loaded with ${key_budget} per month.

+ Key:

{key_token}

+

Usage Example

@@ -901,7 +903,7 @@ Model Info:

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.

From 1434e5b66aa8c89d3b53d7a5d1aaeb202fc77806 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 18:12:51 -0700 Subject: [PATCH 5/7] feat - customize logo, support contact on email --- litellm/integrations/slack_alerting.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index 0a76c6a6c97..aeb1b369fc1 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -17,9 +17,12 @@ from litellm.integrations.custom_logger import CustomLogger from litellm.proxy._types import WebhookEvent import random - # 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): @@ -821,7 +824,7 @@ Model Info: "Trying to send email alert to no recipient", extra=webhook_event.dict() ) email_html_content = f""" - LiteLLM Logo + LiteLLM Logo

Hi {recipient_email},
@@ -856,7 +859,7 @@ Model Info: - 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
@@ -899,7 +902,7 @@ Model Info: if webhook_event.event == "budget_crossed": email_html_content = f""" - LiteLLM Logo + LiteLLM Logo

Hi {user_name},
@@ -907,7 +910,7 @@ Model Info: 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
From db5459922269b40bf5a4d348debdaa83b4a3ae00 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 18:14:45 -0700 Subject: [PATCH 6/7] feat - send email on api key created --- litellm/proxy/proxy_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 64c17b30053..8b98b1dcbf0 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -5413,9 +5413,9 @@ async def generate_key_fn( event="key_created", event_group="key", event_message=f"API Key Created", - token=response.get("token", None), + token=response.get("token", ""), spend=response.get("spend", 0.0), - max_budget=response.get("max_budget", "Unlimited"), + 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), From f6d7d0e5207877736fef300a898f59ee17305ec9 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 24 May 2024 18:31:10 -0700 Subject: [PATCH 7/7] fix - update webhook event validation --- litellm/integrations/slack_alerting.py | 3 +++ litellm/proxy/_types.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index aeb1b369fc1..3608d12daf0 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -655,6 +655,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 diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 0d52ad0ecef..e43522e7d16 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1093,7 +1093,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