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.
@@ -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=self.DEFAULT_BUDGET_ALERT_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
- await self.store_unified_file_id( # need to store otherwise any retrieve call will fail
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=None,
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]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: litellm-enterprise
3
- Version: 0.1.26
3
+ Version: 0.1.27
4
4
  Summary: Package for LiteLLM Enterprise features
5
5
  License-File: LICENSE.md
6
6
  Author: BerriAI
@@ -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=hUdNvzjE5MM05BWlkFnaM232B8jwABWskNntd4mPmSo,21174
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=_LxdyyhHJa_uZDbkwpdUCbDSCbM6qK-4z8_BHoj189o,41480
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=to7W50SCGKh14FB3gsnlzY_snyxa8M8LAAi_zXbJsb8,2044
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.26.dist-info/METADATA,sha256=CUF0ZS8-TuSbpGp3-q4g3VV30wvf4HaSIIaz0A6Gz-Q,1441
128
- litellm_enterprise-0.1.26.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
129
- litellm_enterprise-0.1.26.dist-info/licenses/LICENSE.md,sha256=nq3D9ZqOvRDT6hLkypQFTc3XsE15kbkg5rkkLJVSqKY,2251
130
- litellm_enterprise-0.1.26.dist-info/RECORD,,
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,,