Merge pull request #12218 from BerriAI/email_template

Customizable Email template - Subject and Signature
This commit is contained in:
Jugal D. Bhatt 2025-07-02 06:15:41 +05:30 • committed by GitHub
commit 923004db1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 280 additions and 33 deletions

View file

@ -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

View file

@ -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.

View file

@ -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:

View file

@ -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()

View 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