1919
2020from collections .abc import Mapping
2121from collections .abc import MutableMapping
22- import contextvars
2322import json
24- import os
2523import sys
2624from typing import Any
2725from typing import Literal
5452GEN_AI_OUTPUT_MESSAGES = 'gen_ai.output.messages'
5553GEN_AI_SYSTEM_INSTRUCTIONS = 'gen_ai.system_instructions'
5654
55+ from .context import TelemetryConfig
56+
5757# Use the import symbol once the minimum OpenTelemetry SDK version is updated to 1.39.0
5858# from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_TOOL_DEFINITIONS
5959GEN_AI_TOOL_DEFINITIONS = 'gen_ai.tool.definitions'
6060
61- OTEL_SEMCONV_STABILITY_OPT_IN = 'OTEL_SEMCONV_STABILITY_OPT_IN'
61+ # Use the import symbol once the minimum OpenTelemetry SDK version is updated to 1.40.0
62+ # from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS
63+ GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = 'gen_ai.usage.cache_read.input_tokens'
6264
63- OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT = (
64- 'OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT'
65- )
65+ GEN_AI_USAGE_REASONING_OUTPUT_TOKENS = 'gen_ai.usage.reasoning.output_tokens'
6666
6767FUNCTION_TOOL_DEFINITION_TYPE = 'function'
6868
@@ -133,7 +133,8 @@ def _safe_json_serialize_no_whitespaces(obj) -> str:
133133 obj: The object to serialize.
134134
135135 Returns:
136- The JSON-serialized object string or <non-serializable> if the object cannot be serialized.
136+ The JSON-serialized object string or <non-serializable> if the object cannot
137+ be serialized.
137138 """
138139
139140 try :
@@ -148,18 +149,43 @@ def _safe_json_serialize_no_whitespaces(obj) -> str:
148149 return '<not serializable>'
149150
150151
151- def is_experimental_semconv () -> bool :
152- opt_ins = os .getenv (OTEL_SEMCONV_STABILITY_OPT_IN )
153- if not opt_ins :
154- return False
155- opt_ins_list = [s .strip () for s in opt_ins .split (',' )]
156- return 'gen_ai_latest_experimental' in opt_ins_list
152+ def is_experimental_semconv (
153+ telemetry_config : TelemetryConfig | None = None ,
154+ ) -> bool :
155+ """Returns whether to emit experimental Generative AI semconv attributes.
156+
157+ Thin wrapper over
158+ :attr:`TelemetryConfig.should_use_experimental_genai_semconv`, which owns the
159+ precedence ladder (admin lock > per-request field > env var > default).
160+
161+ Args:
162+ telemetry_config: The per-request config, or ``None`` for the env-only path
163+ (modeled as an empty :class:`TelemetryConfig`).
164+
165+ Returns:
166+ Whether the experimental GenAI semconv attributes should be emitted.
167+ """
168+ cfg = telemetry_config if telemetry_config is not None else TelemetryConfig ()
169+ return cfg .should_use_experimental_genai_semconv
157170
158171
159- def get_content_capturing_mode () -> str :
160- return os .getenv (
161- OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT , ''
162- ).upper ()
172+ def get_content_capturing_mode (
173+ telemetry_config : TelemetryConfig | None = None ,
174+ ) -> str :
175+ """Returns the experimental GenAI semconv content-capturing mode string.
176+
177+ Thin wrapper over :attr:`TelemetryConfig.content_capturing_mode_value`, which
178+ owns the precedence ladder and the legacy env-string coercion.
179+
180+ Args:
181+ telemetry_config: The per-request config, or ``None`` for the env-only path
182+ (modeled as an empty :class:`TelemetryConfig`).
183+
184+ Returns:
185+ One of ``''`` / ``'EVENT_ONLY'`` / ``'SPAN_ONLY'`` / ``'SPAN_AND_EVENT'``.
186+ """
187+ cfg = telemetry_config if telemetry_config is not None else TelemetryConfig ()
188+ return cfg .content_capturing_mode_value
163189
164190
165191def _model_dump_to_tool_definition (tool : Any ) -> dict [str , Any ]:
@@ -439,12 +465,11 @@ def set_operation_details_common_attributes(
439465 operation_details_common_attributes : MutableMapping [str , AttributeValue ],
440466 attributes : Mapping [str , AttributeValue ],
441467 log_only_attributes : Mapping [str , AttributeValue ] | None = None ,
468+ telemetry_config : TelemetryConfig | None = None ,
442469) -> None :
443470 operation_details_common_attributes .update (attributes )
444- if log_only_attributes and get_content_capturing_mode () in (
445- 'EVENT_ONLY' ,
446- 'SPAN_AND_EVENT' ,
447- ):
471+ cfg = telemetry_config if telemetry_config is not None else TelemetryConfig ()
472+ if log_only_attributes and cfg .should_add_content_to_logs :
448473 operation_details_common_attributes .update (log_only_attributes )
449474
450475
@@ -499,18 +524,19 @@ def maybe_log_completion_details(
499524 otel_logger : Logger ,
500525 operation_details_attributes : Mapping [str , AttributeValue ],
501526 operation_details_common_attributes : Mapping [str , AttributeValue ],
527+ telemetry_config : TelemetryConfig | None = None ,
502528):
503- """Logs completion details based on the experimental semantic convention capturing mode."""
529+ """Logs completion details based on the experimental semconv capturing mode."""
504530 if span is None :
505531 return
506532
507- if not is_experimental_semconv ():
533+ cfg = telemetry_config if telemetry_config is not None else TelemetryConfig ()
534+ if not cfg .should_use_experimental_genai_semconv :
508535 return
509536
510- capturing_mode = get_content_capturing_mode ()
511537 final_attributes = operation_details_common_attributes
512538
513- if capturing_mode in [ 'EVENT_ONLY' , 'SPAN_AND_EVENT' ] :
539+ if cfg . should_add_content_to_logs :
514540 final_attributes = final_attributes | operation_details_attributes
515541 else :
516542 final_attributes = (
@@ -525,7 +551,7 @@ def maybe_log_completion_details(
525551 )
526552 )
527553
528- if capturing_mode in [ 'SPAN_ONLY' , 'SPAN_AND_EVENT' ] :
554+ if cfg . should_add_content_to_experimental_spans :
529555 for key , value in operation_details_attributes .items ():
530556 span .set_attribute (key , _safe_json_serialize_no_whitespaces (value ))
531557 else :
0 commit comments