mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-04 02:23:45 +00:00
Merge pull request #17775 from BerriAI/litellm_sendgrid
[Feature] Sendgrid integration
This commit is contained in:
@@ -819,6 +819,8 @@ router_settings:
|
||||
| SMTP_SENDER_LOGO | Logo used in emails sent via SMTP
|
||||
| SMTP_TLS | Flag to enable or disable TLS for SMTP connections
|
||||
| SMTP_USERNAME | Username for SMTP authentication (do not set if SMTP does not require auth)
|
||||
| SENDGRID_API_KEY | API key for SendGrid email service
|
||||
| SENDGRID_SENDER_EMAIL | Email address used as the sender in SendGrid email transactions
|
||||
| SPEND_LOGS_URL | URL for retrieving spend logs
|
||||
| SPEND_LOG_CLEANUP_BATCH_SIZE | Number of logs deleted per batch during cleanup. Default is 1000
|
||||
| SSL_CERTIFICATE | Path to the SSL certificate file
|
||||
|
||||
@@ -68,6 +68,23 @@ litellm_settings:
|
||||
callbacks: ["resend_email"]
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="sendgrid" label="SendGrid API">
|
||||
|
||||
Add `sendgrid_email` to your proxy config.yaml under `litellm_settings`
|
||||
|
||||
set the following env variables
|
||||
|
||||
```shell showLineNumbers
|
||||
SENDGRID_API_KEY="SG.1234"
|
||||
SENDGRID_SENDER_EMAIL="notifications@your-domain.com"
|
||||
```
|
||||
|
||||
```yaml showLineNumbers title="proxy_config.yaml"
|
||||
litellm_settings:
|
||||
callbacks: ["sendgrid_email"]
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
LiteLLM x SendGrid email integration.
|
||||
|
||||
Docs: https://docs.sendgrid.com/api-reference/mail-send/mail-send
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.llms.custom_httpx.http_handler import (
|
||||
get_async_httpx_client,
|
||||
httpxSpecialProvider,
|
||||
)
|
||||
|
||||
from .base_email import BaseEmailLogger
|
||||
|
||||
|
||||
SENDGRID_API_ENDPOINT = "https://api.sendgrid.com/v3/mail/send"
|
||||
|
||||
|
||||
class SendGridEmailLogger(BaseEmailLogger):
|
||||
"""
|
||||
Send emails using SendGrid's Mail Send API.
|
||||
|
||||
Required env vars:
|
||||
- SENDGRID_API_KEY
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.async_httpx_client = get_async_httpx_client(
|
||||
llm_provider=httpxSpecialProvider.LoggingCallback
|
||||
)
|
||||
self.sendgrid_api_key = os.getenv("SENDGRID_API_KEY")
|
||||
self.sendgrid_sender_email = os.getenv("SENDGRID_SENDER_EMAIL")
|
||||
verbose_logger.debug("SendGrid Email Logger initialized.")
|
||||
|
||||
async def send_email(
|
||||
self,
|
||||
from_email: str,
|
||||
to_email: List[str],
|
||||
subject: str,
|
||||
html_body: str,
|
||||
):
|
||||
"""
|
||||
Send an email via SendGrid.
|
||||
"""
|
||||
if not self.sendgrid_api_key:
|
||||
raise ValueError("SENDGRID_API_KEY is not set")
|
||||
|
||||
sender_email = self.sendgrid_sender_email or from_email
|
||||
verbose_logger.debug(
|
||||
f"Sending email via SendGrid from {sender_email} to {to_email} with subject {subject}"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"from": {"email": sender_email},
|
||||
"personalizations": [
|
||||
{
|
||||
"to": [{"email": email} for email in to_email],
|
||||
"subject": subject,
|
||||
}
|
||||
],
|
||||
"content": [
|
||||
{
|
||||
"type": "text/html",
|
||||
"value": html_body,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
response = await self.async_httpx_client.post(
|
||||
url=SENDGRID_API_ENDPOINT,
|
||||
json=payload,
|
||||
headers={"Authorization": f"Bearer {self.sendgrid_api_key}"},
|
||||
)
|
||||
|
||||
verbose_logger.debug(
|
||||
f"SendGrid response status={response.status_code}, body={response.text}"
|
||||
)
|
||||
return
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "litellm-enterprise"
|
||||
version = "0.1.23"
|
||||
version = "0.1.25"
|
||||
description = "Package for LiteLLM Enterprise features"
|
||||
authors = ["BerriAI"]
|
||||
readme = "README.md"
|
||||
@@ -22,7 +22,7 @@ requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.commitizen]
|
||||
version = "0.1.23"
|
||||
version = "0.1.25"
|
||||
version_files = [
|
||||
"pyproject.toml:version",
|
||||
"../requirements.txt:litellm-enterprise==",
|
||||
|
||||
@@ -159,6 +159,7 @@ _custom_logger_compatible_callbacks_literal = Literal[
|
||||
"anthropic_cache_control_hook",
|
||||
"generic_api",
|
||||
"resend_email",
|
||||
"sendgrid_email",
|
||||
"smtp_email",
|
||||
"deepeval",
|
||||
"s3_v2",
|
||||
|
||||
@@ -102,6 +102,9 @@ class CustomLoggerRegistry:
|
||||
from litellm_enterprise.enterprise_callbacks.send_emails.resend_email import (
|
||||
ResendEmailLogger,
|
||||
)
|
||||
from litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email import (
|
||||
SendGridEmailLogger,
|
||||
)
|
||||
from litellm_enterprise.enterprise_callbacks.send_emails.smtp_email import (
|
||||
SMTPEmailLogger,
|
||||
)
|
||||
@@ -114,6 +117,7 @@ class CustomLoggerRegistry:
|
||||
"pagerduty": PagerDutyAlerting,
|
||||
"generic_api": GenericAPILogger,
|
||||
"resend_email": ResendEmailLogger,
|
||||
"sendgrid_email": SendGridEmailLogger,
|
||||
"smtp_email": SMTPEmailLogger,
|
||||
}
|
||||
CALLBACK_CLASS_STR_TO_CLASS_TYPE.update(enterprise_loggers)
|
||||
|
||||
@@ -172,6 +172,9 @@ try:
|
||||
from litellm_enterprise.enterprise_callbacks.send_emails.resend_email import (
|
||||
ResendEmailLogger,
|
||||
)
|
||||
from litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email import (
|
||||
SendGridEmailLogger,
|
||||
)
|
||||
from litellm_enterprise.enterprise_callbacks.send_emails.smtp_email import (
|
||||
SMTPEmailLogger,
|
||||
)
|
||||
@@ -190,6 +193,7 @@ except Exception as e:
|
||||
)
|
||||
GenericAPILogger = CustomLogger # type: ignore
|
||||
ResendEmailLogger = CustomLogger # type: ignore
|
||||
SendGridEmailLogger = CustomLogger # type: ignore
|
||||
SMTPEmailLogger = CustomLogger # type: ignore
|
||||
PagerDutyAlerting = CustomLogger # type: ignore
|
||||
EnterpriseCallbackControls = None # type: ignore
|
||||
@@ -3904,6 +3908,13 @@ def _init_custom_logger_compatible_class( # noqa: PLR0915
|
||||
resend_email_logger = ResendEmailLogger()
|
||||
_in_memory_loggers.append(resend_email_logger)
|
||||
return resend_email_logger # type: ignore
|
||||
elif logging_integration == "sendgrid_email":
|
||||
for callback in _in_memory_loggers:
|
||||
if isinstance(callback, SendGridEmailLogger):
|
||||
return callback
|
||||
sendgrid_email_logger = SendGridEmailLogger()
|
||||
_in_memory_loggers.append(sendgrid_email_logger)
|
||||
return sendgrid_email_logger # type: ignore
|
||||
elif logging_integration == "smtp_email":
|
||||
for callback in _in_memory_loggers:
|
||||
if isinstance(callback, SMTPEmailLogger):
|
||||
@@ -4144,6 +4155,10 @@ def get_custom_logger_compatible_class( # noqa: PLR0915
|
||||
for callback in _in_memory_loggers:
|
||||
if isinstance(callback, ResendEmailLogger):
|
||||
return callback
|
||||
elif logging_integration == "sendgrid_email":
|
||||
for callback in _in_memory_loggers:
|
||||
if isinstance(callback, SendGridEmailLogger):
|
||||
return callback
|
||||
elif logging_integration == "smtp_email":
|
||||
for callback in _in_memory_loggers:
|
||||
if isinstance(callback, SMTPEmailLogger):
|
||||
|
||||
Generated
+4
-4
@@ -2720,13 +2720,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "litellm-enterprise"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
description = "Package for LiteLLM Enterprise features"
|
||||
optional = true
|
||||
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
|
||||
files = [
|
||||
{file = "litellm_enterprise-0.1.23-py3-none-any.whl", hash = "sha256:d803ce3ef79494f21447368f1f4e05669183714e5081da9c27a05b1770eb1422"},
|
||||
{file = "litellm_enterprise-0.1.23.tar.gz", hash = "sha256:0171e1d10c10b29e663d03a6b84c77465e58fd1923ecd0f89796622ffb5c7bb0"},
|
||||
{file = "litellm_enterprise-0.1.24-py3-none-any.whl", hash = "sha256:82548d0377282c8491d695e6b891e0930910ab410ac10f01773c13c263ecef3f"},
|
||||
{file = "litellm_enterprise-0.1.24.tar.gz", hash = "sha256:e009b9e1be09735c58458b356a9d2b942f468b4a934c0cb6ace8c43c6f43ba0f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6973,4 +6973,4 @@ utils = ["numpydoc"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9,<4.0"
|
||||
content-hash = "fec0ac9f9222e9952c6244bf874fac20201ac1e14e435d3201611ab4f882c4d7"
|
||||
content-hash = "ddc452ea7bacb386fe494f5a2b8f6bfa4715eb0a16ce43c23cff731076b2cc67"
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.
|
||||
mcp = {version = "^1.21.2", optional = true, python = ">=3.10"}
|
||||
litellm-proxy-extras = {version = "0.4.12", optional = true}
|
||||
rich = {version = "13.7.1", optional = true}
|
||||
litellm-enterprise = {version = "0.1.23", optional = true}
|
||||
litellm-enterprise = {version = "0.1.25", optional = true}
|
||||
diskcache = {version = "^5.6.1", optional = true}
|
||||
polars = {version = "^1.31.0", optional = true, python = ">=3.10"}
|
||||
semantic-router = {version = ">=0.1.12", optional = true, python = ">=3.9,<3.14"}
|
||||
|
||||
+1
-1
@@ -64,4 +64,4 @@ soundfile==0.12.1 # for audio file processing
|
||||
########################
|
||||
# LITELLM ENTERPRISE DEPENDENCIES
|
||||
########################
|
||||
litellm-enterprise==0.1.23
|
||||
litellm-enterprise==0.1.25
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import os
|
||||
import sys
|
||||
import unittest.mock as mock
|
||||
|
||||
import pytest
|
||||
from httpx import Response
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../.."))
|
||||
|
||||
from litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email import (
|
||||
SendGridEmailLogger,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_env_vars():
|
||||
with mock.patch.dict(os.environ, {"SENDGRID_API_KEY": "test_api_key"}):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_httpx_client():
|
||||
with mock.patch(
|
||||
"litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email.get_async_httpx_client"
|
||||
) as mock_client:
|
||||
mock_response = mock.AsyncMock(spec=Response)
|
||||
mock_response.status_code = 202
|
||||
mock_response.text = "accepted"
|
||||
|
||||
mock_async_client = mock.AsyncMock()
|
||||
mock_async_client.post.return_value = mock_response
|
||||
mock_client.return_value = mock_async_client
|
||||
|
||||
yield mock_async_client
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_email_success(mock_env_vars, mock_httpx_client):
|
||||
logger = SendGridEmailLogger()
|
||||
|
||||
from_email = "test@example.com"
|
||||
to_email = ["recipient@example.com"]
|
||||
subject = "Test Subject"
|
||||
html_body = "<p>Test email body</p>"
|
||||
|
||||
await logger.send_email(
|
||||
from_email=from_email, to_email=to_email, subject=subject, html_body=html_body
|
||||
)
|
||||
|
||||
mock_httpx_client.post.assert_called_once()
|
||||
call_args = mock_httpx_client.post.call_args
|
||||
assert call_args[1]["url"] == "https://api.sendgrid.com/v3/mail/send"
|
||||
|
||||
payload = call_args[1]["json"]
|
||||
assert payload["from"] == {"email": from_email}
|
||||
assert payload["personalizations"][0]["to"] == [{"email": to_email[0]}]
|
||||
assert payload["personalizations"][0]["subject"] == subject
|
||||
assert payload["content"][0]["type"] == "text/html"
|
||||
assert payload["content"][0]["value"] == html_body
|
||||
|
||||
assert call_args[1]["headers"] == {"Authorization": "Bearer test_api_key"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_email_missing_api_key(mock_httpx_client):
|
||||
with mock.patch.dict(os.environ, {}, clear=True):
|
||||
logger = SendGridEmailLogger()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await logger.send_email(
|
||||
from_email="test@example.com",
|
||||
to_email=["recipient@example.com"],
|
||||
subject="Test Subject",
|
||||
html_body="<p>Test email body</p>",
|
||||
)
|
||||
|
||||
mock_httpx_client.post.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_email_multiple_recipients(mock_env_vars, mock_httpx_client):
|
||||
logger = SendGridEmailLogger()
|
||||
|
||||
from_email = "test@example.com"
|
||||
to_email = ["recipient1@example.com", "recipient2@example.com"]
|
||||
subject = "Test Subject"
|
||||
html_body = "<p>Test email body</p>"
|
||||
|
||||
await logger.send_email(
|
||||
from_email=from_email, to_email=to_email, subject=subject, html_body=html_body
|
||||
)
|
||||
|
||||
mock_httpx_client.post.assert_called_once()
|
||||
payload = mock_httpx_client.post.call_args[1]["json"]
|
||||
|
||||
assert payload["personalizations"][0]["to"] == [
|
||||
{"email": "recipient1@example.com"},
|
||||
{"email": "recipient2@example.com"},
|
||||
]
|
||||
Reference in New Issue
Block a user