diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index db011a5825c..6660ed3b71e 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 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 4eb35367dbe..9cd027da7f6 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 @@ -134,13 +132,96 @@ Customizing Email Branding is an Enterprise Feature [Get in touch with us for a ::: -LiteLLM allows you to customize the: -- Logo on the Email -- Email support contact +LiteLLM allows you to customize various aspects of your email notifications. Below is a complete reference of all customizable fields: -Set the following in your env to customize your emails +| 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
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 (``) +- Basic inline styling +- Company information and social media links +- Legal disclaimers or terms of service links + +## Environment Variables + +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
` +- 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. 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..ec2fc285c63 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,57 @@ 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") + # 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 = 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 = 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}" + + subject = subject_template.format(event_message=event_message) if event_message else "LiteLLM Notification" recipient_email: Optional[ str @@ -127,11 +172,25 @@ 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: + 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" + ) + verbose_proxy_logger.warning( + f"{warning_msg}" + ) + return EmailParams( logo_url=logo_url, 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..2d3c8adf2c6 100644 --- a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py +++ b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py @@ -5,19 +5,19 @@ from pydantic import BaseModel, Field from litellm.proxy._types import WebhookEvent - class EmailParams(BaseModel): logo_url: str support_contact: str base_url: str recipient_email: str + subject: str + signature: str 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 """ @@ -26,35 +26,25 @@ 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..e72c2415fcb --- /dev/null +++ b/tests/proxy_admin_ui_tests/test_email_customization.py @@ -0,0 +1,114 @@ +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 +from litellm.proxy._types import CommonProxyErrors + +@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") + monkeypatch.setenv("PROXY_API_URL", "https://test.com") + +@pytest.mark.asyncio +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): + """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) + + # 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