Email budget alerts working

This commit is contained in:
yuneng-jiang
2025-12-15 16:51:25 -08:00
parent 7a18c81cf4
commit 14feda694b
9 changed files with 52 additions and 15 deletions
Binary file not shown.
Binary file not shown.
@@ -178,12 +178,11 @@ class BaseEmailLogger(CustomLogger):
"""
Send email to user when soft budget is crossed
"""
print("SENDING SOFT BUDGET ALERT EMAIL", event.json())
email_params = await self._get_email_params(
email_event=EmailEvent.virtual_key_created, # Reuse existing event type for subject template
email_event=EmailEvent.soft_budget_crossed, # Reuse existing event type for subject template
user_id=event.user_id,
user_email=event.user_email,
event_message=event.event_message or "Soft Budget Crossed",
event_message=event.event_message,
)
verbose_proxy_logger.debug(
@@ -206,7 +205,6 @@ class BaseEmailLogger(CustomLogger):
base_url=email_params.base_url,
email_support_contact=email_params.support_contact,
)
await self.send_email(
from_email=self.DEFAULT_LITELLM_EMAIL,
to_email=[email_params.recipient_email],
@@ -19,7 +19,8 @@ RESEND_API_ENDPOINT = "https://api.resend.com/emails"
class ResendEmailLogger(BaseEmailLogger):
def __init__(self):
def __init__(self, internal_usage_cache=None, **kwargs):
super().__init__(internal_usage_cache=internal_usage_cache, **kwargs)
self.async_httpx_client = get_async_httpx_client(
llm_provider=httpxSpecialProvider.LoggingCallback
)
@@ -27,7 +27,8 @@ class SendGridEmailLogger(BaseEmailLogger):
- SENDGRID_API_KEY
"""
def __init__(self):
def __init__(self, internal_usage_cache=None, **kwargs):
super().__init__(internal_usage_cache=internal_usage_cache, **kwargs)
self.async_httpx_client = get_async_httpx_client(
llm_provider=httpxSpecialProvider.LoggingCallback
)
@@ -21,7 +21,8 @@ class SMTPEmailLogger(BaseEmailLogger):
- SMTP_SENDER_EMAIL
"""
def __init__(self):
def __init__(self, internal_usage_cache=None, **kwargs):
super().__init__(internal_usage_cache=internal_usage_cache, **kwargs)
verbose_logger.debug("SMTP Email Logger initialized....")
async def send_email(
@@ -36,6 +36,7 @@ class EmailEvent(str, enum.Enum):
virtual_key_created = "Virtual Key Created"
new_user_invitation = "New User Invitation"
virtual_key_rotated = "Virtual Key Rotated"
soft_budget_crossed = "Soft Budget Crossed"
class EmailEventSettings(BaseModel):
event: EmailEvent
@@ -51,6 +52,7 @@ class DefaultEmailSettings(BaseModel):
EmailEvent.virtual_key_created: True, # On by default
EmailEvent.new_user_invitation: True, # On by default
EmailEvent.virtual_key_rotated: True, # On by default
EmailEvent.soft_budget_crossed: True, # On by default
}
)
def to_dict(self) -> Dict[str, bool]:
+3 -2
View File
@@ -1911,6 +1911,7 @@ async def _virtual_key_max_budget_check(
token=valid_token.token,
spend=valid_token.spend,
max_budget=valid_token.max_budget,
soft_budget=valid_token.soft_budget,
user_id=valid_token.user_id,
team_id=valid_token.team_id,
organization_id=valid_token.org_id,
@@ -1939,6 +1940,7 @@ async def _virtual_key_max_budget_check(
async def _virtual_key_soft_budget_check(
valid_token: UserAPIKeyAuth,
proxy_logging_obj: ProxyLogging,
user_obj: Optional[LiteLLM_UserTable] = None,
):
"""
Triggers a budget alert if the token is over it's soft budget.
@@ -1961,12 +1963,11 @@ async def _virtual_key_soft_budget_check(
team_id=valid_token.team_id,
team_alias=valid_token.team_alias,
organization_id=valid_token.org_id,
user_email=None,
user_email=user_obj.user_email if user_obj else None,
key_alias=valid_token.key_alias,
event_group=Litellm_EntityType.KEY,
)
print("VIRTUAL KEY SOFT BUDGET CHECK", call_info.json())
asyncio.create_task(
proxy_logging_obj.budget_alerts(
type="soft_budget",
+39 -6
View File
@@ -37,8 +37,14 @@ from litellm.types.utils import CallTypes, CallTypesLiteral
try:
from litellm_enterprise.enterprise_callbacks.send_emails.base_email import BaseEmailLogger
from litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email import SendGridEmailLogger
from litellm_enterprise.enterprise_callbacks.send_emails.smtp_email import SMTPEmailLogger
from litellm_enterprise.enterprise_callbacks.send_emails.resend_email import ResendEmailLogger
except ImportError:
BaseEmailLogger = None # type: ignore
SendGridEmailLogger = None # type: ignore
SMTPEmailLogger = None # type: ignore
ResendEmailLogger = None # type: ignore
try:
import backoff
@@ -133,6 +139,33 @@ def print_verbose(print_statement):
print(f"LiteLLM Proxy: {print_statement}") # noqa
def _get_email_logger_class():
"""
Determine which email logger class to use based on environment variables.
Priority: SendGrid > Resend > SMTP > BaseEmailLogger (fallback)
Returns:
The email logger class to use, or None if BaseEmailLogger is not available
"""
if BaseEmailLogger is None:
return None
# Check for SendGrid API key
if SendGridEmailLogger is not None and os.getenv("SENDGRID_API_KEY"):
return SendGridEmailLogger
# Check for Resend API key
if ResendEmailLogger is not None and os.getenv("RESEND_API_KEY"):
return ResendEmailLogger
# Check for SMTP configuration
if SMTPEmailLogger is not None and os.getenv("SMTP_HOST"):
return SMTPEmailLogger
# Fallback to BaseEmailLogger (though it won't actually send emails)
return BaseEmailLogger
class InternalUsageCache:
def __init__(self, dual_cache: DualCache):
self.dual_cache: DualCache = dual_cache
@@ -273,9 +306,12 @@ class ProxyLogging:
)
self.email_logging_instance: Optional[Any] = None
if BaseEmailLogger is not None:
self.email_logging_instance = BaseEmailLogger(
internal_usage_cache=self.internal_usage_cache.dual_cache,
)
email_logger_class = _get_email_logger_class()
if email_logger_class is not None:
# All email logger classes now accept internal_usage_cache
self.email_logging_instance = email_logger_class(
internal_usage_cache=self.internal_usage_cache.dual_cache,
)
self.premium_user = premium_user
self.service_logging_obj = ServiceLogging()
self.db_spend_update_writer = DBSpendUpdateWriter()
@@ -1166,8 +1202,6 @@ class ProxyLogging:
],
user_info: CallInfo,
):
print("BUDGET ALERTS", type, user_info)
print("ALERTING", self.alerting)
if self.alerting is None:
# do nothing if alerting is not switched on
return
@@ -1179,7 +1213,6 @@ class ProxyLogging:
)
if "email" in self.alerting and self.email_logging_instance is not None:
print("BUDGET ALERTS EMAIL", type, user_info)
await self.email_logging_instance.budget_alerts(
type=type,
user_info=user_info,