77import random
88import threading
99import time
10- from collections .abc import Callable
10+ from collections .abc import Callable , Mapping , Sequence
1111from functools import cached_property
1212from typing import Any , cast
1313
@@ -318,7 +318,10 @@ def _should_sanitize_for_openai_tracing_api(self) -> bool:
318318 return self .endpoint .rstrip ("/" ) == self ._OPENAI_TRACING_INGEST_ENDPOINT .rstrip ("/" )
319319
320320 def _sanitize_for_openai_tracing_api (self , payload_item : dict [str , Any ]) -> dict [str , Any ]:
321- """Drop or truncate span fields known to be rejected by traces ingest."""
321+ """Omit reasoning from generation data and enforce traces ingest field limits.
322+
323+ Original spans remain available to custom processors and exporters.
324+ """
322325 span_data = payload_item .get ("span_data" )
323326 if not isinstance (span_data , dict ):
324327 return payload_item
@@ -329,7 +332,10 @@ def _sanitize_for_openai_tracing_api(self, payload_item: dict[str, Any]) -> dict
329332 for field_name in ("input" , "output" ):
330333 if field_name not in span_data :
331334 continue
332- sanitized_field = self ._truncate_span_field_value (span_data [field_name ])
335+ field_value = span_data [field_name ]
336+ if span_data .get ("type" ) == "generation" :
337+ field_value = self ._omit_generation_reasoning (field_value )
338+ sanitized_field = self ._truncate_span_field_value (field_value )
333339 if sanitized_field is span_data [field_name ]:
334340 continue
335341 if not did_mutate :
@@ -377,6 +383,46 @@ def _sanitize_for_openai_tracing_api(self, payload_item: dict[str, Any]) -> dict
377383 sanitized_payload_item ["span_data" ] = sanitized_span_data
378384 return sanitized_payload_item
379385
386+ def _omit_generation_reasoning (self , value : Any ) -> Any :
387+ """Filter SDK generation message shapes without traversing arbitrary user data."""
388+ if not isinstance (value , Sequence ) or isinstance (value , str | bytes | bytearray ):
389+ return value
390+
391+ def omit_message_reasoning (item : Any ) -> Any :
392+ if not isinstance (item , Mapping ):
393+ return item
394+ item = dict (item )
395+ if item .get ("role" ) == "assistant" :
396+ for key in ("reasoning" , "reasoning_content" , "thinking_blocks" , "thinking" ):
397+ item .pop (key , None )
398+ content = item .get ("content" )
399+ if isinstance (content , list ):
400+ # Legacy thinking replay uses inline Anthropic content blocks.
401+ item ["content" ] = [
402+ part
403+ for part in content
404+ if not isinstance (part , dict )
405+ or part .get ("type" ) not in ("thinking" , "redacted_thinking" )
406+ ]
407+ return item
408+
409+ def omit_items (items : Sequence [Any ]) -> list [Any ]:
410+ # Third-party reasoning_content is also normalized into summary text.
411+ return [
412+ omit_message_reasoning (item )
413+ for item in items
414+ if not isinstance (item , Mapping ) or item .get ("type" ) != "reasoning"
415+ ]
416+
417+ return [
418+ {** item , "output" : omit_items (item ["output" ])}
419+ if isinstance (item , Mapping )
420+ and item .get ("object" ) == "response"
421+ and isinstance (item .get ("output" ), list )
422+ else item
423+ for item in omit_items (value )
424+ ]
425+
380426 def _value_json_size_bytes (self , value : Any ) -> int :
381427 try :
382428 serialized = json .dumps (value , ensure_ascii = False , separators = ("," , ":" ))
0 commit comments