From 44eb2ea56e882c29f7ad17f9b9dee47c29efb183 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Fri, 17 Apr 2026 17:54:16 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20address=20Greptile=20review=20=E2=80=94?= =?UTF-8?q?=20empty=20recipients=20guard,=20type=20annotation,=20task=20pr?= =?UTF-8?q?e-filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Guard empty recipients in _handle_multi_threshold_max_budget_alert: log warning and skip instead of falling through to old path error loop - Widen max_budget_alert_emails type to Dict[str, Union[str, List[str]]] to match _parse_email_list runtime behavior (accepts comma-separated strings) - Pre-filter asyncio.create_task with min threshold check to avoid unnecessary task allocation on every request when spend is below all configured thresholds --- .../enterprise_callbacks/send_emails/base_email.py | 9 ++++++++- litellm/proxy/_types.py | 2 +- litellm/proxy/auth/auth_checks.py | 9 ++++++++- tests/test_litellm/proxy/auth/test_auth_checks.py | 6 +++--- 4 files changed, 20 insertions(+), 6 deletions(-) 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 3616325383..533fb13493 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py @@ -615,7 +615,14 @@ class BaseEmailLogger(CustomLogger): emails = _parse_email_list(raw_emails) if user_info.user_email: emails.append(user_info.user_email) - recipient_emails = list(set(emails)) if emails else None + if not emails: + verbose_proxy_logger.warning( + "No recipients for %d%% threshold on key %s, skipping alert", + threshold_pct, + _id, + ) + continue + recipient_emails = list(set(emails)) event_message = f"Max Budget Alert - {threshold_pct}% of Maximum Budget Reached" webhook_event = WebhookEvent( diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index d3c8324870..d12414c19c 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3095,7 +3095,7 @@ class CallInfo(LiteLLMPydanticObjectBase): default=None, description="Additional email addresses to send alerts to (e.g., from team metadata)", ) - max_budget_alert_emails: Optional[Dict[str, List[str]]] = Field( + max_budget_alert_emails: Optional[Dict[str, Union[str, List[str]]]] = Field( default=None, description="Map of threshold percentage to email recipients (e.g., {'50': ['a@co.com'], '75': ['a@co.com', 'b@co.com']})", ) diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 19751f6edc..65b75dffb6 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -2969,7 +2969,14 @@ async def _virtual_key_max_budget_alert_check( ) or litellm.default_key_max_budget_alert_emails if isinstance(alert_email_config, dict) and alert_email_config: - # New path: pass the map through, let the email handler decide what to fire + # New path: only create task if spend has crossed the lowest threshold + min_pct = min( + (int(k) for k in alert_email_config if k.isdigit()), + default=None, + ) + if min_pct is None or valid_token.spend < valid_token.max_budget * (min_pct / 100.0): + return + call_info = CallInfo( token=valid_token.token, spend=valid_token.spend, diff --git a/tests/test_litellm/proxy/auth/test_auth_checks.py b/tests/test_litellm/proxy/auth/test_auth_checks.py index 0f7ffa541a..d74c4883f6 100644 --- a/tests/test_litellm/proxy/auth/test_auth_checks.py +++ b/tests/test_litellm/proxy/auth/test_auth_checks.py @@ -1562,7 +1562,7 @@ async def test_virtual_key_max_budget_alert_check_with_multi_threshold_map(): } valid_token = UserAPIKeyAuth( token="test-token", - spend=30.0, + spend=60.0, max_budget=100.0, user_id="test-user", key_alias="test-key", @@ -1670,7 +1670,7 @@ async def test_virtual_key_max_budget_alert_check_global_fallback(): } valid_token = UserAPIKeyAuth( token="test-token", - spend=30.0, + spend=60.0, max_budget=100.0, user_id="test-user", key_alias="test-key", @@ -1709,7 +1709,7 @@ async def test_virtual_key_max_budget_alert_check_per_key_overrides_global(): valid_token = UserAPIKeyAuth( token="test-token", - spend=30.0, + spend=60.0, max_budget=100.0, user_id="test-user", key_alias="test-key",