1616
1717from ..agent .agent_result import AgentResult
1818from ..types .content import ContentBlock , Message , Messages
19- from ..types .streaming import StopReason , Usage
19+ from ..types .streaming import Metrics , StopReason , Usage
2020from ..types .tools import ToolResult , ToolUse
2121from ..types .traces import Attributes , AttributeValue
2222
@@ -153,6 +153,28 @@ def _set_attributes(self, span: Span, attributes: Dict[str, AttributeValue]) ->
153153 for key , value in attributes .items ():
154154 span .set_attribute (key , value )
155155
156+ def _add_optional_usage_and_metrics_attributes (
157+ self , attributes : Dict [str , AttributeValue ], usage : Usage , metrics : Metrics
158+ ) -> None :
159+ """Add optional usage and metrics attributes if they have values.
160+
161+ Args:
162+ attributes: Dictionary to add attributes to
163+ usage: Token usage information from the model call
164+ metrics: Metrics from the model call
165+ """
166+ if "cacheReadInputTokens" in usage :
167+ attributes ["gen_ai.usage.cache_read_input_tokens" ] = usage ["cacheReadInputTokens" ]
168+
169+ if "cacheWriteInputTokens" in usage :
170+ attributes ["gen_ai.usage.cache_write_input_tokens" ] = usage ["cacheWriteInputTokens" ]
171+
172+ if metrics .get ("timeToFirstByteMs" , 0 ) > 0 :
173+ attributes ["gen_ai.server.time_to_first_token" ] = metrics ["timeToFirstByteMs" ]
174+
175+ if metrics .get ("latencyMs" , 0 ) > 0 :
176+ attributes ["gen_ai.server.request.duration" ] = metrics ["latencyMs" ]
177+
156178 def _end_span (
157179 self ,
158180 span : Span ,
@@ -277,14 +299,21 @@ def start_model_invoke_span(
277299 return span
278300
279301 def end_model_invoke_span (
280- self , span : Span , message : Message , usage : Usage , stop_reason : StopReason , error : Optional [Exception ] = None
302+ self ,
303+ span : Span ,
304+ message : Message ,
305+ usage : Usage ,
306+ metrics : Metrics ,
307+ stop_reason : StopReason ,
308+ error : Optional [Exception ] = None ,
281309 ) -> None :
282310 """End a model invocation span with results and metrics.
283311
284312 Args:
285313 span: The span to end.
286314 message: The message response from the model.
287315 usage: Token usage information from the model call.
316+ metrics: Metrics from the model call.
288317 stop_reason (StopReason): The reason the model stopped generating.
289318 error: Optional exception if the model call failed.
290319 """
@@ -294,10 +323,11 @@ def end_model_invoke_span(
294323 "gen_ai.usage.completion_tokens" : usage ["outputTokens" ],
295324 "gen_ai.usage.output_tokens" : usage ["outputTokens" ],
296325 "gen_ai.usage.total_tokens" : usage ["totalTokens" ],
297- "gen_ai.usage.cache_read_input_tokens" : usage .get ("cacheReadInputTokens" , 0 ),
298- "gen_ai.usage.cache_write_input_tokens" : usage .get ("cacheWriteInputTokens" , 0 ),
299326 }
300327
328+ # Add optional attributes if they have values
329+ self ._add_optional_usage_and_metrics_attributes (attributes , usage , metrics )
330+
301331 if self .use_latest_genai_conventions :
302332 self ._add_event (
303333 span ,
@@ -307,7 +337,7 @@ def end_model_invoke_span(
307337 [
308338 {
309339 "role" : message ["role" ],
310- "parts" : [{ "type" : "text" , "content" : message ["content" ]}] ,
340+ "parts" : self . _map_content_blocks_to_otel_parts ( message ["content" ]) ,
311341 "finish_reason" : str (stop_reason ),
312342 }
313343 ]
@@ -362,7 +392,7 @@ def start_tool_call_span(self, tool: ToolUse, parent_span: Optional[Span] = None
362392 "type" : "tool_call" ,
363393 "name" : tool ["name" ],
364394 "id" : tool ["toolUseId" ],
365- "arguments" : [{ "content" : tool ["input" ]} ],
395+ "arguments" : tool ["input" ],
366396 }
367397 ],
368398 }
@@ -417,7 +447,7 @@ def end_tool_call_span(
417447 {
418448 "type" : "tool_call_response" ,
419449 "id" : tool_result .get ("toolUseId" , "" ),
420- "result " : tool_result .get ("content" ),
450+ "response " : tool_result .get ("content" ),
421451 }
422452 ],
423453 }
@@ -504,7 +534,7 @@ def end_event_loop_cycle_span(
504534 [
505535 {
506536 "role" : tool_result_message ["role" ],
507- "parts" : [{ "type" : "text" , "content" : tool_result_message ["content" ]}] ,
537+ "parts" : self . _map_content_blocks_to_otel_parts ( tool_result_message ["content" ]) ,
508538 }
509539 ]
510540 )
@@ -634,19 +664,23 @@ def start_multiagent_span(
634664 )
635665
636666 span = self ._start_span (operation , attributes = attributes , span_kind = trace_api .SpanKind .CLIENT )
637- content = serialize (task ) if isinstance (task , list ) else task
638667
639668 if self .use_latest_genai_conventions :
669+ parts : list [dict [str , Any ]] = []
670+ if isinstance (task , list ):
671+ parts = self ._map_content_blocks_to_otel_parts (task )
672+ else :
673+ parts = [{"type" : "text" , "content" : task }]
640674 self ._add_event (
641675 span ,
642676 "gen_ai.client.inference.operation.details" ,
643- {"gen_ai.input.messages" : serialize ([{"role" : "user" , "parts" : [{ "type" : "text" , "content" : task }] }])},
677+ {"gen_ai.input.messages" : serialize ([{"role" : "user" , "parts" : parts }])},
644678 )
645679 else :
646680 self ._add_event (
647681 span ,
648682 "gen_ai.user.message" ,
649- event_attributes = {"content" : content },
683+ event_attributes = {"content" : serialize ( task ) if isinstance ( task , list ) else task },
650684 )
651685
652686 return span
@@ -718,7 +752,7 @@ def _add_event_messages(self, span: Span, messages: Messages) -> None:
718752 input_messages : list = []
719753 for message in messages :
720754 input_messages .append (
721- {"role" : message ["role" ], "parts" : [{ "type" : "text" , "content" : message ["content" ]}] }
755+ {"role" : message ["role" ], "parts" : self . _map_content_blocks_to_otel_parts ( message ["content" ]) }
722756 )
723757 self ._add_event (
724758 span , "gen_ai.client.inference.operation.details" , {"gen_ai.input.messages" : serialize (input_messages )}
@@ -731,6 +765,41 @@ def _add_event_messages(self, span: Span, messages: Messages) -> None:
731765 {"content" : serialize (message ["content" ])},
732766 )
733767
768+ def _map_content_blocks_to_otel_parts (self , content_blocks : list [ContentBlock ]) -> list [dict [str , Any ]]:
769+ """Map ContentBlock objects to OpenTelemetry parts format."""
770+ parts : list [dict [str , Any ]] = []
771+
772+ for block in content_blocks :
773+ if "text" in block :
774+ # Standard TextPart
775+ parts .append ({"type" : "text" , "content" : block ["text" ]})
776+ elif "toolUse" in block :
777+ # Standard ToolCallRequestPart
778+ tool_use = block ["toolUse" ]
779+ parts .append (
780+ {
781+ "type" : "tool_call" ,
782+ "name" : tool_use ["name" ],
783+ "id" : tool_use ["toolUseId" ],
784+ "arguments" : tool_use ["input" ],
785+ }
786+ )
787+ elif "toolResult" in block :
788+ # Standard ToolCallResponsePart
789+ tool_result = block ["toolResult" ]
790+ parts .append (
791+ {
792+ "type" : "tool_call_response" ,
793+ "id" : tool_result ["toolUseId" ],
794+ "response" : tool_result ["content" ],
795+ }
796+ )
797+ else :
798+ # For all other ContentBlock types, use the key as type and value as content
799+ for key , value in block .items ():
800+ parts .append ({"type" : key , "content" : value })
801+ return parts
802+
734803
735804# Singleton instance for global access
736805_tracer_instance = None
0 commit comments