2929from opentelemetry .instrumentation .bedrock .span_utils import (
3030 converse_usage_record ,
3131 set_converse_input_prompt_span_attributes ,
32+ _set_converse_finish_reasons ,
33+ _set_finish_reasons_unconditionally ,
3234 set_converse_model_span_attributes ,
3335 set_converse_response_span_attributes ,
3436 set_converse_streaming_response_span_attributes ,
4850 unwrap ,
4951)
5052from opentelemetry .metrics import Counter , Histogram , Meter , get_meter
53+ from opentelemetry .semconv ._incubating .attributes import (
54+ gen_ai_attributes as GenAIAttributes ,
55+ )
56+ from opentelemetry .semconv ._incubating .attributes .gen_ai_attributes import (
57+ GenAiOperationNameValues ,
58+ GenAiSystemValues ,
59+ )
5160from opentelemetry .semconv_ai import (
5261 SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY ,
5362 Meters ,
@@ -104,8 +113,9 @@ def __init__(
104113 {"package" : "botocore.session" , "object" : "Session" , "method" : "create_client" },
105114]
106115
107- _BEDROCK_INVOKE_SPAN_NAME = "bedrock.completion"
108- _BEDROCK_CONVERSE_SPAN_NAME = "bedrock.converse"
116+ def _span_name (operation_name , model ):
117+ """Build span name per OTel semconv: '{operation_name} {model}'."""
118+ return f"{ operation_name } { model } " if model else operation_name
109119
110120
111121def is_metrics_enabled () -> bool :
@@ -200,8 +210,15 @@ def with_instrumentation(*args, **kwargs):
200210 if context_api .get_value (SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY ):
201211 return fn (* args , ** kwargs )
202212
213+ (provider , _model_vendor , _model ) = _get_vendor_model (kwargs .get ("modelId" ))
214+ operation_name = _derive_operation_name (kwargs )
215+ span_attributes = {
216+ GenAIAttributes .GEN_AI_PROVIDER_NAME : provider ,
217+ GenAIAttributes .GEN_AI_OPERATION_NAME : operation_name ,
218+ GenAIAttributes .GEN_AI_REQUEST_MODEL : _model ,
219+ }
203220 with tracer .start_as_current_span (
204- _BEDROCK_INVOKE_SPAN_NAME , kind = SpanKind .CLIENT
221+ _span_name ( operation_name , _model ), kind = SpanKind .CLIENT , attributes = span_attributes
205222 ) as span :
206223 response = fn (* args , ** kwargs )
207224 _handle_call (span , kwargs , response , metric_params , event_logger )
@@ -218,7 +235,18 @@ def with_instrumentation(*args, **kwargs):
218235 if context_api .get_value (SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY ):
219236 return fn (* args , ** kwargs )
220237
221- span = tracer .start_span (_BEDROCK_INVOKE_SPAN_NAME , kind = SpanKind .CLIENT )
238+ (provider , _model_vendor , _model ) = _get_vendor_model (kwargs .get ("modelId" ))
239+ operation_name = _derive_operation_name (kwargs )
240+ span_attributes = {
241+ GenAIAttributes .GEN_AI_PROVIDER_NAME : provider ,
242+ GenAIAttributes .GEN_AI_OPERATION_NAME : operation_name ,
243+ GenAIAttributes .GEN_AI_REQUEST_MODEL : _model ,
244+ }
245+ span = tracer .start_span (
246+ _span_name (operation_name , _model ),
247+ kind = SpanKind .CLIENT ,
248+ attributes = span_attributes ,
249+ )
222250
223251 response = fn (* args , ** kwargs )
224252 _handle_stream_call (span , kwargs , response , metric_params , event_logger )
@@ -237,8 +265,16 @@ def with_instrumentation(*args, **kwargs):
237265 if context_api .get_value (SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY ):
238266 return fn (* args , ** kwargs )
239267
268+ (provider , _model_vendor , _model ) = _get_vendor_model (kwargs .get ("modelId" ))
269+ span_attributes = {
270+ GenAIAttributes .GEN_AI_PROVIDER_NAME : provider ,
271+ GenAIAttributes .GEN_AI_OPERATION_NAME : GenAiOperationNameValues .CHAT .value ,
272+ GenAIAttributes .GEN_AI_REQUEST_MODEL : _model ,
273+ }
240274 with tracer .start_as_current_span (
241- _BEDROCK_CONVERSE_SPAN_NAME , kind = SpanKind .CLIENT
275+ _span_name (GenAiOperationNameValues .CHAT .value , _model ),
276+ kind = SpanKind .CLIENT ,
277+ attributes = span_attributes ,
242278 ) as span :
243279 response = fn (* args , ** kwargs )
244280 _handle_converse (span , kwargs , response , metric_params , event_logger )
@@ -254,7 +290,17 @@ def with_instrumentation(*args, **kwargs):
254290 if context_api .get_value (SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY ):
255291 return fn (* args , ** kwargs )
256292
257- span = tracer .start_span (_BEDROCK_CONVERSE_SPAN_NAME , kind = SpanKind .CLIENT )
293+ (provider , _model_vendor , _model ) = _get_vendor_model (kwargs .get ("modelId" ))
294+ span_attributes = {
295+ GenAIAttributes .GEN_AI_PROVIDER_NAME : provider ,
296+ GenAIAttributes .GEN_AI_OPERATION_NAME : GenAiOperationNameValues .CHAT .value ,
297+ GenAIAttributes .GEN_AI_REQUEST_MODEL : _model ,
298+ }
299+ span = tracer .start_span (
300+ _span_name (GenAiOperationNameValues .CHAT .value , _model ),
301+ kind = SpanKind .CLIENT ,
302+ attributes = span_attributes ,
303+ )
258304 response = fn (* args , ** kwargs )
259305 if span .is_recording ():
260306 _handle_converse_stream (span , kwargs , response , metric_params , event_logger )
@@ -299,6 +345,7 @@ def stream_done(response_body):
299345 if should_emit_events () and event_logger :
300346 emit_message_events (event_logger , kwargs )
301347 emit_streaming_response_event (response_body , event_logger )
348+ _set_finish_reasons_unconditionally (model_vendor , span , response_body )
302349 else :
303350 set_model_message_span_attributes (model_vendor , span , request_body )
304351 set_model_choice_span_attributes (model_vendor , span , response_body )
@@ -345,6 +392,7 @@ def _handle_call(span: Span, kwargs, response, metric_params, event_logger):
345392 if should_emit_events () and event_logger :
346393 emit_message_events (event_logger , kwargs )
347394 emit_choice_events (event_logger , response )
395+ _set_finish_reasons_unconditionally (model_vendor , span , response_body )
348396 else :
349397 set_model_message_span_attributes (model_vendor , span , request_body )
350398 set_model_choice_span_attributes (model_vendor , span , response_body )
@@ -362,6 +410,7 @@ def _handle_converse(span, kwargs, response, metric_params, event_logger):
362410 if should_emit_events () and event_logger :
363411 emit_input_events_converse (kwargs , event_logger )
364412 emit_response_event_converse (response , event_logger )
413+ _set_converse_finish_reasons (span , response .get ("stopReason" ))
365414 else :
366415 set_converse_input_prompt_span_attributes (kwargs , span )
367416 set_converse_response_span_attributes (response , span )
@@ -385,11 +434,28 @@ def _handle_converse_stream(span, kwargs, response, metric_params, event_logger)
385434 def handler (func ):
386435 def wrap (* args , ** kwargs ):
387436 response_msg = kwargs .pop ("response_msg" )
437+ tool_blocks = kwargs .pop ("tool_blocks" )
438+ reasoning_blocks = kwargs .pop ("reasoning_blocks" )
388439 span = kwargs .pop ("span" )
389440 event = func (* args , ** kwargs )
390441 nonlocal role
391- if "contentBlockDelta" in event and "text" in event ["contentBlockDelta" ].get ("delta" , {}):
392- response_msg .append (event ["contentBlockDelta" ]["delta" ]["text" ])
442+ if "contentBlockDelta" in event :
443+ delta = event ["contentBlockDelta" ].get ("delta" , {})
444+ if "text" in delta :
445+ response_msg .append (delta ["text" ])
446+ if "toolUse" in delta :
447+ # Merge delta input into the last tool block (created by contentBlockStart)
448+ if tool_blocks :
449+ tool_blocks [- 1 ].setdefault ("input" , "" )
450+ tool_blocks [- 1 ]["input" ] += delta ["toolUse" ].get ("input" , "" )
451+ else :
452+ tool_blocks .append (delta ["toolUse" ])
453+ if "reasoningContent" in delta :
454+ reasoning_blocks .append (delta ["reasoningContent" ].get ("text" , "" ))
455+ elif "contentBlockStart" in event :
456+ start = event ["contentBlockStart" ].get ("start" , {})
457+ if "toolUse" in start :
458+ tool_blocks .append (start ["toolUse" ])
393459 elif "messageStart" in event :
394460 role = event ["messageStart" ]["role" ]
395461 elif "metadata" in event :
@@ -398,29 +464,36 @@ def wrap(*args, **kwargs):
398464 converse_usage_record (span , event ["metadata" ], metric_params )
399465 span .end ()
400466 elif "messageStop" in event :
467+ stop_reason = event .get ("messageStop" , {}).get ("stopReason" )
401468 if should_emit_events () and event_logger :
402469 emit_streaming_converse_response_event (
403470 event_logger ,
404471 response_msg ,
405472 role ,
406- event . get ( "messageStop" , {}). get ( "stopReason" , "unknown" ) ,
473+ stop_reason ,
407474 )
475+ _set_converse_finish_reasons (span , stop_reason )
408476 else :
409477 set_converse_streaming_response_span_attributes (
410- response_msg , role , span
478+ response_msg ,
479+ role ,
480+ span ,
481+ finish_reason = stop_reason ,
482+ tool_blocks = tool_blocks ,
483+ reasoning_blocks = reasoning_blocks ,
411484 )
412485
413486 return event
414487
415- return partial (wrap , response_msg = [], span = span )
488+ return partial (wrap , response_msg = [], tool_blocks = [], reasoning_blocks = [], span = span )
416489
417490 stream ._parse_event = handler (stream ._parse_event )
418491
419492
420493def _get_vendor_model (modelId ):
421494 # Docs:
422495 # https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html#inference-profiles-support-system
423- provider = "AWS"
496+ provider = GenAiSystemValues . AWS_BEDROCK . value
424497 model_vendor = "imported_model"
425498 model = modelId
426499
@@ -449,19 +522,32 @@ def _cross_region_check(value):
449522 return model_vendor , model
450523
451524
525+ def _derive_operation_name (kwargs ):
526+ """Derive operation name for invoke_model spans prior to creation."""
527+ body_str = kwargs .get ("body" )
528+ if body_str :
529+ try :
530+ body = json .loads (body_str )
531+ if isinstance (body , dict ) and "messages" in body :
532+ return GenAiOperationNameValues .CHAT .value
533+ except (json .JSONDecodeError , TypeError ):
534+ pass
535+ return GenAiOperationNameValues .TEXT_COMPLETION .value
536+
537+
452538class GuardrailMeters :
453- LLM_BEDROCK_GUARDRAIL_ACTIVATION = "gen_ai.bedrock.guardrail.activation"
454- LLM_BEDROCK_GUARDRAIL_LATENCY = "gen_ai.bedrock.guardrail.latency"
455- LLM_BEDROCK_GUARDRAIL_COVERAGE = "gen_ai.bedrock.guardrail.coverage"
456- LLM_BEDROCK_GUARDRAIL_SENSITIVE = "gen_ai.bedrock.guardrail.sensitive_info"
457- LLM_BEDROCK_GUARDRAIL_TOPICS = "gen_ai.bedrock.guardrail.topics"
458- LLM_BEDROCK_GUARDRAIL_CONTENT = "gen_ai.bedrock.guardrail.content"
459- LLM_BEDROCK_GUARDRAIL_WORDS = "gen_ai.bedrock.guardrail.words"
539+ GEN_AI_BEDROCK_GUARDRAIL_ACTIVATION = "gen_ai.bedrock.guardrail.activation"
540+ GEN_AI_BEDROCK_GUARDRAIL_LATENCY = "gen_ai.bedrock.guardrail.latency"
541+ GEN_AI_BEDROCK_GUARDRAIL_COVERAGE = "gen_ai.bedrock.guardrail.coverage"
542+ GEN_AI_BEDROCK_GUARDRAIL_SENSITIVE = "gen_ai.bedrock.guardrail.sensitive_info"
543+ GEN_AI_BEDROCK_GUARDRAIL_TOPICS = "gen_ai.bedrock.guardrail.topics"
544+ GEN_AI_BEDROCK_GUARDRAIL_CONTENT = "gen_ai.bedrock.guardrail.content"
545+ GEN_AI_BEDROCK_GUARDRAIL_WORDS = "gen_ai.bedrock.guardrail.words"
460546
461547
462548class PromptCaching :
463549 # will be moved under the AI SemConv. Not namespaced since also OpenAI supports this.
464- LLM_BEDROCK_PROMPT_CACHING = "gen_ai.prompt.caching"
550+ GEN_AI_PROMPT_CACHING = "gen_ai.prompt.caching"
465551
466552
467553def _create_metrics (meter : Meter ):
@@ -484,58 +570,57 @@ def _create_metrics(meter: Meter):
484570 )
485571
486572 exception_counter = meter .create_counter (
487- # TODO: will fix this in future as a consolidation for semantic convention
488- name = "llm.bedrock.completions.exceptions" ,
573+ name = "gen_ai.bedrock.completions.exceptions" ,
489574 unit = "time" ,
490575 description = "Number of exceptions occurred during chat completions" ,
491576 )
492577
493578 # Guardrail metrics
494579 guardrail_activation = meter .create_counter (
495- name = GuardrailMeters .LLM_BEDROCK_GUARDRAIL_ACTIVATION ,
580+ name = GuardrailMeters .GEN_AI_BEDROCK_GUARDRAIL_ACTIVATION ,
496581 unit = "" ,
497582 description = "Number of guardrail activation" ,
498583 )
499584
500585 guardrail_latency_histogram = meter .create_histogram (
501- name = GuardrailMeters .LLM_BEDROCK_GUARDRAIL_LATENCY ,
586+ name = GuardrailMeters .GEN_AI_BEDROCK_GUARDRAIL_LATENCY ,
502587 unit = "ms" ,
503588 description = "GenAI guardrail latency" ,
504589 )
505590
506591 guardrail_coverage = meter .create_counter (
507- name = GuardrailMeters .LLM_BEDROCK_GUARDRAIL_COVERAGE ,
592+ name = GuardrailMeters .GEN_AI_BEDROCK_GUARDRAIL_COVERAGE ,
508593 unit = "char" ,
509594 description = "GenAI guardrail coverage" ,
510595 )
511596
512597 guardrail_sensitive_info = meter .create_counter (
513- name = GuardrailMeters .LLM_BEDROCK_GUARDRAIL_SENSITIVE ,
598+ name = GuardrailMeters .GEN_AI_BEDROCK_GUARDRAIL_SENSITIVE ,
514599 unit = "" ,
515600 description = "GenAI guardrail sensitive information protection" ,
516601 )
517602
518603 guardrail_topic = meter .create_counter (
519- name = GuardrailMeters .LLM_BEDROCK_GUARDRAIL_TOPICS ,
604+ name = GuardrailMeters .GEN_AI_BEDROCK_GUARDRAIL_TOPICS ,
520605 unit = "" ,
521606 description = "GenAI guardrail topics protection" ,
522607 )
523608
524609 guardrail_content = meter .create_counter (
525- name = GuardrailMeters .LLM_BEDROCK_GUARDRAIL_CONTENT ,
610+ name = GuardrailMeters .GEN_AI_BEDROCK_GUARDRAIL_CONTENT ,
526611 unit = "" ,
527612 description = "GenAI guardrail content filter protection" ,
528613 )
529614
530615 guardrail_words = meter .create_counter (
531- name = GuardrailMeters .LLM_BEDROCK_GUARDRAIL_WORDS ,
616+ name = GuardrailMeters .GEN_AI_BEDROCK_GUARDRAIL_WORDS ,
532617 unit = "" ,
533618 description = "GenAI guardrail words filter protection" ,
534619 )
535620
536621 # Prompt Caching
537622 prompt_caching = meter .create_counter (
538- name = PromptCaching .LLM_BEDROCK_PROMPT_CACHING ,
623+ name = PromptCaching .GEN_AI_PROMPT_CACHING ,
539624 unit = "" ,
540625 description = "Number of cached tokens" ,
541626 )
0 commit comments