fix: address Greptile review — empty recipients guard, type annotation, task pre-filter

- 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
This commit is contained in:
Ryan Crabbe
2026-04-17 17:54:16 -07:00
parent 41a719a537
commit 44eb2ea56e
4 changed files with 20 additions and 6 deletions
@@ -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(
+1 -1
View File
@@ -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']})",
)
+8 -1
View File
@@ -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,
@@ -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",