mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-02 08:21:53 +00:00
Email budget alerts working
This commit is contained in:
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]:
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user