def traced_llm(func: Optional[Callable] = None, *, name: Optional[str] = None) -> Callable:
    """Traces LLM service methods with LLM-specific attributes.

    Automatically captures and records:
    - Service name and model information
    - Context content and messages
    - Tool configurations
    - Token usage metrics
    - Performance metrics like TTFB
    - Aggregated output text

    Args:
        func: The LLM method to trace.
        name: Custom span name. Defaults to service type and class name.

    Returns:
        Wrapped method with LLM-specific tracing.
    """
    if not is_tracing_available():
        return _noop_decorator if func is None else _noop_decorator(func)

    def decorator(f):
        @functools.wraps(f)
        async def wrapper(self, context, *args, **kwargs):
            try:
                if not is_tracing_available():
                    return await f(self, context, *args, **kwargs)

                service_class_name = self.__class__.__name__
                span_name = "llm"

                # Get the parent context - turn context if available, otherwise service context
                turn_context = get_current_turn_context()
                parent_context = turn_context or _get_parent_service_context(self)

                # Create a new span as child of the turn span or service span
                tracer = trace.get_tracer("pipecat")
                with tracer.start_as_current_span(
                    span_name, context=parent_context
                ) as current_span:
                    try:
                        # Store original method and output aggregator
                        original_push_frame = self.push_frame
                        output_text = ""  # Simple string accumulation

                        async def traced_push_frame(frame, direction=None):
                            nonlocal output_text
                            # Capture text from LLMTextFrame during streaming
                            if (
                                hasattr(frame, "__class__")
                                and frame.__class__.__name__ == "LLMTextFrame"
                                and hasattr(frame, "text")
                            ):
                                output_text += frame.text

                            # Call original
                            if direction is not None:
                                return await original_push_frame(frame, direction)
                            else:
                                return await original_push_frame(frame)

                        # For token usage monitoring
                        original_start_llm_usage_metrics = None
                        if hasattr(self, "start_llm_usage_metrics"):
                            original_start_llm_usage_metrics = self.start_llm_usage_metrics

                            # Override the method to capture token usage
                            @functools.wraps(original_start_llm_usage_metrics)
                            async def wrapped_start_llm_usage_metrics(tokens):
                                # Call the original method
                                await original_start_llm_usage_metrics(tokens)

                                # Add token usage to the current span
                                _add_token_usage_to_span(current_span, tokens)

                            # Replace the method temporarily
                            self.start_llm_usage_metrics = wrapped_start_llm_usage_metrics

                        try:
                            # Replace push_frame to capture output
                            self.push_frame = traced_push_frame

                            # Detect if we're using Google's service
                            is_google_service = "google" in service_class_name.lower()

                            # Try to get messages based on service type
                            messages = None
                            serialized_messages = None

                            # TODO: Revisit once we unify the messages across services
                            if is_google_service:
                                # Handle Google service specifically
                                if hasattr(context, "get_messages_for_logging"):
                                    messages = context.get_messages_for_logging()
                            else:
                                # Handle other services like OpenAI
                                if hasattr(context, "get_messages"):
                                    messages = context.get_messages()
                                elif hasattr(context, "messages"):
                                    messages = context.messages

                            # Serialize messages if available
                            if messages:
                                try:
                                    serialized_messages = json.dumps(messages)
                                except Exception as e:
                                    serialized_messages = f"Error serializing messages: {str(e)}"

                            # Get tools, system message, etc. based on the service type
                            tools = getattr(context, "tools", None)
                            serialized_tools = None
                            tool_count = 0

                            if tools:
                                try:
                                    serialized_tools = json.dumps(tools)
                                    tool_count = len(tools) if isinstance(tools, list) else 1
                                except Exception as e:
                                    serialized_tools = f"Error serializing tools: {str(e)}"

                            # Handle system message for different services
                            system_message = None
                            if 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

                            # Get settings from the service
                            params = {}
                            if hasattr(self, "_settings"):
                                for key, value in self._settings.items():
                                    if key == "extra":
                                        continue
                                    # Add value directly if it's a basic type
                                    if isinstance(value, (int, float, bool, str)):
                                        params[key] = value
                                    elif value is None or (
                                        hasattr(value, "__name__") and value.__name__ == "NOT_GIVEN"
                                    ):
                                        params[key] = "NOT_GIVEN"

                            # Add all available attributes to the span
                            attribute_kwargs = {
                                "service_name": service_class_name,
                                "model": getattr(self, "model_name", "unknown"),
                                "stream": True,  # Most LLM services use streaming
                                "parameters": params,
                            }

                            # Add optional attributes only if they exist
                            if serialized_messages:
                                attribute_kwargs["messages"] = serialized_messages
                            if serialized_tools:
                                attribute_kwargs["tools"] = serialized_tools
                                attribute_kwargs["tool_count"] = tool_count
                            if system_message:
                                attribute_kwargs["system"] = system_message

                            # Add all gathered attributes to the span
                            add_llm_span_attributes(span=current_span, **attribute_kwargs)

                        except Exception as e:
                            logging.warning(f"Error setting up LLM tracing: {e}")
                            # Don't raise - let the function execute anyway

                        # Run function with modified push_frame to capture the output
                        result = await f(self, context, *args, **kwargs)

                        # Add aggregated output after function completes, if available
                        if output_text:
                            current_span.set_attribute("output", output_text)

                        return result

                    finally:
                        # Always restore the original methods
                        self.push_frame = original_push_frame

                        if (
                            "original_start_llm_usage_metrics" in locals()
                            and original_start_llm_usage_metrics
                        ):
                            self.start_llm_usage_metrics = original_start_llm_usage_metrics

                        # Update TTFB metric
                        ttfb: Optional[float] = getattr(
                            getattr(self, "_metrics", None), "ttfb", None
                        )
                        if ttfb is not None:
                            current_span.set_attribute("metrics.ttfb", ttfb)
            except Exception as e:
                logging.error(f"Error in LLM tracing (continuing without tracing): {e}")
                # If tracing fails, fall back to the original function
                return await f(self, context, *args, **kwargs)

        return wrapper

    if func is not None:
        return decorator(func)
    return decorator
Traces LLM service methods with LLM-specific attributes.

    Automatically captures and records:
    - Service name and model information
    - Context content and messages
    - Tool configurations
    - Token usage metrics
    - Performance metrics like TTFB
    - Aggregated output text

    Args:
        func: The LLM method to trace.
        name: Custom span name. Defaults to service type and class name.

    Returns:
        Wrapped method with LLM-specific tracing.
    
src/pipecat/utils/tracing/service_decorators.py