mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Merge pull request #12218 from BerriAI/email_template
Customizable Email template - Subject and Signature
This commit is contained in:
commit
923004db1b
5 changed files with 280 additions and 33 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 | `"<p>Best regards,<br/>Your Team</p><p><a href='https://your-company.com'>Visit us</a></p>"` | 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 |
|
||||
|
||||
```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
|
||||
|
||||
## 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
|
||||
<p>Best regards,<br/>The LiteLLM Team</p>
|
||||
<p>
|
||||
<a href='https://docs.litellm.ai'>Documentation</a> |
|
||||
<a href='https://github.com/BerriAI/litellm'>GitHub</a>
|
||||
</p>
|
||||
<p style='font-size: 12px; color: #666;'>
|
||||
This is an automated message from LiteLLM Proxy
|
||||
</p>
|
||||
```
|
||||
|
||||
Supported HTML features:
|
||||
- Text formatting (bold, italic, etc.)
|
||||
- Line breaks (`<br/>`)
|
||||
- Links (`<a href='...'>`)
|
||||
- Paragraphs (`<p>`)
|
||||
- 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="<p>Best regards,<br/>Your Company Team</p><p><a href='https://your-company.com'>Visit our website</a></p>" # 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
|
||||
```
|
||||
|
||||
## 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 `<br/>`
|
||||
- Links using `<a href='...'>`
|
||||
- Paragraphs using `<p>`
|
||||
- Company information and social media links
|
||||
- Legal disclaimers or terms of service links
|
||||
|
||||
Example HTML signature:
|
||||
```html
|
||||
<p>Best regards,<br/>The LiteLLM Team</p>
|
||||
<p>
|
||||
<a href='https://docs.litellm.ai'>Documentation</a> |
|
||||
<a href='https://github.com/BerriAI/litellm'>GitHub</a>
|
||||
</p>
|
||||
<p style='font-size: 12px; color: #666;'>
|
||||
This is an automated message from LiteLLM Proxy
|
||||
</p>
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
114
tests/proxy_admin_ui_tests/test_email_customization.py
Normal file
114
tests/proxy_admin_ui_tests/test_email_customization.py
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue