1
1
import asyncio
2
2
import os
3
3
import sys
4
- from datetime import datetime , timedelta
4
+ from datetime import datetime , timedelta , timezone
5
5
from typing import Optional
6
6
from unittest .mock import Mock , patch , MagicMock
7
7
@@ -901,89 +901,12 @@ def test_tool_call_response_handling(self, mock_env_vars):
901
901
output_function_info = output_tool_calls [0 ].get ("function" , {})
902
902
assert output_function_info .get ("name" ) == "format_response"
903
903
904
-
905
- def create_standard_logging_payload () -> StandardLoggingPayload :
906
- """Create a standard logging payload for testing"""
907
- return {
908
- "id" : "test_id" ,
909
- "trace_id" : "test_trace_id" ,
910
- "call_type" : "completion" ,
911
- "stream" : False ,
912
- "response_cost" : 0.1 ,
913
- "response_cost_failure_debug_info" : None ,
914
- "status" : "success" ,
915
- "custom_llm_provider" : None ,
916
- "total_tokens" : 30 ,
917
- "prompt_tokens" : 20 ,
918
- "completion_tokens" : 10 ,
919
- "startTime" : 1234567890.0 ,
920
- "endTime" : 1234567891.0 ,
921
- "completionStartTime" : 1234567890.5 ,
922
- "response_time" : 1.0 ,
923
- "model_map_information" : {
924
- "model_map_key" : "gpt-3.5-turbo" ,
925
- "model_map_value" : None
926
- },
927
- "model" : "gpt-3.5-turbo" ,
928
- "model_id" : "model-123" ,
929
- "model_group" : "openai-gpt" ,
930
- "api_base" : "https://api.openai.com" ,
931
- "metadata" : {
932
- "user_api_key_hash" : "test_hash" ,
933
- "user_api_key_org_id" : None ,
934
- "user_api_key_alias" : "test_alias" ,
935
- "user_api_key_team_id" : "test_team" ,
936
- "user_api_key_user_id" : "test_user" ,
937
- "user_api_key_team_alias" : "test_team_alias" ,
938
- "user_api_key_end_user_id" : None ,
939
- "user_api_key_request_route" : None ,
940
- "user_api_key_max_budget" : None ,
941
- "user_api_key_budget_reset_at" : None ,
942
- "user_api_key_user_email" : None ,
943
- "spend_logs_metadata" : None ,
944
- "requester_ip_address" : "127.0.0.1" ,
945
- "requester_metadata" : None ,
946
- "requester_custom_headers" : None ,
947
- "prompt_management_metadata" : None ,
948
- "mcp_tool_call_metadata" : None ,
949
- "vector_store_request_metadata" : None ,
950
- "applied_guardrails" : None ,
951
- "usage_object" : None ,
952
- "cold_storage_object_key" : None ,
953
- },
954
- "cache_hit" : False ,
955
- "cache_key" : None ,
956
- "saved_cache_cost" : 0.0 ,
957
- "request_tags" : [],
958
- "end_user" : None ,
959
- "requester_ip_address" : "127.0.0.1" ,
960
- "messages" : [{"role" : "user" , "content" : "Hello, world!" }],
961
- "response" : {"choices" : [{"message" : {"content" : "Hi there!" }}]},
962
- "error_str" : None ,
963
- "model_parameters" : {"stream" : True },
964
- "hidden_params" : {
965
- "model_id" : "model-123" ,
966
- "cache_key" : None ,
967
- "api_base" : "https://api.openai.com" ,
968
- "response_cost" : "0.1" ,
969
- "additional_headers" : None ,
970
- "litellm_overhead_time_ms" : None ,
971
- "batch_models" : None ,
972
- "litellm_model_name" : None ,
973
- "usage_object" : None ,
974
- },
975
- "error_information" : None ,
976
- "guardrail_information" : None ,
977
- "standard_built_in_tools_params" : None ,
978
- } # type: ignore
979
-
980
-
981
904
def create_standard_logging_payload_with_spend_metrics () -> StandardLoggingPayload :
982
905
"""Create a StandardLoggingPayload object with spend metrics for testing"""
983
906
from datetime import datetime , timezone
984
907
985
- # Create a budget reset time 24 hours from now
986
- budget_reset_at = datetime .now (timezone .utc ) + timedelta (hours = 24 )
908
+ # Create a budget reset time 10 days from now (using "10d" format)
909
+ budget_reset_at = datetime .now (timezone .utc ) + timedelta (days = 10 )
987
910
988
911
return {
989
912
"id" : "test-request-id-spend" ,
@@ -1019,8 +942,9 @@ def create_standard_logging_payload_with_spend_metrics() -> StandardLoggingPaylo
1019
942
"user_api_key_user_email" : None ,
1020
943
"user_api_key_end_user_id" : None ,
1021
944
"user_api_key_request_route" : None ,
945
+ "user_api_key_spend" : 0.67 ,
1022
946
"user_api_key_max_budget" : 10.0 , # $10 max budget
1023
- "user_api_key_budget_reset_at" : budget_reset_at .isoformat (),
947
+ "user_api_key_budget_reset_at" : budget_reset_at .isoformat (), # ISO format: 2025-09-26T...
1024
948
"spend_logs_metadata" : None ,
1025
949
"requester_ip_address" : "127.0.0.1" ,
1026
950
"requester_metadata" : None ,
@@ -1064,23 +988,32 @@ async def test_datadog_llm_obs_spend_metrics(mock_env_vars):
1064
988
"""Test that budget metrics are properly extracted and logged"""
1065
989
datadog_llm_obs_logger = DataDogLLMObsLogger ()
1066
990
1067
- # Create a standard logging payload with budget metadata
1068
- payload = create_standard_logging_payload ()
991
+ # Create a standard logging payload with spend metrics
992
+ payload = create_standard_logging_payload_with_spend_metrics ()
1069
993
1070
- # Add budget information to metadata
1071
- payload ["metadata" ]["user_api_key_max_budget" ] = 10.0
1072
- payload ["metadata" ]["user_api_key_budget_reset_at" ] = "2025-09-15T00:00:00+00:00"
994
+ # Show the budget reset time in ISO format
995
+ budget_reset_iso = payload ["metadata" ]["user_api_key_budget_reset_at" ]
996
+ print (f"Budget reset time (ISO format): { budget_reset_iso } " )
997
+ from datetime import datetime , timezone
998
+ print (f"Current time: { datetime .now (timezone .utc ).isoformat ()} " )
1073
999
1074
1000
# Test the _get_spend_metrics method
1075
1001
spend_metrics = datadog_llm_obs_logger ._get_spend_metrics (payload )
1076
1002
1077
1003
# Verify budget metrics are present
1078
- assert "litellm_api_key_max_budget_metric" in spend_metrics
1079
- assert spend_metrics ["litellm_api_key_max_budget_metric" ] == 10.0
1080
-
1081
- assert "litellm_api_key_budget_remaining_hours_metric" in spend_metrics
1082
- # The remaining hours should be calculated based on the reset time
1083
- assert spend_metrics ["litellm_api_key_budget_remaining_hours_metric" ] >= 0
1004
+ assert "user_api_key_max_budget" in spend_metrics
1005
+ assert spend_metrics ["user_api_key_max_budget" ] == 10.0
1006
+
1007
+ assert "user_api_key_budget_reset_at" in spend_metrics
1008
+ # The budget reset should be a datetime string in ISO format
1009
+ budget_reset = spend_metrics ["user_api_key_budget_reset_at" ]
1010
+ assert isinstance (budget_reset , str )
1011
+ print (f"Budget reset datetime: { budget_reset } " )
1012
+ # Should be close to 10 days from now
1013
+ budget_reset_dt = datetime .fromisoformat (budget_reset .replace ('Z' , '+00:00' ))
1014
+ now = datetime .now (timezone .utc )
1015
+ time_diff = (budget_reset_dt - now ).total_seconds () / 86400 # days
1016
+ assert 9.5 <= time_diff <= 10.5 # Should be close to 10 days
1084
1017
1085
1018
print (f"Spend metrics: { spend_metrics } " )
1086
1019
@@ -1091,25 +1024,30 @@ async def test_datadog_llm_obs_spend_metrics_no_budget(mock_env_vars):
1091
1024
datadog_llm_obs_logger = DataDogLLMObsLogger ()
1092
1025
1093
1026
# Create a standard logging payload without budget metadata
1094
- payload = create_standard_logging_payload ()
1027
+ payload = create_standard_logging_payload_with_spend_metrics ()
1028
+
1029
+ # Remove budget-related metadata to test no-budget scenario
1030
+ payload ["metadata" ].pop ("user_api_key_max_budget" , None )
1031
+ payload ["metadata" ].pop ("user_api_key_budget_reset_at" , None )
1095
1032
1096
1033
# Test the _get_spend_metrics method
1097
1034
spend_metrics = datadog_llm_obs_logger ._get_spend_metrics (payload )
1098
1035
1099
1036
# Verify only response cost is present
1100
- assert "litellm_spend_metric " in spend_metrics
1101
- assert spend_metrics ["litellm_spend_metric " ] == 0.1
1037
+ assert "response_cost " in spend_metrics
1038
+ assert spend_metrics ["response_cost " ] == 0.15
1102
1039
1103
1040
# Budget metrics should not be present
1104
- assert "litellm_api_key_max_budget_metric " not in spend_metrics
1105
- assert "litellm_api_key_budget_remaining_hours_metric " not in spend_metrics
1041
+ assert "user_api_key_max_budget " not in spend_metrics
1042
+ assert "user_api_key_budget_reset_at " not in spend_metrics
1106
1043
1107
1044
print (f"Spend metrics (no budget): { spend_metrics } " )
1108
1045
1109
1046
1110
1047
@pytest .mark .asyncio
1111
1048
async def test_spend_metrics_in_datadog_payload (mock_env_vars ):
1112
1049
"""Test that spend metrics are correctly included in DataDog LLM Observability payloads"""
1050
+ from datetime import datetime
1113
1051
datadog_llm_obs_logger = DataDogLLMObsLogger ()
1114
1052
1115
1053
standard_payload = create_standard_logging_payload_with_spend_metrics ()
@@ -1138,17 +1076,28 @@ async def test_spend_metrics_in_datadog_payload(mock_env_vars):
1138
1076
spend_metrics = metadata .get ("spend_metrics" , {})
1139
1077
assert spend_metrics , "Spend metrics should exist in metadata"
1140
1078
1141
- # Check that all three spend metrics are present
1142
- assert "litellm_spend_metric" in spend_metrics
1143
- assert "litellm_api_key_max_budget_metric" in spend_metrics
1144
- assert "litellm_api_key_budget_remaining_hours_metric" in spend_metrics
1079
+ # Check that all metrics are present
1080
+ assert "response_cost" in spend_metrics
1081
+ assert "user_api_key_spend" in spend_metrics
1082
+ assert "user_api_key_max_budget" in spend_metrics
1083
+ assert "user_api_key_budget_reset_at" in spend_metrics
1145
1084
1146
1085
# Verify the values are correct
1147
- assert spend_metrics ["litellm_spend_metric" ] == 0.15 # response_cost
1148
- assert spend_metrics ["litellm_api_key_max_budget_metric" ] == 10.0 # max budget
1149
-
1150
- # Verify remaining hours is a reasonable value (should be close to 24 since we set it to 24 hours from now)
1151
- remaining_hours = spend_metrics ["litellm_api_key_budget_remaining_hours_metric" ]
1152
- assert isinstance (remaining_hours , (int , float ))
1153
- assert 20 <= remaining_hours <= 25 # Should be close to 24 hours
1154
-
1086
+ assert spend_metrics ["response_cost" ] == 0.15 # response_cost
1087
+ assert spend_metrics ["user_api_key_spend" ] == 0.67 # lol
1088
+ assert spend_metrics ["user_api_key_max_budget" ] == 10.0 # max budget
1089
+
1090
+ # Verify budget reset is a datetime string in ISO format
1091
+ budget_reset = spend_metrics ["user_api_key_budget_reset_at" ]
1092
+ assert isinstance (budget_reset , str )
1093
+ print (f"Budget reset in payload: { budget_reset } " ) # In StandardLoggingUserAPIKeyMetadata
1094
+ user_api_key_budget_reset_at : Optional [str ] = None
1095
+
1096
+ # In DDLLMObsSpendMetrics
1097
+ user_api_key_budget_reset_at : str
1098
+ # Should be close to 10 days from now
1099
+ from datetime import datetime , timezone
1100
+ budget_reset_dt = datetime .fromisoformat (budget_reset .replace ('Z' , '+00:00' ))
1101
+ now = datetime .now (timezone .utc )
1102
+ time_diff = (budget_reset_dt - now ).total_seconds () / 86400 # days
1103
+ assert 9.5 <= time_diff <= 10.5 # Should be close to 10 days
0 commit comments