From 9cc264471918e4622a98b8be4b3103cd4e6947f1 Mon Sep 17 00:00:00 2001 From: Kinshuk Bairagi Date: Wed, 14 Jan 2026 21:46:58 +0530 Subject: [PATCH 1/4] Improve system message extraction in traced_llm Enhanced the logic for extracting the system message in the traced_llm decorator to support LLMContext via adapter and handle exceptions gracefully. This improves compatibility with different context types and ensures better tracing information. --- changelog/3449.fixed.md | 1 + .../utils/tracing/service_decorators.py | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 changelog/3449.fixed.md diff --git a/changelog/3449.fixed.md b/changelog/3449.fixed.md new file mode 100644 index 000000000..a8663e004 --- /dev/null +++ b/changelog/3449.fixed.md @@ -0,0 +1 @@ +- Fixed telemetry record correct system_instruction in span for LLM services diff --git a/src/pipecat/utils/tracing/service_decorators.py b/src/pipecat/utils/tracing/service_decorators.py index 304ecb5e8..6f772aa88 100644 --- a/src/pipecat/utils/tracing/service_decorators.py +++ b/src/pipecat/utils/tracing/service_decorators.py @@ -500,7 +500,24 @@ def traced_llm(func: Optional[Callable] = None, *, name: Optional[str] = None) - # Handle system message for different services system_message = None - if hasattr(context, "system"): + # Handle system message for different services + if isinstance(context, LLMContext): + # Universal LLMContext - use adapter to convert and get system message + if hasattr(self, "get_llm_adapter"): + adapter = self.get_llm_adapter() + try: + # Get LLM invocation params which includes system_instruction + params = adapter.get_llm_invocation_params(context) + if ( + isinstance(params, dict) + and "system_instruction" in params + ): + system_message = params["system_instruction"] + except Exception as e: + logging.debug( + f"Could not extract system instruction from adapter: {e}" + ) + elif hasattr(context, "system"): system_message = context.system elif hasattr(context, "system_message"): system_message = context.system_message From e5aaa4c4ebc6eda8d297140587cfaadd95498afb Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 20 Mar 2026 13:12:40 -0400 Subject: [PATCH 2/4] fix: read system_instruction from _settings instead of removed attribute Replace adapter-based extraction in traced_llm with direct reads from _settings.system_instruction (priority) and context messages (fallback). The old approach had three bugs: signature mismatch with Anthropic adapter, key name inconsistency, and unnecessary overhead from full message/tools conversion. Also deduplicate the system instruction in spans -- it was appearing as both "system" and "param.system_instruction". --- changelog/3449.fixed.md | 2 +- .../utils/tracing/service_decorators.py | 42 ++++++++++++------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/changelog/3449.fixed.md b/changelog/3449.fixed.md index a8663e004..7cf01c0cb 100644 --- a/changelog/3449.fixed.md +++ b/changelog/3449.fixed.md @@ -1 +1 @@ -- Fixed telemetry record correct system_instruction in span for LLM services +- Fixed stale `system_instruction` in LLM tracing spans by reading from `_settings.system_instruction` instead of the removed `_system_instruction` attribute. diff --git a/src/pipecat/utils/tracing/service_decorators.py b/src/pipecat/utils/tracing/service_decorators.py index 0b63989ea..2b189feb5 100644 --- a/src/pipecat/utils/tracing/service_decorators.py +++ b/src/pipecat/utils/tracing/service_decorators.py @@ -502,35 +502,45 @@ def traced_llm(func: Optional[Callable] = None, *, name: Optional[str] = None) - # Handle system message for different services system_message = None - # Handle system message for different services if isinstance(context, LLMContext): - # Universal LLMContext - use adapter to convert and get system message - if hasattr(self, "get_llm_adapter"): - adapter = self.get_llm_adapter() - try: - # Get LLM invocation params which includes system_instruction - params = adapter.get_llm_invocation_params(context) + # settings.system_instruction takes priority (matches service behavior) + if hasattr(self, "_settings") and getattr( + self._settings, "system_instruction", None + ): + system_message = self._settings.system_instruction + else: + # Fall back to extracting from context messages + ctx_messages = context.get_messages() + if ctx_messages: + first = ctx_messages[0] if ( - isinstance(params, dict) - and "system_instruction" in params + isinstance(first, dict) + and first.get("role") == "system" ): - system_message = params["system_instruction"] - except Exception as e: - logging.debug( - f"Could not extract system instruction from adapter: {e}" - ) + content = first.get("content") + if isinstance(content, str): + system_message = content + elif isinstance(content, list): + system_message = " ".join( + part.get("text", "") + for part in content + if isinstance(part, dict) + and part.get("type") == "text" + ) elif hasattr(context, "system"): system_message = context.system elif hasattr(context, "system_message"): system_message = context.system_message - elif hasattr(self, "_system_instruction"): - system_message = self._system_instruction # Use given_fields() defensively in case a service doesn't # initialize all settings. params = {} if hasattr(self, "_settings"): for key, value in self._settings.given_fields().items(): + # system_instruction is already captured as the + # "system" span attribute above. + if key == "system_instruction": + continue if isinstance(value, (int, float, bool, str)): params[key] = value elif value is None: From b5c362d6e61afaa0d2ac1dc958ef68da5be78241 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 20 Mar 2026 13:20:03 -0400 Subject: [PATCH 3/4] refactor: rename tracing span attribute "system" to "system_instructions" Align with the OpenTelemetry GenAI semantic convention gen_ai.system_instructions for system prompts. The old "system" attribute name was unrelated to gen_ai.system (which is for provider name). --- changelog/3449.changed.md | 1 + src/pipecat/utils/tracing/service_attributes.py | 8 ++++---- src/pipecat/utils/tracing/service_decorators.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 changelog/3449.changed.md diff --git a/changelog/3449.changed.md b/changelog/3449.changed.md new file mode 100644 index 000000000..d06ad37c4 --- /dev/null +++ b/changelog/3449.changed.md @@ -0,0 +1 @@ +- Renamed tracing span attribute `system` to `system_instructions` to align with the OpenTelemetry GenAI semantic conventions. diff --git a/src/pipecat/utils/tracing/service_attributes.py b/src/pipecat/utils/tracing/service_attributes.py index 21a22341f..1a6537f2b 100644 --- a/src/pipecat/utils/tracing/service_attributes.py +++ b/src/pipecat/utils/tracing/service_attributes.py @@ -193,7 +193,7 @@ def add_llm_span_attributes( tools: Optional[str] = None, tool_count: Optional[int] = None, tool_choice: Optional[str] = None, - system: Optional[str] = None, + system_instructions: Optional[str] = None, parameters: Optional[Dict[str, Any]] = None, extra_parameters: Optional[Dict[str, Any]] = None, ttfb: Optional[float] = None, @@ -211,7 +211,7 @@ def add_llm_span_attributes( tools: JSON-serialized tools configuration. tool_count: Number of tools available. tool_choice: Tool selection configuration. - system: System message. + system_instructions: System instructions. parameters: Service parameters. extra_parameters: Additional parameters. ttfb: Time to first byte in seconds. @@ -240,8 +240,8 @@ def add_llm_span_attributes( if tool_choice: span.set_attribute("tool_choice", tool_choice) - if system: - span.set_attribute("system", system) + if system_instructions: + span.set_attribute("system_instructions", system_instructions) if ttfb is not None: span.set_attribute("metrics.ttfb", ttfb) diff --git a/src/pipecat/utils/tracing/service_decorators.py b/src/pipecat/utils/tracing/service_decorators.py index 2b189feb5..2e54732f1 100644 --- a/src/pipecat/utils/tracing/service_decorators.py +++ b/src/pipecat/utils/tracing/service_decorators.py @@ -538,7 +538,7 @@ def traced_llm(func: Optional[Callable] = None, *, name: Optional[str] = None) - if hasattr(self, "_settings"): for key, value in self._settings.given_fields().items(): # system_instruction is already captured as the - # "system" span attribute above. + # "system_instructions" span attribute above. if key == "system_instruction": continue if isinstance(value, (int, float, bool, str)): @@ -561,7 +561,7 @@ def traced_llm(func: Optional[Callable] = None, *, name: Optional[str] = None) - attribute_kwargs["tools"] = serialized_tools attribute_kwargs["tool_count"] = tool_count if system_message: - attribute_kwargs["system"] = system_message + attribute_kwargs["system_instructions"] = system_message # Add all gathered attributes to the span add_llm_span_attributes(span=current_span, **attribute_kwargs) From c89e36673912839fb2c5262ac6ecf2d231591545 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 20 Mar 2026 13:36:20 -0400 Subject: [PATCH 4/4] refactor: align tracing attributes with OpenTelemetry GenAI conventions - gen_ai.system -> gen_ai.provider.name (deprecated) - system / system_instructions -> gen_ai.system_instructions - gen_ai.usage.cache_read_input_tokens -> gen_ai.usage.cache_read.input_tokens - gen_ai.usage.cache_creation_input_tokens -> gen_ai.usage.cache_creation.input_tokens --- changelog/3449.changed.md | 2 +- .../utils/tracing/service_attributes.py | 22 +++++++++---------- .../utils/tracing/service_decorators.py | 8 +++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/changelog/3449.changed.md b/changelog/3449.changed.md index d06ad37c4..d5ea2f6d7 100644 --- a/changelog/3449.changed.md +++ b/changelog/3449.changed.md @@ -1 +1 @@ -- Renamed tracing span attribute `system` to `system_instructions` to align with the OpenTelemetry GenAI semantic conventions. +- Renamed tracing span attributes to align with OpenTelemetry GenAI semantic conventions: `gen_ai.system` to `gen_ai.provider.name`, `system` to `gen_ai.system_instructions`, `gen_ai.usage.cache_read_input_tokens` to `gen_ai.usage.cache_read.input_tokens`, and `gen_ai.usage.cache_creation_input_tokens` to `gen_ai.usage.cache_creation.input_tokens`. diff --git a/src/pipecat/utils/tracing/service_attributes.py b/src/pipecat/utils/tracing/service_attributes.py index 1a6537f2b..e5cf4a83f 100644 --- a/src/pipecat/utils/tracing/service_attributes.py +++ b/src/pipecat/utils/tracing/service_attributes.py @@ -25,20 +25,20 @@ if is_tracing_available(): from opentelemetry.trace import Span -def _get_gen_ai_system_from_service_name(service_name: str) -> str: - """Extract the standardized gen_ai.system value from a service class name. +def _get_provider_name_from_service_name(service_name: str) -> str: + """Extract the standardized gen_ai.provider.name value from a service class name. Source: - https://opentelemetry.io/docs/specs/semconv/attributes-registry/gen-ai/#gen-ai-system + https://opentelemetry.io/docs/specs/semconv/attributes-registry/gen-ai/ Uses standard OTel names where possible, with special case mappings for service names that don't follow the pattern. Args: - service_name: The service class name to extract system name from. + service_name: The service class name to extract provider name from. Returns: - The standardized gen_ai.system value. + The standardized gen_ai.provider.name value. """ SPECIAL_CASE_MAPPINGS = { # AWS @@ -91,7 +91,7 @@ def add_tts_span_attributes( **kwargs: Additional attributes to add. """ # Add standard attributes - span.set_attribute("gen_ai.system", service_name.replace("TTSService", "").lower()) + span.set_attribute("gen_ai.provider.name", service_name.replace("TTSService", "").lower()) span.set_attribute("gen_ai.request.model", model) span.set_attribute("gen_ai.operation.name", operation_name) span.set_attribute("gen_ai.output.type", "speech") @@ -150,7 +150,7 @@ def add_stt_span_attributes( **kwargs: Additional attributes to add. """ # Add standard attributes - span.set_attribute("gen_ai.system", service_name.replace("STTService", "").lower()) + span.set_attribute("gen_ai.provider.name", service_name.replace("STTService", "").lower()) span.set_attribute("gen_ai.request.model", model) span.set_attribute("gen_ai.operation.name", operation_name) span.set_attribute("vad_enabled", vad_enabled) @@ -218,7 +218,7 @@ def add_llm_span_attributes( **kwargs: Additional attributes to add. """ # Add standard attributes - span.set_attribute("gen_ai.system", _get_gen_ai_system_from_service_name(service_name)) + span.set_attribute("gen_ai.provider.name", _get_provider_name_from_service_name(service_name)) span.set_attribute("gen_ai.request.model", model) span.set_attribute("gen_ai.operation.name", "chat") span.set_attribute("gen_ai.output.type", "text") @@ -241,7 +241,7 @@ def add_llm_span_attributes( span.set_attribute("tool_choice", tool_choice) if system_instructions: - span.set_attribute("system_instructions", system_instructions) + span.set_attribute("gen_ai.system_instructions", system_instructions) if ttfb is not None: span.set_attribute("metrics.ttfb", ttfb) @@ -313,7 +313,7 @@ def add_gemini_live_span_attributes( **kwargs: Additional attributes to add. """ # Add standard attributes - span.set_attribute("gen_ai.system", "gcp.gemini") + span.set_attribute("gen_ai.provider.name", "gcp.gemini") span.set_attribute("gen_ai.request.model", model) span.set_attribute("gen_ai.operation.name", operation_name) span.set_attribute("service.operation", operation_name) @@ -414,7 +414,7 @@ def add_openai_realtime_span_attributes( **kwargs: Additional attributes to add. """ # Add standard attributes - span.set_attribute("gen_ai.system", "openai") + span.set_attribute("gen_ai.provider.name", "openai") span.set_attribute("gen_ai.request.model", model) span.set_attribute("gen_ai.operation.name", operation_name) span.set_attribute("service.operation", operation_name) diff --git a/src/pipecat/utils/tracing/service_decorators.py b/src/pipecat/utils/tracing/service_decorators.py index 2e54732f1..cc353e1a3 100644 --- a/src/pipecat/utils/tracing/service_decorators.py +++ b/src/pipecat/utils/tracing/service_decorators.py @@ -137,14 +137,14 @@ def _add_token_usage_to_span(span, token_usage): and token_usage["cache_read_input_tokens"] is not None ): span.set_attribute( - "gen_ai.usage.cache_read_input_tokens", token_usage["cache_read_input_tokens"] + "gen_ai.usage.cache_read.input_tokens", token_usage["cache_read_input_tokens"] ) if ( "cache_creation_input_tokens" in token_usage and token_usage["cache_creation_input_tokens"] is not None ): span.set_attribute( - "gen_ai.usage.cache_creation_input_tokens", + "gen_ai.usage.cache_creation.input_tokens", token_usage["cache_creation_input_tokens"], ) if "reasoning_tokens" in token_usage and token_usage["reasoning_tokens"] is not None: @@ -159,11 +159,11 @@ def _add_token_usage_to_span(span, token_usage): # Add cached token metrics for LLMTokenUsage object cache_read_tokens = getattr(token_usage, "cache_read_input_tokens", None) if cache_read_tokens is not None: - span.set_attribute("gen_ai.usage.cache_read_input_tokens", cache_read_tokens) + span.set_attribute("gen_ai.usage.cache_read.input_tokens", cache_read_tokens) cache_creation_tokens = getattr(token_usage, "cache_creation_input_tokens", None) if cache_creation_tokens is not None: - span.set_attribute("gen_ai.usage.cache_creation_input_tokens", cache_creation_tokens) + span.set_attribute("gen_ai.usage.cache_creation.input_tokens", cache_creation_tokens) reasoning_tokens = getattr(token_usage, "reasoning_tokens", None) if reasoning_tokens is not None: