From 0a50deb6dc926d3b19305e06f682fac4539f62fe Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 27 May 2024 22:37:12 -0700 Subject: [PATCH 1/6] ui - setup / test email alerting --- .../src/components/settings.tsx | 85 ++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index 5f712b1d4..badea6341 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -188,6 +188,43 @@ const Settings: React.FC = ({ console.log("Selected values:", values); }; + const handleSaveEmailSettings = () => { + if (!accessToken) { + return; + } + + + let updatedVariables: Record = {}; + + alerts + .filter((alert) => alert.name === "email") + .forEach((alert) => { + Object.entries(alert.variables ?? {}).forEach(([key, value]) => { + const inputElement = document.querySelector(`input[name="${key}"]`) as HTMLInputElement; + if (inputElement && inputElement.value) { + updatedVariables[key] = inputElement?.value; + } + }); + }); + + console.log("updatedVariables", updatedVariables); + //filter out null / undefined values for updatedVariables + + const payload = { + general_settings: { + alerting: ["email"], + }, + environment_variables: updatedVariables, + }; + try { + setCallbacksCall(accessToken, payload); + } catch (error) { + message.error("Failed to update alerts: " + error, 20); + } + + message.success("Email settings updated successfully"); + } + const handleSaveAlerts = () => { if (!accessToken) { return; @@ -369,7 +406,8 @@ const Settings: React.FC = ({ Logging Callbacks Alerting Types - Alerting Settings + Alerting Settings + Email Alerts @@ -526,6 +564,51 @@ const Settings: React.FC = ({ premiumUser={premiumUser} /> + + + {/* + Email Alerts are sent to when
+ - A key is created for a specific user_id
+ - A key belonging to a user crosses its budget +
*/} + Setup +
+ {alerts + .filter((alert) => alert.name == "email") + .map((alert, index) => ( + +
    + {Object.entries(alert.variables ?? {}) + + .map(([key, value]) => ( +
  • + {key} + +
  • + ))} +
+
+ ))} + +
+ + + + +
+
From b5f88b67b360db9c5b06acbe63e0a296cbad8e6b Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 27 May 2024 22:38:17 -0700 Subject: [PATCH 2/6] fix - validation for email alerting --- litellm/integrations/slack_alerting.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/litellm/integrations/slack_alerting.py b/litellm/integrations/slack_alerting.py index c822958d9..8fa1be891 100644 --- a/litellm/integrations/slack_alerting.py +++ b/litellm/integrations/slack_alerting.py @@ -41,6 +41,7 @@ class ProviderRegionOutageModel(BaseOutageModel): # 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" +LITELLM_SUPPORT_CONTACT = "support@berri.ai" class LiteLLMBase(BaseModel): @@ -1171,6 +1172,10 @@ Model Info: await self._check_if_using_premium_email_feature( premium_user, email_logo_url, email_support_contact ) + if email_logo_url is None: + email_logo_url = LITELLM_LOGO_URL + if email_support_contact is None: + email_support_contact = LITELLM_SUPPORT_CONTACT event_name = webhook_event.event_message recipient_email = webhook_event.user_email @@ -1271,6 +1276,11 @@ Model Info: premium_user, email_logo_url, email_support_contact ) + if email_logo_url is None: + email_logo_url = LITELLM_LOGO_URL + if email_support_contact is None: + email_support_contact = LITELLM_SUPPORT_CONTACT + event_name = webhook_event.event_message recipient_email = webhook_event.user_email user_name = webhook_event.user_id From 24c80e6bc758333655c7ccfffba6c7db0d0fdb1a Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 27 May 2024 22:46:45 -0700 Subject: [PATCH 3/6] fix - testing email alerting --- litellm/proxy/proxy_server.py | 164 ++++++++++++++++++++++++---------- 1 file changed, 118 insertions(+), 46 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 0e8d20f16..c67363456 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -2930,48 +2930,54 @@ class ProxyConfig: global llm_router, llm_model_list, master_key, general_settings import base64 - if llm_router is None and master_key is not None: - verbose_proxy_logger.debug(f"len new_models: {len(new_models)}") + try: + if llm_router is None and master_key is not None: + verbose_proxy_logger.debug(f"len new_models: {len(new_models)}") - _model_list: list = [] - for m in new_models: - _litellm_params = m.litellm_params - if isinstance(_litellm_params, dict): - # decrypt values - for k, v in _litellm_params.items(): - if isinstance(v, str): - # decode base64 - decoded_b64 = base64.b64decode(v) - # decrypt value - _litellm_params[k] = decrypt_value( - value=decoded_b64, master_key=master_key # type: ignore - ) - _litellm_params = LiteLLM_Params(**_litellm_params) - else: - verbose_proxy_logger.error( - f"Invalid model added to proxy db. Invalid litellm params. litellm_params={_litellm_params}" + _model_list: list = [] + for m in new_models: + _litellm_params = m.litellm_params + if isinstance(_litellm_params, dict): + # decrypt values + for k, v in _litellm_params.items(): + if isinstance(v, str): + # decode base64 + decoded_b64 = base64.b64decode(v) + # decrypt value + _litellm_params[k] = decrypt_value( + value=decoded_b64, master_key=master_key # type: ignore + ) + _litellm_params = LiteLLM_Params(**_litellm_params) + else: + verbose_proxy_logger.error( + f"Invalid model added to proxy db. Invalid litellm params. litellm_params={_litellm_params}" + ) + continue # skip to next model + + _model_info = self.get_model_info_with_id(model=m) + _model_list.append( + Deployment( + model_name=m.model_name, + litellm_params=_litellm_params, + model_info=_model_info, + ).to_json(exclude_none=True) ) - continue # skip to next model + if len(_model_list) > 0: + verbose_proxy_logger.debug(f"_model_list: {_model_list}") + llm_router = litellm.Router(model_list=_model_list) + verbose_proxy_logger.debug(f"updated llm_router: {llm_router}") + else: + verbose_proxy_logger.debug(f"len new_models: {len(new_models)}") + ## DELETE MODEL LOGIC + await self._delete_deployment(db_models=new_models) - _model_info = self.get_model_info_with_id(model=m) - _model_list.append( - Deployment( - model_name=m.model_name, - litellm_params=_litellm_params, - model_info=_model_info, - ).to_json(exclude_none=True) - ) - if len(_model_list) > 0: - verbose_proxy_logger.debug(f"_model_list: {_model_list}") - llm_router = litellm.Router(model_list=_model_list) - verbose_proxy_logger.debug(f"updated llm_router: {llm_router}") - else: - verbose_proxy_logger.debug(f"len new_models: {len(new_models)}") - ## DELETE MODEL LOGIC - await self._delete_deployment(db_models=new_models) + ## ADD MODEL LOGIC + self._add_deployment(db_models=new_models) - ## ADD MODEL LOGIC - self._add_deployment(db_models=new_models) + except Exception as e: + verbose_proxy_logger.error( + f"Error adding/deleting model to llm_router: {str(e)}" + ) if llm_router is not None: llm_model_list = llm_router.get_model_list() @@ -3020,11 +3026,20 @@ class ProxyConfig: ## ALERTING ## [TODO] move this to the _update_general_settings() block _general_settings = config_data.get("general_settings", {}) if "alerting" in _general_settings: - general_settings["alerting"] = _general_settings["alerting"] - proxy_logging_obj.alerting = general_settings["alerting"] - proxy_logging_obj.slack_alerting_instance.alerting = general_settings[ - "alerting" - ] + if ( + general_settings["alerting"] is not None + and isinstance(general_settings["alerting"], list) + and _general_settings["alerting"] is not None + and isinstance(_general_settings["alerting"], list) + ): + for alert in _general_settings["alerting"]: + if alert not in general_settings["alerting"]: + general_settings["alerting"].append(alert) + + proxy_logging_obj.alerting = general_settings["alerting"] + proxy_logging_obj.slack_alerting_instance.alerting = general_settings[ + "alerting" + ] if "alert_types" in _general_settings: general_settings["alert_types"] = _general_settings["alert_types"] @@ -10897,10 +10912,10 @@ async def update_config(config_info: ConfigYAML): if k == "alert_to_webhook_url": # check if slack is already enabled. if not, enable it if "alerting" not in _existing_settings: - _existing_settings["alerting"] = ["slack"] + _existing_settings["alerting"].append("slack") elif isinstance(_existing_settings["alerting"], list): if "slack" not in _existing_settings["alerting"]: - _existing_settings["alerting"] = ["slack"] + _existing_settings["alerting"].append("slack") _existing_settings[k] = v config["general_settings"] = _existing_settings @@ -11386,6 +11401,36 @@ async def get_config(): "alerts_to_webhook": _alerts_to_webhook, } ) + # pass email alerting vars + _email_vars = [ + "SMTP_HOST", + "SMTP_PORT", + "SMTP_USERNAME", + "SMTP_PASSWORD", + "SMTP_SENDER_EMAIL", + "TEST_EMAIL_ADDRESS", + "EMAIL_LOGO_URL", + "EMAIL_SUPPORT_CONTACT", + ] + _email_env_vars = {} + for _var in _email_vars: + env_variable = environment_variables.get(_var, None) + if env_variable is None: + _email_env_vars[_var] = None + else: + # decode + decrypt the value + decoded_b64 = base64.b64decode(env_variable) + _decrypted_value = decrypt_value( + value=decoded_b64, master_key=master_key + ) + _email_env_vars[_var] = _decrypted_value + + alerting_data.append( + { + "name": "email", + "variables": _email_env_vars, + } + ) if llm_router is None: _router_settings = {} @@ -11472,7 +11517,7 @@ async def test_endpoint(request: Request): async def health_services_endpoint( user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), service: Literal[ - "slack_budget_alerts", "langfuse", "slack", "openmeter", "webhook" + "slack_budget_alerts", "langfuse", "slack", "openmeter", "webhook", "email" ] = fastapi.Query(description="Specify the service being hit."), ): """ @@ -11489,6 +11534,7 @@ async def health_services_endpoint( ) if service not in [ "slack_budget_alerts", + "email", "langfuse", "slack", "openmeter", @@ -11621,6 +11667,32 @@ async def health_services_endpoint( ) }, ) + if service == "email": + webhook_event = WebhookEvent( + event="key_created", + event_group="key", + event_message="Test Email Alert", + token=user_api_key_dict.token or "", + key_alias="Email Test key (This is only a test alert key. DO NOT USE THIS IN PRODUCTION.)", + spend=0, + max_budget=0, + user_id=user_api_key_dict.user_id, + user_email=os.getenv("TEST_EMAIL_ADDRESS"), + team_id=user_api_key_dict.team_id, + ) + + # use create task - this can take 10 seconds. don't keep ui users waiting for notification to check their email + asyncio.create_task( + proxy_logging_obj.slack_alerting_instance.send_key_created_email( + webhook_event=webhook_event + ) + ) + + return { + "status": "success", + "message": "Mock Email Alert sent, verify Email Alert Received", + } + except Exception as e: traceback.print_exc() if isinstance(e, HTTPException): From 66feb7f8841920e3a8bdf79564c090033bbc06d2 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Mon, 27 May 2024 23:21:16 -0700 Subject: [PATCH 4/6] feat -working email settins --- .../src/components/settings.tsx | 131 +++++++++++++++--- 1 file changed, 112 insertions(+), 19 deletions(-) diff --git a/ui/litellm-dashboard/src/components/settings.tsx b/ui/litellm-dashboard/src/components/settings.tsx index badea6341..b1c6587bd 100644 --- a/ui/litellm-dashboard/src/components/settings.tsx +++ b/ui/litellm-dashboard/src/components/settings.tsx @@ -571,26 +571,119 @@ const Settings: React.FC = ({ - A key is created for a specific user_id
- A key belonging to a user crosses its budget */} - Setup -
- {alerts - .filter((alert) => alert.name == "email") - .map((alert, index) => ( - -
    - {Object.entries(alert.variables ?? {}) - - .map(([key, value]) => ( -
  • - {key} - -
  • - ))} -
-
- ))} +Email Settings +
+ {alerts + .filter((alert) => alert.name === "email") + .map((alert, index) => ( + -
+
    + + {Object.entries(alert.variables ?? {}).map(([key, value]) => ( +
  • + + { premiumUser!= true && (key === "EMAIL_LOGO_URL" || key === "EMAIL_SUPPORT_CONTACT") ? ( + + + ) : ( +
    + {key} + +
    + + )} + + {/* Added descriptions for input fields */} +

    + {key === "SMTP_HOST" && ( +

    + Enter the SMTP host address, e.g. 'smtp.resend.com' + Required * +
    + + )} + + {key === "SMTP_PORT" && ( +
    + Enter the SMTP port number, e.g. '587' + Required * + +
    + + )} + + {key === "SMTP_USERNAME" && ( +
    + Enter the SMTP username, e.g. 'username' + Required * +
    + + )} + + {key === "SMTP_PASSWORD" && ( + Required * + )} + + {key === "SMTP_SENDER_EMAIL" && ( +
    + Enter the sender email address, e.g. 'sender@berri.ai' + Required * + +
    + )} + + {key === "TEST_EMAIL_ADDRESS" && ( +
    + Email Address to send 'Test Email Alert' to. example: 'info@berri.ai' + Required * +
    + ) + } + {key === "EMAIL_LOGO_URL" && ( +
    + (Optional) Customize the Logo that appears in the email, pass a url to your logo +
    + ) + } + {key === "EMAIL_SUPPORT_CONTACT" && ( +
    + (Optional) Customize the support email address that appears in the email. Default is support@berri.ai +
    + ) + } +

    +
  • + ))} +
    +
+ + ))} +