@@ -148,10 +148,22 @@ async def async_send_batch(self):
148
148
),
149
149
),
150
150
}
151
- verbose_logger .debug ("payload %s" , json .dumps (payload , indent = 4 ))
151
+
152
+ # serialize datetime objects - for budget reset time in spend metrics
153
+ from litellm .litellm_core_utils .safe_json_dumps import safe_dumps
154
+
155
+ try :
156
+ verbose_logger .debug ("payload %s" , safe_dumps (payload ))
157
+ except Exception as debug_error :
158
+ verbose_logger .debug (
159
+ "payload serialization failed: %s" , str (debug_error )
160
+ )
161
+
162
+ json_payload = safe_dumps (payload )
163
+
152
164
response = await self .async_client .post (
153
165
url = self .intake_url ,
154
- json = payload ,
166
+ content = json_payload ,
155
167
headers = {
156
168
"DD-API-KEY" : self .DD_API_KEY ,
157
169
"Content-Type" : "application/json" ,
@@ -494,6 +506,12 @@ def _get_dd_llm_obs_payload_metadata(
494
506
latency_metrics = self ._get_latency_metrics (standard_logging_payload )
495
507
_metadata .update ({"latency_metrics" : dict (latency_metrics )})
496
508
509
+ #########################################################
510
+ # Add spend metrics to metadata
511
+ #########################################################
512
+ spend_metrics = self ._get_spend_metrics (standard_logging_payload )
513
+ _metadata .update ({"spend_metrics" : dict (spend_metrics )})
514
+
497
515
## extract tool calls and add to metadata
498
516
tool_call_metadata = self ._extract_tool_call_metadata (standard_logging_payload )
499
517
_metadata .update (tool_call_metadata )
@@ -543,6 +561,71 @@ def _get_latency_metrics(
543
561
544
562
return latency_metrics
545
563
564
+ def _get_spend_metrics (
565
+ self , standard_logging_payload : StandardLoggingPayload
566
+ ) -> DDLLMObsSpendMetrics :
567
+ """
568
+ Get the spend metrics from the standard logging payload
569
+ """
570
+ spend_metrics : DDLLMObsSpendMetrics = DDLLMObsSpendMetrics ()
571
+
572
+ # send response cost
573
+ spend_metrics ["response_cost" ] = standard_logging_payload .get (
574
+ "response_cost" , 0.0
575
+ )
576
+
577
+ # Get budget information from metadata
578
+ metadata = standard_logging_payload .get ("metadata" , {})
579
+
580
+ # API key max budget
581
+ user_api_key_max_budget = metadata .get ("user_api_key_max_budget" )
582
+ if user_api_key_max_budget is not None :
583
+ spend_metrics ["user_api_key_max_budget" ] = float (user_api_key_max_budget )
584
+
585
+ # API key spend
586
+ user_api_key_spend = metadata .get ("user_api_key_spend" )
587
+ if user_api_key_spend is not None :
588
+ try :
589
+ spend_metrics ["user_api_key_spend" ] = float (user_api_key_spend )
590
+ except (ValueError , TypeError ):
591
+ verbose_logger .debug (
592
+ f"Invalid user_api_key_spend value: { user_api_key_spend } "
593
+ )
594
+
595
+ # API key budget reset datetime
596
+ user_api_key_budget_reset_at = metadata .get ("user_api_key_budget_reset_at" )
597
+ if user_api_key_budget_reset_at is not None :
598
+ try :
599
+ from datetime import datetime , timezone
600
+
601
+ budget_reset_at = None
602
+ if isinstance (user_api_key_budget_reset_at , str ):
603
+ # Handle ISO format strings that might have 'Z' suffix
604
+ iso_string = user_api_key_budget_reset_at .replace ("Z" , "+00:00" )
605
+ budget_reset_at = datetime .fromisoformat (iso_string )
606
+ elif isinstance (user_api_key_budget_reset_at , datetime ):
607
+ budget_reset_at = user_api_key_budget_reset_at
608
+
609
+ if budget_reset_at is not None :
610
+ # Preserve timezone info if already present
611
+ if budget_reset_at .tzinfo is None :
612
+ budget_reset_at = budget_reset_at .replace (tzinfo = timezone .utc )
613
+
614
+ # Convert to ISO string format for JSON serialization
615
+ # This prevents circular reference issues and ensures proper timezone representation
616
+ iso_string = budget_reset_at .isoformat ()
617
+ spend_metrics ["user_api_key_budget_reset_at" ] = iso_string
618
+
619
+ # Debug logging to verify the conversion
620
+ verbose_logger .debug (
621
+ f"Converted budget_reset_at to ISO format: { iso_string } "
622
+ )
623
+ except Exception as e :
624
+ verbose_logger .debug (f"Error processing budget reset datetime: { e } " )
625
+ verbose_logger .debug (f"Original value: { user_api_key_budget_reset_at } " )
626
+
627
+ return spend_metrics
628
+
546
629
def _process_input_messages_preserving_tool_calls (
547
630
self , messages : List [Any ]
548
631
) -> List [Dict [str , Any ]]:
0 commit comments