11from __future__ import annotations
22
3+ import math
34import os
45import queue
56import random
@@ -33,10 +34,9 @@ class BackendSpanExporter(TracingExporter):
3334 {
3435 "input_tokens" ,
3536 "output_tokens" ,
36- "input_tokens_details" ,
37- "output_tokens_details" ,
3837 }
3938 )
39+ _UNSERIALIZABLE = object ()
4040
4141 def __init__ (
4242 self ,
@@ -181,7 +181,7 @@ def _should_sanitize_for_openai_tracing_api(self) -> bool:
181181 return self .endpoint .rstrip ("/" ) == self ._OPENAI_TRACING_INGEST_ENDPOINT .rstrip ("/" )
182182
183183 def _sanitize_for_openai_tracing_api (self , payload_item : dict [str , Any ]) -> dict [str , Any ]:
184- """Drop fields known to be rejected by OpenAI tracing ingestion ."""
184+ """Move unsupported generation usage fields under usage.details for traces ingest ."""
185185 span_data = payload_item .get ("span_data" )
186186 if not isinstance (span_data , dict ):
187187 return payload_item
@@ -193,20 +193,109 @@ def _sanitize_for_openai_tracing_api(self, payload_item: dict[str, Any]) -> dict
193193 if not isinstance (usage , dict ):
194194 return payload_item
195195
196- filtered_usage = {
197- key : value
198- for key , value in usage .items ()
199- if key in self ._OPENAI_TRACING_ALLOWED_USAGE_KEYS
200- }
201- if filtered_usage == usage :
196+ sanitized_usage = self ._sanitize_generation_usage_for_openai_tracing_api (usage )
197+
198+ if sanitized_usage is None :
199+ sanitized_span_data = dict (span_data )
200+ sanitized_span_data .pop ("usage" , None )
201+ sanitized_payload_item = dict (payload_item )
202+ sanitized_payload_item ["span_data" ] = sanitized_span_data
203+ return sanitized_payload_item
204+
205+ if sanitized_usage == usage :
202206 return payload_item
203207
204208 sanitized_span_data = dict (span_data )
205- sanitized_span_data ["usage" ] = filtered_usage
209+ sanitized_span_data ["usage" ] = sanitized_usage
206210 sanitized_payload_item = dict (payload_item )
207211 sanitized_payload_item ["span_data" ] = sanitized_span_data
208212 return sanitized_payload_item
209213
214+ def _sanitize_generation_usage_for_openai_tracing_api (
215+ self , usage : dict [str , Any ]
216+ ) -> dict [str , Any ] | None :
217+ input_tokens = usage .get ("input_tokens" )
218+ output_tokens = usage .get ("output_tokens" )
219+ if not self ._is_finite_json_number (input_tokens ) or not self ._is_finite_json_number (
220+ output_tokens
221+ ):
222+ return None
223+
224+ details : dict [str , Any ] = {}
225+ existing_details = usage .get ("details" )
226+ if isinstance (existing_details , dict ):
227+ for key , value in existing_details .items ():
228+ if not isinstance (key , str ):
229+ continue
230+ sanitized_value = self ._sanitize_json_compatible_value (value )
231+ if sanitized_value is self ._UNSERIALIZABLE :
232+ continue
233+ details [key ] = sanitized_value
234+
235+ for key , value in usage .items ():
236+ if key in self ._OPENAI_TRACING_ALLOWED_USAGE_KEYS or key == "details" or value is None :
237+ continue
238+ sanitized_value = self ._sanitize_json_compatible_value (value )
239+ if sanitized_value is self ._UNSERIALIZABLE :
240+ continue
241+ details [key ] = sanitized_value
242+
243+ sanitized_usage : dict [str , Any ] = {
244+ "input_tokens" : input_tokens ,
245+ "output_tokens" : output_tokens ,
246+ }
247+ if details :
248+ sanitized_usage ["details" ] = details
249+ return sanitized_usage
250+
251+ def _is_finite_json_number (self , value : Any ) -> bool :
252+ if isinstance (value , bool ):
253+ return False
254+ return isinstance (value , int | float ) and not (
255+ isinstance (value , float ) and not math .isfinite (value )
256+ )
257+
258+ def _sanitize_json_compatible_value (self , value : Any , seen_ids : set [int ] | None = None ) -> Any :
259+ if value is None or isinstance (value , str | bool | int ):
260+ return value
261+ if isinstance (value , float ):
262+ return value if math .isfinite (value ) else self ._UNSERIALIZABLE
263+ if seen_ids is None :
264+ seen_ids = set ()
265+ if isinstance (value , dict ):
266+ value_id = id (value )
267+ if value_id in seen_ids :
268+ return self ._UNSERIALIZABLE
269+ seen_ids .add (value_id )
270+ sanitized_dict : dict [str , Any ] = {}
271+ try :
272+ for key , nested_value in value .items ():
273+ if not isinstance (key , str ):
274+ continue
275+ sanitized_nested = self ._sanitize_json_compatible_value (nested_value , seen_ids )
276+ if sanitized_nested is self ._UNSERIALIZABLE :
277+ continue
278+ sanitized_dict [key ] = sanitized_nested
279+ finally :
280+ seen_ids .remove (value_id )
281+ return sanitized_dict
282+ if isinstance (value , list | tuple ):
283+ value_id = id (value )
284+ if value_id in seen_ids :
285+ return self ._UNSERIALIZABLE
286+ seen_ids .add (value_id )
287+ sanitized_list : list [Any ] = []
288+ try :
289+ for nested_value in value :
290+ sanitized_nested = self ._sanitize_json_compatible_value (nested_value , seen_ids )
291+ if sanitized_nested is self ._UNSERIALIZABLE :
292+ continue
293+ sanitized_list .append (sanitized_nested )
294+ finally :
295+ seen_ids .remove (value_id )
296+ return sanitized_list
297+ return self ._UNSERIALIZABLE
298+
210299 def close (self ):
211300 """Close the underlying HTTP client."""
212301 self ._client .close ()
0 commit comments