mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-17 23:51:30 +00:00
fix(proxy): auto-send user invitation emails when email is configured
When send_invite_email is not explicitly set (None/omitted), auto-detect whether to send by checking if email infrastructure is configured and the user has an email address. Previously, emails were silently skipped because the field defaulted to None and both V1/V2 paths required strict `is True`. Fixes #20499 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
b8f7d61400
commit
4a3c69c43d
3 changed files with 194 additions and 4 deletions
|
|
@ -90,9 +90,28 @@ litellm_settings:
|
|||
|
||||
### 2. Create a new user
|
||||
|
||||
On the LiteLLM Proxy UI, go to users > create a new user.
|
||||
On the LiteLLM Proxy UI, go to users > create a new user with an email address.
|
||||
|
||||
After creating a new user, they will automatically receive an invitation email if you have configured an email integration (step 1 above). No additional flag or toggle is needed — the proxy detects that email is configured and sends the invite automatically.
|
||||
|
||||
When using the API directly, you can also explicitly control this behavior with the `send_invite_email` parameter:
|
||||
|
||||
```shell
|
||||
curl -X POST '<your_proxy_base_url>/user/new' \
|
||||
-H 'Authorization: Bearer <your_api_key>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"user_email": "user@example.com",
|
||||
"send_invite_email": true
|
||||
}'
|
||||
```
|
||||
|
||||
| `send_invite_email` value | Behavior |
|
||||
|--------------------------|----------|
|
||||
| `true` | Always send invitation email |
|
||||
| `false` | Never send invitation email |
|
||||
| omitted / `null` | Auto-detect: send if email integration is configured and user has an email |
|
||||
|
||||
After creating a new user, they will receive an email invite a the email you specified when creating the user.
|
||||
|
||||
### 3. Configure Budget Alerts (Optional)
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,59 @@ from litellm.proxy.management_helpers.audit_logs import create_audit_log_for_upd
|
|||
|
||||
|
||||
class UserManagementEventHooks:
|
||||
@staticmethod
|
||||
def _is_email_sending_enabled() -> bool:
|
||||
"""
|
||||
Check if email sending is enabled via v2 enterprise loggers or v0 alerting config.
|
||||
|
||||
Returns True only if email is actually configured, preventing any email
|
||||
processing when the user has not opted in.
|
||||
"""
|
||||
try:
|
||||
from litellm_enterprise.enterprise_callbacks.send_emails.base_email import (
|
||||
BaseEmailLogger,
|
||||
)
|
||||
|
||||
initialized_email_loggers = (
|
||||
litellm.logging_callback_manager.get_custom_loggers_for_type(
|
||||
callback_type=BaseEmailLogger
|
||||
)
|
||||
)
|
||||
if len(initialized_email_loggers) > 0:
|
||||
return True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from litellm.proxy.proxy_server import general_settings
|
||||
|
||||
if "email" in general_settings.get("alerting", []):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _should_send_user_invitation_email(
|
||||
data: NewUserRequest,
|
||||
response: NewUserResponse,
|
||||
) -> bool:
|
||||
"""
|
||||
Determine whether a user invitation email should be sent.
|
||||
|
||||
- send_invite_email=True -> always send
|
||||
- send_invite_email=False -> never send
|
||||
- send_invite_email=None -> auto-detect: send if email infra is configured
|
||||
AND the user has an email address
|
||||
"""
|
||||
if data.send_invite_email is True:
|
||||
return True
|
||||
if data.send_invite_email is False:
|
||||
return False
|
||||
if not UserManagementEventHooks._is_email_sending_enabled():
|
||||
return False
|
||||
if not response.user_email:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def async_user_created_hook(
|
||||
data: NewUserRequest,
|
||||
|
|
@ -105,6 +158,10 @@ class UserManagementEventHooks:
|
|||
key_alias=response.key_alias,
|
||||
)
|
||||
|
||||
should_send_email = UserManagementEventHooks._should_send_user_invitation_email(
|
||||
data=data, response=response
|
||||
)
|
||||
|
||||
#########################################################
|
||||
########## V2 USER INVITATION EMAIL ################
|
||||
#########################################################
|
||||
|
|
@ -121,7 +178,7 @@ class UserManagementEventHooks:
|
|||
)
|
||||
use_enterprise_email_hooks = False
|
||||
|
||||
if use_enterprise_email_hooks and (data.send_invite_email is True):
|
||||
if use_enterprise_email_hooks and should_send_email:
|
||||
initialized_email_loggers = litellm.logging_callback_manager.get_custom_loggers_for_type(
|
||||
callback_type=BaseEmailLogger # type: ignore
|
||||
)
|
||||
|
|
@ -135,7 +192,7 @@ class UserManagementEventHooks:
|
|||
#########################################################
|
||||
########## LEGACY V1 USER INVITATION EMAIL ################
|
||||
#########################################################
|
||||
if data.send_invite_email is True:
|
||||
if should_send_email:
|
||||
await UserManagementEventHooks.send_legacy_v1_user_invitation_email(
|
||||
data=data,
|
||||
response=response,
|
||||
|
|
|
|||
|
|
@ -152,3 +152,117 @@ async def test_v1_key_generation_no_email_when_send_invite_email_false():
|
|||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
mock_send_key_created_email.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_v1_user_creation_sends_email_when_send_invite_email_none_and_email_configured():
|
||||
"""
|
||||
When send_invite_email is None (default) and email alerting is configured
|
||||
and the user has an email address, an invitation email should be sent.
|
||||
"""
|
||||
mock_slack_alerting = MagicMock()
|
||||
mock_slack_alerting.send_key_created_or_user_invited_email = AsyncMock()
|
||||
mock_proxy_logging_obj = MagicMock()
|
||||
mock_proxy_logging_obj.slack_alerting_instance = mock_slack_alerting
|
||||
|
||||
with patch(
|
||||
"litellm.logging_callback_manager.get_custom_loggers_for_type", return_value=[]
|
||||
):
|
||||
mock_proxy_server = SimpleNamespace(
|
||||
general_settings={"alerting": ["email"]},
|
||||
proxy_logging_obj=mock_proxy_logging_obj,
|
||||
litellm_proxy_admin_name="admin-user",
|
||||
)
|
||||
with patch.dict(sys.modules, {"litellm.proxy.proxy_server": mock_proxy_server}):
|
||||
data = NewUserRequest(
|
||||
user_email="test@example.com",
|
||||
)
|
||||
response = NewUserResponse(
|
||||
user_id="test-user",
|
||||
user_email="test@example.com",
|
||||
key="sk-test-key",
|
||||
)
|
||||
user_api_key_dict = UserAPIKeyAuth(
|
||||
user_id="admin-user", api_key="admin-key"
|
||||
)
|
||||
await UserManagementEventHooks.async_send_user_invitation_email(
|
||||
data=data,
|
||||
response=response,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
mock_slack_alerting.send_key_created_or_user_invited_email.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_v1_user_creation_no_email_when_send_invite_email_none_and_email_not_configured():
|
||||
"""
|
||||
When send_invite_email is None (default) and email alerting is NOT configured,
|
||||
no invitation email should be sent.
|
||||
"""
|
||||
mock_slack_alerting = MagicMock()
|
||||
mock_slack_alerting.send_key_created_or_user_invited_email = AsyncMock()
|
||||
mock_proxy_logging_obj = MagicMock()
|
||||
mock_proxy_logging_obj.slack_alerting_instance = mock_slack_alerting
|
||||
|
||||
with patch(
|
||||
"litellm.logging_callback_manager.get_custom_loggers_for_type", return_value=[]
|
||||
):
|
||||
mock_proxy_server = SimpleNamespace(
|
||||
general_settings={},
|
||||
proxy_logging_obj=mock_proxy_logging_obj,
|
||||
litellm_proxy_admin_name="admin-user",
|
||||
)
|
||||
with patch.dict(sys.modules, {"litellm.proxy.proxy_server": mock_proxy_server}):
|
||||
data = NewUserRequest(
|
||||
user_email="test@example.com",
|
||||
)
|
||||
response = NewUserResponse(
|
||||
user_id="test-user",
|
||||
user_email="test@example.com",
|
||||
key="sk-test-key",
|
||||
)
|
||||
user_api_key_dict = UserAPIKeyAuth(
|
||||
user_id="admin-user", api_key="admin-key"
|
||||
)
|
||||
await UserManagementEventHooks.async_send_user_invitation_email(
|
||||
data=data,
|
||||
response=response,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
mock_slack_alerting.send_key_created_or_user_invited_email.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_v1_user_creation_no_email_when_send_invite_email_none_and_no_user_email():
|
||||
"""
|
||||
When send_invite_email is None (default) and email alerting is configured
|
||||
but the user has no email address, no invitation email should be sent.
|
||||
"""
|
||||
mock_slack_alerting = MagicMock()
|
||||
mock_slack_alerting.send_key_created_or_user_invited_email = AsyncMock()
|
||||
mock_proxy_logging_obj = MagicMock()
|
||||
mock_proxy_logging_obj.slack_alerting_instance = mock_slack_alerting
|
||||
|
||||
with patch(
|
||||
"litellm.logging_callback_manager.get_custom_loggers_for_type", return_value=[]
|
||||
):
|
||||
mock_proxy_server = SimpleNamespace(
|
||||
general_settings={"alerting": ["email"]},
|
||||
proxy_logging_obj=mock_proxy_logging_obj,
|
||||
litellm_proxy_admin_name="admin-user",
|
||||
)
|
||||
with patch.dict(sys.modules, {"litellm.proxy.proxy_server": mock_proxy_server}):
|
||||
data = NewUserRequest()
|
||||
response = NewUserResponse(
|
||||
user_id="test-user",
|
||||
key="sk-test-key",
|
||||
)
|
||||
user_api_key_dict = UserAPIKeyAuth(
|
||||
user_id="admin-user", api_key="admin-key"
|
||||
)
|
||||
await UserManagementEventHooks.async_send_user_invitation_email(
|
||||
data=data,
|
||||
response=response,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
)
|
||||
mock_slack_alerting.send_key_created_or_user_invited_email.assert_not_called()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue