diff --git a/enterprise/dist/litellm_enterprise-0.1.26-py3-none-any.whl b/enterprise/dist/litellm_enterprise-0.1.26-py3-none-any.whl new file mode 100644 index 0000000000..e4cfac6553 Binary files /dev/null and b/enterprise/dist/litellm_enterprise-0.1.26-py3-none-any.whl differ diff --git a/enterprise/dist/litellm_enterprise-0.1.26.tar.gz b/enterprise/dist/litellm_enterprise-0.1.26.tar.gz new file mode 100644 index 0000000000..c8e0081ff1 Binary files /dev/null and b/enterprise/dist/litellm_enterprise-0.1.26.tar.gz differ 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 e02a14613e..f50f2e3157 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/base_email.py @@ -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], diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/resend_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/resend_email.py index 8119e4a7ef..7593e66aa4 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/resend_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/resend_email.py @@ -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 ) diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py index dfde9ce329..8fc2d66d53 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py @@ -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 ) diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py index 4ede8ee59f..8efdaf231b 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/send_emails/smtp_email.py @@ -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( diff --git a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py index 736aaff1f7..86f1b74b31 100644 --- a/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py +++ b/enterprise/litellm_enterprise/types/enterprise_callbacks/send_emails.py @@ -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]: diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 936ab05f16..0a6310c3bc 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -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", diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index e1a33885b4..2c6d818696 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -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,