From 69670001de07597bc0be294178615002c7b842c9 Mon Sep 17 00:00:00 2001 From: Jugal Bhatt Date: Tue, 1 Jul 2025 14:34:15 -0700 Subject: [PATCH 1/7] email_templates --- docs/my-website/docs/proxy/config_settings.md | 3 ++ docs/my-website/docs/proxy/email.md | 13 +++++-- .../send_emails/base_email.py | 38 +++++++++++++++---- .../types/enterprise_callbacks/send_emails.py | 11 +++--- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index db011a5825c..5db76088fa6 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -434,6 +434,9 @@ router_settings: | DOCS_URL | The path to the Swagger API documentation. **By default this is "/"** | EMAIL_LOGO_URL | URL for the logo used in emails | EMAIL_SUPPORT_CONTACT | Support contact email address +| EMAIL_SIGNATURE | Custom signature for emails +| EMAIL_SUBJECT_INVITATION | Custom subject template for invitation emails +| EMAIL_SUBJECT_KEY_CREATED | Custom subject template for key creation emails | EXPERIMENTAL_MULTI_INSTANCE_RATE_LIMITING | Flag to enable new multi-instance rate limiting. **Default is False** | FIREWORKS_AI_4_B | Size parameter for Fireworks AI 4B model. Default is 4 | FIREWORKS_AI_16_B | Size parameter for Fireworks AI 16B model. Default is 16 diff --git a/docs/my-website/docs/proxy/email.md b/docs/my-website/docs/proxy/email.md index 4eb35367dbe..5d5a07b156e 100644 --- a/docs/my-website/docs/proxy/email.md +++ b/docs/my-website/docs/proxy/email.md @@ -134,13 +134,20 @@ Customizing Email Branding is an Enterprise Feature [Get in touch with us for a ::: -LiteLLM allows you to customize the: +LiteLLM allows you to customize: - Logo on the Email -- Email support contact +- Email support contact +- Email signature +- Email subject lines for different events -Set the following in your env to customize your emails +Set the following in your env to customize your emails: ```shell EMAIL_LOGO_URL="https://litellm-listing.s3.amazonaws.com/litellm_logo.png" # public url to your logo EMAIL_SUPPORT_CONTACT="support@berri.ai" # Your company support email +EMAIL_SIGNATURE="Best regards,\nYour Company Team" # Custom email signature +EMAIL_SUBJECT_INVITATION="Welcome to {company_name}!" # Subject for invitation emails +EMAIL_SUBJECT_KEY_CREATED="Your API Key for {company_name}" # Subject for key creation emails ``` + +If custom subject templates are not provided, the system will use default templates in the format "LiteLLM: {event_message}". diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py index a7c127cffff..92ad14403b6 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py @@ -29,6 +29,10 @@ from litellm.types.integrations.slack_alerting import LITELLM_LOGO_URL class BaseEmailLogger(CustomLogger): DEFAULT_LITELLM_EMAIL = "notifications@alerts.litellm.ai" DEFAULT_SUPPORT_EMAIL = "support@berri.ai" + DEFAULT_SUBJECT_TEMPLATES = { + EmailEvent.new_user_invitation: "LiteLLM: {event_message}", + EmailEvent.virtual_key_created: "LiteLLM: {event_message}", + } async def send_user_invitation_email(self, event: WebhookEvent): """ @@ -38,8 +42,8 @@ class BaseEmailLogger(CustomLogger): email_event=EmailEvent.new_user_invitation, user_id=event.user_id, user_email=getattr(event, "user_email", None), + event_message=event.event_message, ) - # Implement invitation email logic using email_params verbose_proxy_logger.debug( f"send_user_invitation_email_event: {json.dumps(event, indent=4, default=str)}" @@ -50,13 +54,13 @@ class BaseEmailLogger(CustomLogger): recipient_email=email_params.recipient_email, base_url=email_params.base_url, email_support_contact=email_params.support_contact, - email_footer=EMAIL_FOOTER, + email_footer=email_params.signature, ) await self.send_email( from_email=self.DEFAULT_LITELLM_EMAIL, to_email=[email_params.recipient_email], - subject=f"LiteLLM: {event.event_message}", + subject=email_params.subject, html_body=email_html_content, ) @@ -68,11 +72,11 @@ class BaseEmailLogger(CustomLogger): """ Send email to user after creating key for the user """ - email_params = await self._get_email_params( user_id=send_key_created_email_event.user_id, user_email=send_key_created_email_event.user_email, email_event=EmailEvent.virtual_key_created, + event_message=send_key_created_email_event.event_message, ) verbose_proxy_logger.debug( @@ -86,13 +90,13 @@ class BaseEmailLogger(CustomLogger): key_token=send_key_created_email_event.virtual_key, base_url=email_params.base_url, email_support_contact=email_params.support_contact, - email_footer=EMAIL_FOOTER, + email_footer=email_params.signature, ) await self.send_email( from_email=self.DEFAULT_LITELLM_EMAIL, to_email=[email_params.recipient_email], - subject=f"LiteLLM: {send_key_created_email_event.event_message}", + subject=email_params.subject, html_body=email_html_content, ) pass @@ -102,16 +106,34 @@ class BaseEmailLogger(CustomLogger): email_event: EmailEvent, user_id: Optional[str] = None, user_email: Optional[str] = None, + event_message: Optional[str] = None, ) -> EmailParams: """ Get common email parameters used across different email sending methods Returns: - EmailParams object containing logo_url, support_contact, base_url, and recipient_email + EmailParams object containing logo_url, support_contact, base_url, recipient_email, subject, and signature """ logo_url = os.getenv("EMAIL_LOGO_URL", None) or LITELLM_LOGO_URL support_contact = os.getenv("EMAIL_SUPPORT_CONTACT", self.DEFAULT_SUPPORT_EMAIL) base_url = os.getenv("PROXY_BASE_URL", "http://0.0.0.0:4000") + signature = os.getenv("EMAIL_SIGNATURE", EMAIL_FOOTER) + + # Get custom subject template based on email event type + if email_event == EmailEvent.new_user_invitation: + subject_template = os.getenv( + "EMAIL_SUBJECT_INVITATION", + self.DEFAULT_SUBJECT_TEMPLATES[EmailEvent.new_user_invitation] + ) + elif email_event == EmailEvent.virtual_key_created: + subject_template = os.getenv( + "EMAIL_SUBJECT_KEY_CREATED", + self.DEFAULT_SUBJECT_TEMPLATES[EmailEvent.virtual_key_created] + ) + else: + subject_template = "LiteLLM: {event_message}" + + subject = subject_template.format(event_message=event_message) if event_message else "LiteLLM Notification" recipient_email: Optional[ str @@ -132,6 +154,8 @@ class BaseEmailLogger(CustomLogger): support_contact=support_contact, base_url=base_url, recipient_email=recipient_email, + subject=subject, + signature=signature, ) def _format_key_budget(self, max_budget: Optional[float]) -> str: diff --git a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py index 95bc7ff94e9..3adeefa62bb 100644 --- a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py +++ b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py @@ -1,7 +1,6 @@ -import enum -from typing import Dict, List - -from pydantic import BaseModel, Field +from enum import Enum +from typing import Dict, List, Optional +from pydantic import BaseModel from litellm.proxy._types import WebhookEvent @@ -11,6 +10,8 @@ class EmailParams(BaseModel): support_contact: str base_url: str recipient_email: str + subject: str + signature: str class SendKeyCreatedEmailEvent(WebhookEvent): @@ -22,7 +23,7 @@ class SendKeyCreatedEmailEvent(WebhookEvent): """ -class EmailEvent(str, enum.Enum): +class EmailEvent(str, Enum): virtual_key_created = "Virtual Key Created" new_user_invitation = "New User Invitation" From b63d64dcf531b4ff623c481ed23e52fc9939d060 Mon Sep 17 00:00:00 2001 From: Jugal Bhatt Date: Tue, 1 Jul 2025 15:15:40 -0700 Subject: [PATCH 2/7] add subject and signature tests --- .../types/enterprise_callbacks/send_emails.py | 23 ++---- .../test_email_customization.py | 77 +++++++++++++++++++ 2 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 tests/proxy_admin_ui_tests/test_email_customization.py diff --git a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py index 3adeefa62bb..2d3c8adf2c6 100644 --- a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py +++ b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py @@ -1,10 +1,10 @@ -from enum import Enum -from typing import Dict, List, Optional -from pydantic import BaseModel +import enum +from typing import Dict, List + +from pydantic import BaseModel, Field from litellm.proxy._types import WebhookEvent - class EmailParams(BaseModel): logo_url: str support_contact: str @@ -18,44 +18,33 @@ class SendKeyCreatedEmailEvent(WebhookEvent): virtual_key: str """ The virtual key that was created - this will be sk-123xxx, since we will be emailing this to the user to start using the key """ -class EmailEvent(str, Enum): +class EmailEvent(str, enum.Enum): virtual_key_created = "Virtual Key Created" new_user_invitation = "New User Invitation" - class EmailEventSettings(BaseModel): event: EmailEvent enabled: bool - - class EmailEventSettingsUpdateRequest(BaseModel): settings: List[EmailEventSettings] - - class EmailEventSettingsResponse(BaseModel): settings: List[EmailEventSettings] - - class DefaultEmailSettings(BaseModel): """Default settings for email events""" - settings: Dict[EmailEvent, bool] = Field( default_factory=lambda: { EmailEvent.virtual_key_created: False, # Off by default EmailEvent.new_user_invitation: True, # On by default } ) - def to_dict(self) -> Dict[str, bool]: """Convert to dictionary with string keys for storage""" return {event.value: enabled for event, enabled in self.settings.items()} - @classmethod def get_defaults(cls) -> Dict[str, bool]: """Get the default settings as a dictionary with string keys""" - return cls().to_dict() + return cls().to_dict() \ No newline at end of file diff --git a/tests/proxy_admin_ui_tests/test_email_customization.py b/tests/proxy_admin_ui_tests/test_email_customization.py new file mode 100644 index 00000000000..5d3798e9264 --- /dev/null +++ b/tests/proxy_admin_ui_tests/test_email_customization.py @@ -0,0 +1,77 @@ +import pytest +from unittest.mock import patch, MagicMock +import os +import sys +sys.path.insert(0, os.path.abspath("../..")) + +from enterprise.litellm_enterprise.enterprise_callbacks.send_emails.base_email import BaseEmailLogger +from enterprise.litellm_enterprise.types.enterprise_callbacks.send_emails import EmailEvent +from litellm.integrations.email_templates.email_footer import EMAIL_FOOTER + +@pytest.fixture +def mock_env_vars(monkeypatch): + """Set up test environment variables""" + monkeypatch.setenv("EMAIL_LOGO_URL", "https://test-company.com/logo.png") + monkeypatch.setenv("EMAIL_SUPPORT_CONTACT", "support@test-company.com") + monkeypatch.setenv("EMAIL_SIGNATURE", "Best regards,\nTest Company Team") + monkeypatch.setenv("EMAIL_SUBJECT_INVITATION", "Welcome to Test Company!") + monkeypatch.setenv("EMAIL_SUBJECT_KEY_CREATED", "Your Test Company API Key") + monkeypatch.setenv("PROXY_BASE_URL", "http://test.com") + +@pytest.mark.asyncio +async def test_get_email_params_custom_templates(mock_env_vars): + """Test that _get_email_params returns correct values with custom templates""" + email_logger = BaseEmailLogger() + + # Test invitation email params + invitation_params = await email_logger._get_email_params( + email_event=EmailEvent.new_user_invitation, + user_email="test@example.com", + event_message="New User Invitation" + ) + + assert invitation_params.subject == "Welcome to Test Company!" + assert invitation_params.signature == "Best regards,\nTest Company Team" + assert invitation_params.logo_url == "https://test-company.com/logo.png" + assert invitation_params.support_contact == "support@test-company.com" + assert invitation_params.base_url == "http://test.com" + + # Test key created email params + key_params = await email_logger._get_email_params( + email_event=EmailEvent.virtual_key_created, + user_email="test@example.com", + event_message="API Key Created" + ) + + assert key_params.subject == "Your Test Company API Key" + assert key_params.signature == "Best regards,\nTest Company Team" + +@pytest.mark.asyncio +async def test_get_email_params_default_templates(monkeypatch): + """Test that _get_email_params uses default templates when custom ones aren't provided""" + # Clear any existing environment variables + monkeypatch.delenv("EMAIL_SUBJECT_INVITATION", raising=False) + monkeypatch.delenv("EMAIL_SUBJECT_KEY_CREATED", raising=False) + monkeypatch.delenv("EMAIL_SIGNATURE", raising=False) + + email_logger = BaseEmailLogger() + + # Test invitation email params with default template + invitation_params = await email_logger._get_email_params( + email_event=EmailEvent.new_user_invitation, + user_email="test@example.com", + event_message="New User Invitation" + ) + + assert invitation_params.subject == "LiteLLM: New User Invitation" + assert invitation_params.signature == EMAIL_FOOTER + + # Test key created email params with default template + key_params = await email_logger._get_email_params( + email_event=EmailEvent.virtual_key_created, + user_email="test@example.com", + event_message="API Key Created" + ) + + assert key_params.subject == "LiteLLM: API Key Created" + assert key_params.signature == EMAIL_FOOTER \ No newline at end of file From 0834ab79cd3e1dcb1388bbbd170fbcfc5414addc Mon Sep 17 00:00:00 2001 From: Jugal Bhatt Date: Tue, 1 Jul 2025 15:25:23 -0700 Subject: [PATCH 3/7] add docs changes --- docs/my-website/docs/proxy/config_settings.md | 6 +- docs/my-website/docs/proxy/email.md | 67 ++++++++++++++++--- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index 5db76088fa6..6660ed3b71e 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -434,9 +434,9 @@ router_settings: | DOCS_URL | The path to the Swagger API documentation. **By default this is "/"** | EMAIL_LOGO_URL | URL for the logo used in emails | EMAIL_SUPPORT_CONTACT | Support contact email address -| EMAIL_SIGNATURE | Custom signature for emails -| EMAIL_SUBJECT_INVITATION | Custom subject template for invitation emails -| EMAIL_SUBJECT_KEY_CREATED | Custom subject template for key creation emails +| EMAIL_SIGNATURE | Custom HTML footer/signature for all emails. Can include HTML tags for formatting and links. +| EMAIL_SUBJECT_INVITATION | Custom subject template for invitation emails. +| EMAIL_SUBJECT_KEY_CREATED | Custom subject template for key creation emails. | EXPERIMENTAL_MULTI_INSTANCE_RATE_LIMITING | Flag to enable new multi-instance rate limiting. **Default is False** | FIREWORKS_AI_4_B | Size parameter for Fireworks AI 4B model. Default is 4 | FIREWORKS_AI_16_B | Size parameter for Fireworks AI 16B model. Default is 16 diff --git a/docs/my-website/docs/proxy/email.md b/docs/my-website/docs/proxy/email.md index 5d5a07b156e..0102e6aed56 100644 --- a/docs/my-website/docs/proxy/email.md +++ b/docs/my-website/docs/proxy/email.md @@ -124,9 +124,7 @@ On the Create Key Modal, Select Advanced Settings > Set Send Email to True. /> - - -## Customizing Email Branding +## Email Customization :::info @@ -140,14 +138,61 @@ LiteLLM allows you to customize: - Email signature - Email subject lines for different events -Set the following in your env to customize your emails: +## Environment Variables -```shell -EMAIL_LOGO_URL="https://litellm-listing.s3.amazonaws.com/litellm_logo.png" # public url to your logo -EMAIL_SUPPORT_CONTACT="support@berri.ai" # Your company support email -EMAIL_SIGNATURE="Best regards,\nYour Company Team" # Custom email signature -EMAIL_SUBJECT_INVITATION="Welcome to {company_name}!" # Subject for invitation emails -EMAIL_SUBJECT_KEY_CREATED="Your API Key for {company_name}" # Subject for key creation emails +You can customize the following aspects of emails through environment variables: + +```bash +# Email Branding +EMAIL_LOGO_URL="https://your-company.com/logo.png" # Custom logo URL +EMAIL_SUPPORT_CONTACT="support@your-company.com" # Support contact email +EMAIL_SIGNATURE="

Best regards,
Your Company Team

Visit our website

" # Custom HTML footer/signature + +# Email Subject Lines +EMAIL_SUBJECT_INVITATION="Welcome to Your Company!" # Subject for invitation emails +EMAIL_SUBJECT_KEY_CREATED="Your API Key is Ready" # Subject for key creation emails ``` -If custom subject templates are not provided, the system will use default templates in the format "LiteLLM: {event_message}". +## HTML Support in Email Signature + +The `EMAIL_SIGNATURE` environment variable supports HTML formatting, allowing you to create rich, branded email footers. You can include: + +- Text formatting (bold, italic, etc.) +- Line breaks using `
` +- Links using `` +- Paragraphs using `

` +- Company information and social media links +- Legal disclaimers or terms of service links + +Example HTML signature: +```html +

Best regards,
The LiteLLM Team

+

+ Documentation | + GitHub +

+

+ This is an automated message from LiteLLM Proxy +

+``` + +## Default Templates + +If environment variables are not set, LiteLLM will use default templates: + +- Default logo: LiteLLM logo +- Default support contact: support@berri.ai +- Default signature: Standard LiteLLM footer +- Default subjects: "LiteLLM: \{event_message\}" (replaced with actual event message) + +## Template Variables + +When setting custom email subjects, you can use template variables that will be replaced with actual values: + +```bash +# Examples of template variable usage +EMAIL_SUBJECT_INVITATION="Welcome to \{company_name\}!" +EMAIL_SUBJECT_KEY_CREATED="Your \{company_name\} API Key" +``` + +The system will automatically replace `\{event_message\}` and other template variables with their actual values when sending emails. From a6922e68fbcb4a584053256701f597d4ad291f60 Mon Sep 17 00:00:00 2001 From: Jugal Bhatt Date: Tue, 1 Jul 2025 15:50:30 -0700 Subject: [PATCH 4/7] add verbose docs --- docs/my-website/docs/proxy/email.md | 39 +++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/docs/my-website/docs/proxy/email.md b/docs/my-website/docs/proxy/email.md index 0102e6aed56..9cd027da7f6 100644 --- a/docs/my-website/docs/proxy/email.md +++ b/docs/my-website/docs/proxy/email.md @@ -132,11 +132,40 @@ Customizing Email Branding is an Enterprise Feature [Get in touch with us for a ::: -LiteLLM allows you to customize: -- Logo on the Email -- Email support contact -- Email signature -- Email subject lines for different events +LiteLLM allows you to customize various aspects of your email notifications. Below is a complete reference of all customizable fields: + +| Field | Environment Variable | Type | Default Value | Example | Description | +|-------|-------------------|------|---------------|---------|-------------| +| Logo URL | `EMAIL_LOGO_URL` | string | LiteLLM logo | `"https://your-company.com/logo.png"` | Public URL to your company logo | +| Support Contact | `EMAIL_SUPPORT_CONTACT` | string | support@berri.ai | `"support@your-company.com"` | Email address for user support | +| Email Signature | `EMAIL_SIGNATURE` | string (HTML) | Standard LiteLLM footer | `"

Best regards,
Your Team

Visit us

"` | HTML-formatted footer for all emails | +| Invitation Subject | `EMAIL_SUBJECT_INVITATION` | string | "LiteLLM: New User Invitation" | `"Welcome to Your Company!"` | Subject line for invitation emails | +| Key Creation Subject | `EMAIL_SUBJECT_KEY_CREATED` | string | "LiteLLM: API Key Created" | `"Your New API Key is Ready"` | Subject line for key creation emails | + + +## HTML Support in Email Signature + +The `EMAIL_SIGNATURE` field supports HTML formatting for rich, branded email footers. Here's an example of what you can include: + +```html +

Best regards,
The LiteLLM Team

+

+ Documentation | + GitHub +

+

+ This is an automated message from LiteLLM Proxy +

+``` + +Supported HTML features: +- Text formatting (bold, italic, etc.) +- Line breaks (`
`) +- Links (``) +- Paragraphs (`

`) +- Basic inline styling +- Company information and social media links +- Legal disclaimers or terms of service links ## Environment Variables From fefc5fc33ac51c283b9ca1584b7f3b17c1f385e1 Mon Sep 17 00:00:00 2001 From: Jugal Bhatt Date: Tue, 1 Jul 2025 16:27:31 -0700 Subject: [PATCH 5/7] add warning message for non premium users --- .../send_emails/base_email.py | 56 ++++++-- .../test_email_customization.py | 131 +++++++++++------- 2 files changed, 130 insertions(+), 57 deletions(-) diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py index 92ad14403b6..9052aa6f8d7 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py @@ -114,21 +114,44 @@ class BaseEmailLogger(CustomLogger): Returns: EmailParams object containing logo_url, support_contact, base_url, recipient_email, subject, and signature """ - logo_url = os.getenv("EMAIL_LOGO_URL", None) or LITELLM_LOGO_URL - support_contact = os.getenv("EMAIL_SUPPORT_CONTACT", self.DEFAULT_SUPPORT_EMAIL) - base_url = os.getenv("PROXY_BASE_URL", "http://0.0.0.0:4000") - signature = os.getenv("EMAIL_SIGNATURE", EMAIL_FOOTER) + # Get email parameters with premium check for custom values + custom_logo = os.getenv("EMAIL_LOGO_URL", None) + custom_support = os.getenv("EMAIL_SUPPORT_CONTACT", None) + custom_signature = os.getenv("EMAIL_SIGNATURE", None) + custom_subject_invitation = os.getenv("EMAIL_SUBJECT_INVITATION", None) + custom_subject_key_created = os.getenv("EMAIL_SUBJECT_KEY_CREATED", None) + + # Track which custom values were not applied + unused_custom_fields = [] + + # Function to safely get custom value or default + def get_custom_or_default(custom_value: Optional[str], default_value: str, field_name: str) -> str: + if custom_value is not None: # Only check premium if trying to use custom value + from litellm.proxy.proxy_server import premium_user + if premium_user is not True: + unused_custom_fields.append(field_name) + return default_value + return custom_value + return default_value + + # Get parameters, falling back to defaults if custom values aren't allowed + logo_url = get_custom_or_default(custom_logo, LITELLM_LOGO_URL, "logo URL") + support_contact = get_custom_or_default(custom_support, self.DEFAULT_SUPPORT_EMAIL, "support contact") + base_url = os.getenv("PROXY_BASE_URL", "http://0.0.0.0:4000") # Not a premium feature + signature = get_custom_or_default(custom_signature, EMAIL_FOOTER, "email signature") # Get custom subject template based on email event type if email_event == EmailEvent.new_user_invitation: - subject_template = os.getenv( - "EMAIL_SUBJECT_INVITATION", - self.DEFAULT_SUBJECT_TEMPLATES[EmailEvent.new_user_invitation] + subject_template = get_custom_or_default( + custom_subject_invitation, + self.DEFAULT_SUBJECT_TEMPLATES[EmailEvent.new_user_invitation], + "invitation subject template" ) elif email_event == EmailEvent.virtual_key_created: - subject_template = os.getenv( - "EMAIL_SUBJECT_KEY_CREATED", - self.DEFAULT_SUBJECT_TEMPLATES[EmailEvent.virtual_key_created] + subject_template = get_custom_or_default( + custom_subject_key_created, + self.DEFAULT_SUBJECT_TEMPLATES[EmailEvent.virtual_key_created], + "key created subject template" ) else: subject_template = "LiteLLM: {event_message}" @@ -149,6 +172,19 @@ class BaseEmailLogger(CustomLogger): user_id=user_id, base_url=base_url ) + # If any custom fields were not applied, log a warning + if unused_custom_fields: + import logging + logger = logging.getLogger(__name__) + fields_str = ", ".join(unused_custom_fields) + warning_msg = ( + f"Email sent with default values instead of custom values for: {fields_str}. " + "This is an Enterprise feature. To use custom email fields, please upgrade to LiteLLM Enterprise. " + "Schedule a meeting here: https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat" + ) + logger.warning(warning_msg) + print(warning_msg) + return EmailParams( logo_url=logo_url, support_contact=support_contact, diff --git a/tests/proxy_admin_ui_tests/test_email_customization.py b/tests/proxy_admin_ui_tests/test_email_customization.py index 5d3798e9264..e72c2415fcb 100644 --- a/tests/proxy_admin_ui_tests/test_email_customization.py +++ b/tests/proxy_admin_ui_tests/test_email_customization.py @@ -7,6 +7,7 @@ sys.path.insert(0, os.path.abspath("../..")) from enterprise.litellm_enterprise.enterprise_callbacks.send_emails.base_email import BaseEmailLogger from enterprise.litellm_enterprise.types.enterprise_callbacks.send_emails import EmailEvent from litellm.integrations.email_templates.email_footer import EMAIL_FOOTER +from litellm.proxy._types import CommonProxyErrors @pytest.fixture def mock_env_vars(monkeypatch): @@ -17,34 +18,68 @@ def mock_env_vars(monkeypatch): monkeypatch.setenv("EMAIL_SUBJECT_INVITATION", "Welcome to Test Company!") monkeypatch.setenv("EMAIL_SUBJECT_KEY_CREATED", "Your Test Company API Key") monkeypatch.setenv("PROXY_BASE_URL", "http://test.com") + monkeypatch.setenv("PROXY_API_URL", "https://test.com") @pytest.mark.asyncio -async def test_get_email_params_custom_templates(mock_env_vars): - """Test that _get_email_params returns correct values with custom templates""" - email_logger = BaseEmailLogger() - - # Test invitation email params - invitation_params = await email_logger._get_email_params( - email_event=EmailEvent.new_user_invitation, - user_email="test@example.com", - event_message="New User Invitation" - ) - - assert invitation_params.subject == "Welcome to Test Company!" - assert invitation_params.signature == "Best regards,\nTest Company Team" - assert invitation_params.logo_url == "https://test-company.com/logo.png" - assert invitation_params.support_contact == "support@test-company.com" - assert invitation_params.base_url == "http://test.com" - - # Test key created email params - key_params = await email_logger._get_email_params( - email_event=EmailEvent.virtual_key_created, - user_email="test@example.com", - event_message="API Key Created" - ) - - assert key_params.subject == "Your Test Company API Key" - assert key_params.signature == "Best regards,\nTest Company Team" +async def test_get_email_params_custom_templates_premium_user(mock_env_vars): + """Test that _get_email_params returns correct values with custom templates for premium users""" + # Mock premium_user as True + with patch("litellm.proxy.proxy_server.premium_user", True): + email_logger = BaseEmailLogger() + + # Test invitation email params + invitation_params = await email_logger._get_email_params( + email_event=EmailEvent.new_user_invitation, + user_email="test@example.com", + event_message="New User Invitation" + ) + + assert invitation_params.subject == "Welcome to Test Company!" + assert invitation_params.signature == "Best regards,\nTest Company Team" + assert invitation_params.logo_url == "https://test-company.com/logo.png" + assert invitation_params.support_contact == "support@test-company.com" + assert invitation_params.base_url == "http://test.com" + + # Test key created email params + key_params = await email_logger._get_email_params( + email_event=EmailEvent.virtual_key_created, + user_email="test@example.com", + event_message="API Key Created" + ) + + assert key_params.subject == "Your Test Company API Key" + assert key_params.signature == "Best regards,\nTest Company Team" + +@pytest.mark.asyncio +async def test_get_email_params_non_premium_user(mock_env_vars): + """Test that non-premium users get default templates even when custom ones are provided""" + # Mock premium_user as False + with patch("litellm.proxy.proxy_server.premium_user", False): + email_logger = BaseEmailLogger() + + # Test invitation email params + email_params = await email_logger._get_email_params( + email_event=EmailEvent.new_user_invitation, + user_email="test@example.com", + event_message="New User Invitation" + ) + + # Should use default values even though custom values are set in env + assert email_params.subject == "LiteLLM: New User Invitation" + assert email_params.signature == EMAIL_FOOTER + assert email_params.logo_url == "https://litellm-listing.s3.amazonaws.com/litellm_logo.png" + assert email_params.support_contact == "support@berri.ai" + + + # Test key created email params + key_params = await email_logger._get_email_params( + email_event=EmailEvent.virtual_key_created, + user_email="test@example.com", + event_message="API Key Created" + ) + + assert key_params.subject == "LiteLLM: API Key Created" + assert key_params.signature == EMAIL_FOOTER @pytest.mark.asyncio async def test_get_email_params_default_templates(monkeypatch): @@ -54,24 +89,26 @@ async def test_get_email_params_default_templates(monkeypatch): monkeypatch.delenv("EMAIL_SUBJECT_KEY_CREATED", raising=False) monkeypatch.delenv("EMAIL_SIGNATURE", raising=False) - email_logger = BaseEmailLogger() - - # Test invitation email params with default template - invitation_params = await email_logger._get_email_params( - email_event=EmailEvent.new_user_invitation, - user_email="test@example.com", - event_message="New User Invitation" - ) - - assert invitation_params.subject == "LiteLLM: New User Invitation" - assert invitation_params.signature == EMAIL_FOOTER - - # Test key created email params with default template - key_params = await email_logger._get_email_params( - email_event=EmailEvent.virtual_key_created, - user_email="test@example.com", - event_message="API Key Created" - ) - - assert key_params.subject == "LiteLLM: API Key Created" - assert key_params.signature == EMAIL_FOOTER \ No newline at end of file + # Mock premium_user as True (shouldn't matter since no custom values are set) + with patch("litellm.proxy.proxy_server.premium_user", True): + email_logger = BaseEmailLogger() + + # Test invitation email params with default template + invitation_params = await email_logger._get_email_params( + email_event=EmailEvent.new_user_invitation, + user_email="test@example.com", + event_message="New User Invitation" + ) + + assert invitation_params.subject == "LiteLLM: New User Invitation" + assert invitation_params.signature == EMAIL_FOOTER + + # Test key created email params with default template + key_params = await email_logger._get_email_params( + email_event=EmailEvent.virtual_key_created, + user_email="test@example.com", + event_message="API Key Created" + ) + + assert key_params.subject == "LiteLLM: API Key Created" + assert key_params.signature == EMAIL_FOOTER \ No newline at end of file From 7e6d27e6c34bea3f3f76983ff96de79380e8db8c Mon Sep 17 00:00:00 2001 From: Jugal Bhatt Date: Tue, 1 Jul 2025 16:28:34 -0700 Subject: [PATCH 6/7] change logger --- .../enterprise_callbacks/send_emails/base_email.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py index 9052aa6f8d7..35c4ef4be9d 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py @@ -174,16 +174,15 @@ class BaseEmailLogger(CustomLogger): # If any custom fields were not applied, log a warning if unused_custom_fields: - import logging - logger = logging.getLogger(__name__) fields_str = ", ".join(unused_custom_fields) warning_msg = ( f"Email sent with default values instead of custom values for: {fields_str}. " "This is an Enterprise feature. To use custom email fields, please upgrade to LiteLLM Enterprise. " "Schedule a meeting here: https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat" ) - logger.warning(warning_msg) - print(warning_msg) + verbose_proxy_logger.warning( + f"Prisma client not found. Unable to lookup user email for user_id: {user_id}" + ) return EmailParams( logo_url=logo_url, From e53eb6eb3720d8544c957c99950cce02e66b55dc Mon Sep 17 00:00:00 2001 From: Jugal Bhatt Date: Tue, 1 Jul 2025 16:28:53 -0700 Subject: [PATCH 7/7] change logger --- .../enterprise_callbacks/send_emails/base_email.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py index 35c4ef4be9d..ec2fc285c63 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py @@ -181,7 +181,7 @@ class BaseEmailLogger(CustomLogger): "Schedule a meeting here: https://calendly.com/d/4mp-gd3-k5k/litellm-1-1-onboarding-chat" ) verbose_proxy_logger.warning( - f"Prisma client not found. Unable to lookup user email for user_id: {user_id}" + f"{warning_msg}" ) return EmailParams(