Skip to content

Commit fc7de7a

Browse files
added user_api_key_spend
1 parent f5f0004 commit fc7de7a

File tree

4 files changed

+44
-40
lines changed

4 files changed

+44
-40
lines changed

litellm/integrations/datadog/datadog_llm_obs.py

Lines changed: 36 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -148,23 +148,16 @@ async def async_send_batch(self):
148148
),
149149
),
150150
}
151-
# serialize datetime objects - for budget reset time in spend metrics
152-
import json
153-
from datetime import datetime, date
154151

155-
def custom_json_encoder(obj):
156-
if isinstance(obj, (datetime, date)):
157-
return obj.isoformat()
158-
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
152+
# serialize datetime objects - for budget reset time in spend metrics
153+
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
159154

160-
# Serialize payload with custom encoder for debugging
161155
try:
162-
verbose_logger.debug("payload %s", json.dumps(payload, indent=4, default=custom_json_encoder))
156+
verbose_logger.debug("payload %s", safe_dumps(payload))
163157
except Exception as debug_error:
164158
verbose_logger.debug("payload serialization failed: %s", str(debug_error))
165159

166-
# Convert payload to JSON string with custom encoder for HTTP request
167-
json_payload = json.dumps(payload, default=custom_json_encoder)
160+
json_payload = safe_dumps(payload)
168161

169162
response = await self.async_client.post(
170163
url=self.intake_url,
@@ -338,7 +331,6 @@ def _get_response_messages(
338331
if isinstance(response_obj, str):
339332
try:
340333
import ast
341-
342334
response_obj = ast.literal_eval(response_obj)
343335
except (ValueError, SyntaxError):
344336
try:
@@ -573,48 +565,55 @@ def _get_spend_metrics(
573565
Get the spend metrics from the standard logging payload
574566
"""
575567
spend_metrics: DDLLMObsSpendMetrics = DDLLMObsSpendMetrics()
576-
577-
# Get response cost for litellm_spend_metric
578-
response_cost = standard_logging_payload.get("response_cost", 0.0)
579-
if response_cost > 0:
580-
spend_metrics["litellm_spend_metric"] = response_cost
568+
569+
# send response cost
570+
spend_metrics["response_cost"] = standard_logging_payload.get("response_cost", 0.0)
581571

582572
# Get budget information from metadata
583573
metadata = standard_logging_payload.get("metadata", {})
584574

585575
# API key max budget
586576
user_api_key_max_budget = metadata.get("user_api_key_max_budget")
587577
if user_api_key_max_budget is not None:
588-
# type casting to make sure its a float value
578+
spend_metrics["user_api_key_max_budget"] = float(user_api_key_max_budget)
579+
580+
# API key spend
581+
user_api_key_spend = metadata.get("user_api_key_spend")
582+
if user_api_key_spend is not None:
589583
try:
590-
if isinstance(user_api_key_max_budget, (int, float)):
591-
spend_metrics["litellm_api_key_max_budget_metric"] = float(user_api_key_max_budget)
592-
elif isinstance(user_api_key_max_budget, str):
593-
spend_metrics["litellm_api_key_max_budget_metric"] = float(user_api_key_max_budget)
584+
spend_metrics["user_api_key_spend"] = float(user_api_key_spend)
594585
except (ValueError, TypeError):
595-
verbose_logger.debug(f"Invalid user_api_key_max_budget value: {user_api_key_max_budget}")
586+
verbose_logger.debug(f"Invalid user_api_key_spend value: {user_api_key_spend}")
596587

597-
# API key budget remaining hours
588+
# API key budget reset datetime
598589
user_api_key_budget_reset_at = metadata.get("user_api_key_budget_reset_at")
599590
if user_api_key_budget_reset_at is not None:
600591
try:
601-
from datetime import datetime
602-
budget_reset_at: datetime
592+
from datetime import datetime, timezone
593+
594+
budget_reset_at = None
603595
if isinstance(user_api_key_budget_reset_at, str):
604-
# Parse ISO string if it's a string
605-
budget_reset_at = datetime.fromisoformat(user_api_key_budget_reset_at.replace('Z', '+00:00'))
596+
# Handle ISO format strings that might have 'Z' suffix
597+
iso_string = user_api_key_budget_reset_at.replace('Z', '+00:00')
598+
budget_reset_at = datetime.fromisoformat(iso_string)
606599
elif isinstance(user_api_key_budget_reset_at, datetime):
607600
budget_reset_at = user_api_key_budget_reset_at
608-
else:
609-
verbose_logger.debug(f"Invalid user_api_key_budget_reset_at type: {type(user_api_key_budget_reset_at)}")
610-
return spend_metrics
611-
612-
remaining_hours = (
613-
budget_reset_at - datetime.now(budget_reset_at.tzinfo)
614-
).total_seconds() / 3600
615-
spend_metrics["litellm_api_key_budget_remaining_hours_metric"] = max(0, remaining_hours)
601+
602+
if budget_reset_at is not None:
603+
# Preserve timezone info if already present
604+
if budget_reset_at.tzinfo is None:
605+
budget_reset_at = budget_reset_at.replace(tzinfo=timezone.utc)
606+
607+
# Convert to ISO string format for JSON serialization
608+
# This prevents circular reference issues and ensures proper timezone representation
609+
iso_string = budget_reset_at.isoformat()
610+
spend_metrics["user_api_key_budget_reset_at"] = iso_string
611+
612+
# Debug logging to verify the conversion
613+
verbose_logger.debug(f"Converted budget_reset_at to ISO format: {iso_string}")
616614
except Exception as e:
617-
verbose_logger.debug(f"Error calculating remaining hours for budget reset: {e}")
615+
verbose_logger.debug(f"Error processing budget reset datetime: {e}")
616+
verbose_logger.debug(f"Original value: {user_api_key_budget_reset_at}")
618617

619618
return spend_metrics
620619

litellm/proxy/litellm_pre_call_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,7 @@ def get_sanitized_user_information_from_key(
546546
user_api_key_end_user_id=user_api_key_dict.end_user_id,
547547
user_api_key_user_email=user_api_key_dict.user_email,
548548
user_api_key_request_route=user_api_key_dict.request_route,
549+
user_api_key_budget_reset_at=user_api_key_dict.budget_reset_at,
549550
)
550551
return user_api_key_logged_metadata
551552

litellm/types/integrations/datadog_llm_obs.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class DDLLMObsLatencyMetrics(TypedDict, total=False):
8383
guardrail_overhead_time_ms: float
8484

8585
class DDLLMObsSpendMetrics(TypedDict, total=False):
86-
litellm_spend_metric: float
87-
litellm_api_key_max_budget_metric: float
88-
litellm_api_key_budget_remaining_hours_metric: float
86+
response_cost: float
87+
user_api_key_spend: float
88+
user_api_key_max_budget: float
89+
user_api_key_budget_reset_at: str

litellm/types/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,6 +1807,9 @@ async def __anext__(self):
18071807
class StandardLoggingUserAPIKeyMetadata(TypedDict):
18081808
user_api_key_hash: Optional[str] # hash of the litellm virtual key used
18091809
user_api_key_alias: Optional[str]
1810+
user_api_key_spend: Optional[float]
1811+
user_api_key_max_budget: Optional[float] = None
1812+
user_api_key_budget_reset_at: Optional[str] = None
18101813
user_api_key_org_id: Optional[str]
18111814
user_api_key_team_id: Optional[str]
18121815
user_api_key_user_id: Optional[str]

0 commit comments

Comments
 (0)