litellm-enterprise 0.1.26__py3-none-any.whl → 0.1.27__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- litellm_enterprise/enterprise_callbacks/send_emails/base_email.py +100 -2
- litellm_enterprise/proxy/hooks/managed_files.py +20 -2
- litellm_enterprise/types/enterprise_callbacks/send_emails.py +2 -0
- {litellm_enterprise-0.1.26.dist-info → litellm_enterprise-0.1.27.dist-info}/METADATA +1 -1
- {litellm_enterprise-0.1.26.dist-info → litellm_enterprise-0.1.27.dist-info}/RECORD +7 -7
- {litellm_enterprise-0.1.26.dist-info → litellm_enterprise-0.1.27.dist-info}/WHEEL +0 -0
- {litellm_enterprise-0.1.26.dist-info → litellm_enterprise-0.1.27.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -28,11 +28,16 @@ from litellm.integrations.email_templates.user_invitation_email import (
|
|
|
28
28
|
USER_INVITATION_EMAIL_TEMPLATE,
|
|
29
29
|
)
|
|
30
30
|
from litellm.integrations.email_templates.templates import (
|
|
31
|
+
MAX_BUDGET_ALERT_EMAIL_TEMPLATE,
|
|
31
32
|
SOFT_BUDGET_ALERT_EMAIL_TEMPLATE,
|
|
32
33
|
)
|
|
33
34
|
from litellm.proxy._types import CallInfo, InvitationNew, UserAPIKeyAuth, WebhookEvent
|
|
34
35
|
from litellm.secret_managers.main import get_secret_bool
|
|
35
36
|
from litellm.types.integrations.slack_alerting import LITELLM_LOGO_URL
|
|
37
|
+
from litellm.constants import (
|
|
38
|
+
EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE,
|
|
39
|
+
EMAIL_BUDGET_ALERT_TTL,
|
|
40
|
+
)
|
|
36
41
|
|
|
37
42
|
|
|
38
43
|
class BaseEmailLogger(CustomLogger):
|
|
@@ -43,7 +48,6 @@ class BaseEmailLogger(CustomLogger):
|
|
|
43
48
|
EmailEvent.virtual_key_created: "LiteLLM: {event_message}",
|
|
44
49
|
EmailEvent.virtual_key_rotated: "LiteLLM: {event_message}",
|
|
45
50
|
}
|
|
46
|
-
DEFAULT_BUDGET_ALERT_TTL = 24 * 60 * 60 # 24 hours in seconds
|
|
47
51
|
|
|
48
52
|
def __init__(
|
|
49
53
|
self,
|
|
@@ -213,11 +217,53 @@ class BaseEmailLogger(CustomLogger):
|
|
|
213
217
|
)
|
|
214
218
|
pass
|
|
215
219
|
|
|
220
|
+
async def send_max_budget_alert_email(self, event: WebhookEvent):
|
|
221
|
+
"""
|
|
222
|
+
Send email to user when max budget alert threshold is reached
|
|
223
|
+
"""
|
|
224
|
+
email_params = await self._get_email_params(
|
|
225
|
+
email_event=EmailEvent.max_budget_alert,
|
|
226
|
+
user_id=event.user_id,
|
|
227
|
+
user_email=event.user_email,
|
|
228
|
+
event_message=event.event_message,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
verbose_proxy_logger.debug(
|
|
232
|
+
f"send_max_budget_alert_email_event: {json.dumps(event.model_dump(exclude_none=True), indent=4, default=str)}"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Format budget values
|
|
236
|
+
spend_str = f"${event.spend}" if event.spend is not None else "$0.00"
|
|
237
|
+
max_budget_str = f"${event.max_budget}" if event.max_budget is not None else "N/A"
|
|
238
|
+
|
|
239
|
+
# Calculate percentage and alert threshold
|
|
240
|
+
percentage = int(EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE * 100)
|
|
241
|
+
alert_threshold_str = f"${event.max_budget * EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE:.2f}" if event.max_budget is not None else "N/A"
|
|
242
|
+
|
|
243
|
+
email_html_content = MAX_BUDGET_ALERT_EMAIL_TEMPLATE.format(
|
|
244
|
+
email_logo_url=email_params.logo_url,
|
|
245
|
+
recipient_email=email_params.recipient_email,
|
|
246
|
+
percentage=percentage,
|
|
247
|
+
spend=spend_str,
|
|
248
|
+
max_budget=max_budget_str,
|
|
249
|
+
alert_threshold=alert_threshold_str,
|
|
250
|
+
base_url=email_params.base_url,
|
|
251
|
+
email_support_contact=email_params.support_contact,
|
|
252
|
+
)
|
|
253
|
+
await self.send_email(
|
|
254
|
+
from_email=self.DEFAULT_LITELLM_EMAIL,
|
|
255
|
+
to_email=[email_params.recipient_email],
|
|
256
|
+
subject=email_params.subject,
|
|
257
|
+
html_body=email_html_content,
|
|
258
|
+
)
|
|
259
|
+
pass
|
|
260
|
+
|
|
216
261
|
async def budget_alerts(
|
|
217
262
|
self,
|
|
218
263
|
type: Literal[
|
|
219
264
|
"token_budget",
|
|
220
265
|
"soft_budget",
|
|
266
|
+
"max_budget_alert",
|
|
221
267
|
"user_budget",
|
|
222
268
|
"team_budget",
|
|
223
269
|
"organization_budget",
|
|
@@ -281,7 +327,7 @@ class BaseEmailLogger(CustomLogger):
|
|
|
281
327
|
await _cache.async_set_cache(
|
|
282
328
|
key=_cache_key,
|
|
283
329
|
value="SENT",
|
|
284
|
-
ttl=
|
|
330
|
+
ttl=EMAIL_BUDGET_ALERT_TTL,
|
|
285
331
|
)
|
|
286
332
|
except Exception as e:
|
|
287
333
|
verbose_proxy_logger.error(
|
|
@@ -290,6 +336,58 @@ class BaseEmailLogger(CustomLogger):
|
|
|
290
336
|
)
|
|
291
337
|
return
|
|
292
338
|
|
|
339
|
+
# For max_budget_alert, check if we've already sent an alert
|
|
340
|
+
if type == "max_budget_alert":
|
|
341
|
+
if user_info.max_budget is not None and user_info.spend is not None:
|
|
342
|
+
alert_threshold = user_info.max_budget * EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE
|
|
343
|
+
# Only alert if we've crossed the threshold but haven't exceeded max_budget yet
|
|
344
|
+
if user_info.spend >= alert_threshold and user_info.spend < user_info.max_budget:
|
|
345
|
+
# Generate cache key based on event type and identifier
|
|
346
|
+
_id = user_info.token or user_info.user_id or "default_id"
|
|
347
|
+
_cache_key = f"email_budget_alerts:max_budget_alert:{_id}"
|
|
348
|
+
# Check if we've already sent this alert
|
|
349
|
+
result = await _cache.async_get_cache(key=_cache_key)
|
|
350
|
+
if result is None:
|
|
351
|
+
# Calculate percentage
|
|
352
|
+
percentage = int(EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE * 100)
|
|
353
|
+
|
|
354
|
+
# Create WebhookEvent for max budget alert
|
|
355
|
+
event_message = f"Max Budget Alert - {percentage}% of Maximum Budget Reached"
|
|
356
|
+
webhook_event = WebhookEvent(
|
|
357
|
+
event="max_budget_alert",
|
|
358
|
+
event_message=event_message,
|
|
359
|
+
spend=user_info.spend,
|
|
360
|
+
max_budget=user_info.max_budget,
|
|
361
|
+
soft_budget=user_info.soft_budget,
|
|
362
|
+
token=user_info.token,
|
|
363
|
+
customer_id=user_info.customer_id,
|
|
364
|
+
user_id=user_info.user_id,
|
|
365
|
+
team_id=user_info.team_id,
|
|
366
|
+
team_alias=user_info.team_alias,
|
|
367
|
+
organization_id=user_info.organization_id,
|
|
368
|
+
user_email=user_info.user_email,
|
|
369
|
+
key_alias=user_info.key_alias,
|
|
370
|
+
projected_exceeded_date=user_info.projected_exceeded_date,
|
|
371
|
+
projected_spend=user_info.projected_spend,
|
|
372
|
+
event_group=user_info.event_group,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
await self.send_max_budget_alert_email(webhook_event)
|
|
377
|
+
|
|
378
|
+
# Cache the alert to prevent duplicate sends
|
|
379
|
+
await _cache.async_set_cache(
|
|
380
|
+
key=_cache_key,
|
|
381
|
+
value="SENT",
|
|
382
|
+
ttl=EMAIL_BUDGET_ALERT_TTL,
|
|
383
|
+
)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
verbose_proxy_logger.error(
|
|
386
|
+
f"Error sending max budget alert email: {e}",
|
|
387
|
+
exc_info=True,
|
|
388
|
+
)
|
|
389
|
+
return
|
|
390
|
+
|
|
293
391
|
async def _get_email_params(
|
|
294
392
|
self,
|
|
295
393
|
email_event: EmailEvent,
|
|
@@ -750,9 +750,27 @@ class _PROXY_LiteLLMManagedFiles(CustomLogger, BaseFileEndpoints):
|
|
|
750
750
|
model_id=model_id,
|
|
751
751
|
model_name=model_name,
|
|
752
752
|
)
|
|
753
|
-
|
|
753
|
+
|
|
754
|
+
# Fetch the actual file object for the output file
|
|
755
|
+
file_object = None
|
|
756
|
+
try:
|
|
757
|
+
# Use litellm to retrieve the file object from the provider
|
|
758
|
+
from litellm import afile_retrieve
|
|
759
|
+
file_object = await afile_retrieve(
|
|
760
|
+
custom_llm_provider=model_name.split("/")[0] if model_name and "/" in model_name else "openai",
|
|
761
|
+
file_id=original_output_file_id
|
|
762
|
+
)
|
|
763
|
+
verbose_logger.debug(
|
|
764
|
+
f"Successfully retrieved file object for output_file_id={original_output_file_id}"
|
|
765
|
+
)
|
|
766
|
+
except Exception as e:
|
|
767
|
+
verbose_logger.warning(
|
|
768
|
+
f"Failed to retrieve file object for output_file_id={original_output_file_id}: {str(e)}. Storing with None and will fetch on-demand."
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
await self.store_unified_file_id(
|
|
754
772
|
file_id=response.output_file_id,
|
|
755
|
-
file_object=
|
|
773
|
+
file_object=file_object,
|
|
756
774
|
litellm_parent_otel_span=user_api_key_dict.parent_otel_span,
|
|
757
775
|
model_mappings={model_id: original_output_file_id},
|
|
758
776
|
user_api_key_dict=user_api_key_dict,
|
|
@@ -37,6 +37,7 @@ class EmailEvent(str, enum.Enum):
|
|
|
37
37
|
new_user_invitation = "New User Invitation"
|
|
38
38
|
virtual_key_rotated = "Virtual Key Rotated"
|
|
39
39
|
soft_budget_crossed = "Soft Budget Crossed"
|
|
40
|
+
max_budget_alert = "Max Budget Alert"
|
|
40
41
|
|
|
41
42
|
class EmailEventSettings(BaseModel):
|
|
42
43
|
event: EmailEvent
|
|
@@ -53,6 +54,7 @@ class DefaultEmailSettings(BaseModel):
|
|
|
53
54
|
EmailEvent.new_user_invitation: True, # On by default
|
|
54
55
|
EmailEvent.virtual_key_rotated: True, # On by default
|
|
55
56
|
EmailEvent.soft_budget_crossed: True, # On by default
|
|
57
|
+
EmailEvent.max_budget_alert: True, # On by default
|
|
56
58
|
}
|
|
57
59
|
)
|
|
58
60
|
def to_dict(self) -> Dict[str, bool]:
|
|
@@ -99,7 +99,7 @@ litellm_enterprise/enterprise_callbacks/secrets_plugins/typeform_api_token.py,sh
|
|
|
99
99
|
litellm_enterprise/enterprise_callbacks/secrets_plugins/vault.py,sha256=fqtHTQTC6QaNMIZpuvntBnCSAgAhY2Ka-XOz4ZLafGk,653
|
|
100
100
|
litellm_enterprise/enterprise_callbacks/secrets_plugins/yandex.py,sha256=BVtFVzCTtpAkRJVudeZIEBBz1W8wueDzpu6TBvxngxo,1183
|
|
101
101
|
litellm_enterprise/enterprise_callbacks/secrets_plugins/zendesk_secret_key.py,sha256=3E21lWz12WUAmdnKDZH8znfTp6hRJbE3yImtfEP52qE,613
|
|
102
|
-
litellm_enterprise/enterprise_callbacks/send_emails/base_email.py,sha256=
|
|
102
|
+
litellm_enterprise/enterprise_callbacks/send_emails/base_email.py,sha256=qcY2oBDP9-30OTZF9bxPiXytgosUcGN5WGhKqSXTLE8,26083
|
|
103
103
|
litellm_enterprise/enterprise_callbacks/send_emails/endpoints.py,sha256=hOEpM_q8MJAXlKMOtC9KbgvDVr_YFtF3reu9bjXkpsI,7017
|
|
104
104
|
litellm_enterprise/enterprise_callbacks/send_emails/resend_email.py,sha256=KxNfvONZxSWbNg0HmWwfC0rvHzpN7MBJXAPKGLcy_tU,1541
|
|
105
105
|
litellm_enterprise/enterprise_callbacks/send_emails/sendgrid_email.py,sha256=4bvSOfV-WzCGIJX2V32Ug91I8GBQAmypDDp40qsZbQU,2318
|
|
@@ -113,7 +113,7 @@ litellm_enterprise/proxy/auth/route_checks.py,sha256=FbXwbrOkFr1dODH6XxoIpLG1nKo
|
|
|
113
113
|
litellm_enterprise/proxy/auth/user_api_key_auth.py,sha256=7t5Q-JoKFyoymylaOT8KWAAOFVz0JOTl7PPOmTkpj5c,1144
|
|
114
114
|
litellm_enterprise/proxy/common_utils/check_batch_cost.py,sha256=V0CCHtN-JV-_d-ydXV-cVs3zCImt1699JnICGF3oPOk,7360
|
|
115
115
|
litellm_enterprise/proxy/enterprise_routes.py,sha256=ToJVSSNaYUotzgIg-kWsfsqh2E0GnQirOPkpE4YkHNg,907
|
|
116
|
-
litellm_enterprise/proxy/hooks/managed_files.py,sha256=
|
|
116
|
+
litellm_enterprise/proxy/hooks/managed_files.py,sha256=214MTFrwYs3yrAW84SNp-K8zYBZ9Ck2nkysKnLZjLAQ,42484
|
|
117
117
|
litellm_enterprise/proxy/management_endpoints/__init__.py,sha256=zfaqryxzmFu6se-w4yR2nlHKxDOOtHAWEehA2xFbFNg,270
|
|
118
118
|
litellm_enterprise/proxy/management_endpoints/internal_user_endpoints.py,sha256=GEoOVujrtKXDHfko2KQaLn-ms64zkutFE9PP5IhBBLM,2175
|
|
119
119
|
litellm_enterprise/proxy/management_endpoints/key_management_endpoints.py,sha256=-IXRzVrNQ3_krL-gxngelYQftwyPlB_HmgI3RN-HdvM,1147
|
|
@@ -121,10 +121,10 @@ litellm_enterprise/proxy/proxy_server.py,sha256=fzOeTyiyevLWi2767-2W1Co7reR-0wno
|
|
|
121
121
|
litellm_enterprise/proxy/readme.md,sha256=ZcigMJYSHWs4SWnYriWjrSVDJKsu44c2HsbYbma0EHU,397
|
|
122
122
|
litellm_enterprise/proxy/utils.py,sha256=y4ADfhlEG_mH0x5rfIg7D9FjS586lVgQ9DL0tTdgrMQ,962
|
|
123
123
|
litellm_enterprise/proxy/vector_stores/endpoints.py,sha256=6Guh6zIH00dh2XXStn6GblTGpGyE4hZJ9WThVZggDQg,12944
|
|
124
|
-
litellm_enterprise/types/enterprise_callbacks/send_emails.py,sha256=
|
|
124
|
+
litellm_enterprise/types/enterprise_callbacks/send_emails.py,sha256=AouBXqb1EB1-Mg3fM_3UjUDihIA45zIjRgA6M4vQ7Zw,2150
|
|
125
125
|
litellm_enterprise/types/proxy/audit_logging_endpoints.py,sha256=oSJVAuRD9r6ZjRCqNBFM-J5HSgOltsXts400b2aynRE,894
|
|
126
126
|
litellm_enterprise/types/proxy/proxy_server.py,sha256=kdhtxsU2uok6-XO_ebugCv7PzYYmGgv4vh-XemHJnpM,146
|
|
127
|
-
litellm_enterprise-0.1.
|
|
128
|
-
litellm_enterprise-0.1.
|
|
129
|
-
litellm_enterprise-0.1.
|
|
130
|
-
litellm_enterprise-0.1.
|
|
127
|
+
litellm_enterprise-0.1.27.dist-info/METADATA,sha256=NOumHWDgf-L-MMKoA9gCgEweLGji6O8_DMpB-Wa7TIE,1441
|
|
128
|
+
litellm_enterprise-0.1.27.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
129
|
+
litellm_enterprise-0.1.27.dist-info/licenses/LICENSE.md,sha256=nq3D9ZqOvRDT6hLkypQFTc3XsE15kbkg5rkkLJVSqKY,2251
|
|
130
|
+
litellm_enterprise-0.1.27.dist-info/RECORD,,
|
|
File without changes
|
{litellm_enterprise-0.1.26.dist-info → litellm_enterprise-0.1.27.dist-info}/licenses/LICENSE.md
RENAMED
|
File without changes
|