Merge pull request #3011 from thsunkid/feat/add-cached-reasoning-tokens-metrics-to-opentel-spans

This commit is contained in:
Mark Backman
2025-11-26 07:45:33 -05:00
committed by GitHub
6 changed files with 63 additions and 4 deletions

View File

@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added `cache_read_input_tokens`, `cache_creation_input_tokens` and
`reasoning_tokens` to OTel spans for LLM call
- Added `LiveKitRESTHelper` utility class for managing LiveKit rooms via REST API. - Added `LiveKitRESTHelper` utility class for managing LiveKit rooms via REST API.
- Added `DeepgramSageMakerSTTService` which connects to a SageMaker hosted - Added `DeepgramSageMakerSTTService` which connects to a SageMaker hosted

View File

@@ -1723,6 +1723,8 @@ class GeminiLiveLLMService(LLMService):
prompt_tokens=prompt_tokens, prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens, completion_tokens=completion_tokens,
total_tokens=total_tokens, total_tokens=total_tokens,
cache_read_input_tokens=usage.cached_content_token_count,
reasoning_tokens=usage.thoughts_token_count,
) )
await self.start_llm_usage_metrics(tokens) await self.start_llm_usage_metrics(tokens)

View File

@@ -123,6 +123,8 @@ class GrokLLMService(OpenAILLMService):
self._prompt_tokens = 0 self._prompt_tokens = 0
self._completion_tokens = 0 self._completion_tokens = 0
self._total_tokens = 0 self._total_tokens = 0
self._cache_read_input_tokens = None
self._reasoning_tokens = None
self._has_reported_prompt_tokens = False self._has_reported_prompt_tokens = False
self._is_processing = True self._is_processing = True
@@ -137,6 +139,8 @@ class GrokLLMService(OpenAILLMService):
prompt_tokens=self._prompt_tokens, prompt_tokens=self._prompt_tokens,
completion_tokens=self._completion_tokens, completion_tokens=self._completion_tokens,
total_tokens=self._total_tokens, total_tokens=self._total_tokens,
cache_read_input_tokens=self._cache_read_input_tokens,
reasoning_tokens=self._reasoning_tokens,
) )
await super().start_llm_usage_metrics(tokens) await super().start_llm_usage_metrics(tokens)
@@ -149,7 +153,7 @@ class GrokLLMService(OpenAILLMService):
Args: Args:
tokens: The token usage metrics for the current chunk of processing, tokens: The token usage metrics for the current chunk of processing,
containing prompt_tokens and completion_tokens counts. containing prompt_tokens, completion_tokens, and optional cached/reasoning tokens.
""" """
# Only accumulate metrics during active processing # Only accumulate metrics during active processing
if not self._is_processing: if not self._is_processing:
@@ -164,6 +168,13 @@ class GrokLLMService(OpenAILLMService):
if tokens.completion_tokens > self._completion_tokens: if tokens.completion_tokens > self._completion_tokens:
self._completion_tokens = tokens.completion_tokens self._completion_tokens = tokens.completion_tokens
# Capture cached & reasoning tokens (these typically only appear once per request)
if tokens.cache_read_input_tokens is not None:
self._cache_read_input_tokens = tokens.cache_read_input_tokens
if tokens.reasoning_tokens is not None:
self._reasoning_tokens = tokens.reasoning_tokens
def create_context_aggregator( def create_context_aggregator(
self, self,
context: OpenAILLMContext, context: OpenAILLMContext,

View File

@@ -346,11 +346,17 @@ class BaseOpenAILLMService(LLMService):
if chunk.usage.prompt_tokens_details if chunk.usage.prompt_tokens_details
else None else None
) )
reasoning_tokens = (
chunk.usage.completion_tokens_details.reasoning_tokens
if chunk.usage.completion_tokens_details
else None
)
tokens = LLMTokenUsage( tokens = LLMTokenUsage(
prompt_tokens=chunk.usage.prompt_tokens, prompt_tokens=chunk.usage.prompt_tokens,
completion_tokens=chunk.usage.completion_tokens, completion_tokens=chunk.usage.completion_tokens,
total_tokens=chunk.usage.total_tokens, total_tokens=chunk.usage.total_tokens,
cache_read_input_tokens=cached_tokens, cache_read_input_tokens=cached_tokens,
reasoning_tokens=reasoning_tokens,
) )
await self.start_llm_usage_metrics(tokens) await self.start_llm_usage_metrics(tokens)

View File

@@ -57,7 +57,6 @@ from pipecat.processors.aggregators.openai_llm_context import (
) )
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.llm_service import FunctionCallFromLLM, LLMService from pipecat.services.llm_service import FunctionCallFromLLM, LLMService
from pipecat.services.openai.llm import OpenAIContextAggregatorPair
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_openai_realtime, traced_stt from pipecat.utils.tracing.service_decorators import traced_openai_realtime, traced_stt
@@ -657,10 +656,17 @@ class OpenAIRealtimeLLMService(LLMService):
async def _handle_evt_response_done(self, evt): async def _handle_evt_response_done(self, evt):
# todo: figure out whether there's anything we need to do for "cancelled" events # todo: figure out whether there's anything we need to do for "cancelled" events
# usage metrics # usage metrics
cached_tokens = (
evt.response.usage.input_token_details.cached_tokens
if hasattr(evt.response.usage, "input_token_details")
and evt.response.usage.input_token_details
else None
)
tokens = LLMTokenUsage( tokens = LLMTokenUsage(
prompt_tokens=evt.response.usage.input_tokens, prompt_tokens=evt.response.usage.input_tokens,
completion_tokens=evt.response.usage.output_tokens, completion_tokens=evt.response.usage.output_tokens,
total_tokens=evt.response.usage.total_tokens, total_tokens=evt.response.usage.total_tokens,
cache_read_input_tokens=cached_tokens,
) )
await self.start_llm_usage_metrics(tokens) await self.start_llm_usage_metrics(tokens)
await self.stop_processing_metrics() await self.stop_processing_metrics()
@@ -810,7 +816,7 @@ class OpenAIRealtimeLLMService(LLMService):
# We're done configuring the LLM for this session # We're done configuring the LLM for this session
self._llm_needs_conversation_setup = False self._llm_needs_conversation_setup = False
logger.debug(f"Creating response") logger.debug("Creating response")
await self.push_frame(LLMFullResponseStartFrame()) await self.push_frame(LLMFullResponseStartFrame())
await self.start_processing_metrics() await self.start_processing_metrics()

View File

@@ -92,6 +92,24 @@ def _add_token_usage_to_span(span, token_usage):
span.set_attribute("gen_ai.usage.input_tokens", token_usage["prompt_tokens"]) span.set_attribute("gen_ai.usage.input_tokens", token_usage["prompt_tokens"])
if "completion_tokens" in token_usage: if "completion_tokens" in token_usage:
span.set_attribute("gen_ai.usage.output_tokens", token_usage["completion_tokens"]) span.set_attribute("gen_ai.usage.output_tokens", token_usage["completion_tokens"])
# Add cached token metrics for dictionary
if (
"cache_read_input_tokens" in 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"]
)
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",
token_usage["cache_creation_input_tokens"],
)
if "reasoning_tokens" in token_usage and token_usage["reasoning_tokens"] is not None:
span.set_attribute("gen_ai.usage.reasoning_tokens", token_usage["reasoning_tokens"])
else: else:
# Handle LLMTokenUsage object # Handle LLMTokenUsage object
span.set_attribute("gen_ai.usage.input_tokens", getattr(token_usage, "prompt_tokens", 0)) span.set_attribute("gen_ai.usage.input_tokens", getattr(token_usage, "prompt_tokens", 0))
@@ -99,6 +117,19 @@ def _add_token_usage_to_span(span, token_usage):
"gen_ai.usage.output_tokens", getattr(token_usage, "completion_tokens", 0) "gen_ai.usage.output_tokens", getattr(token_usage, "completion_tokens", 0)
) )
# 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)
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)
reasoning_tokens = getattr(token_usage, "reasoning_tokens", None)
if reasoning_tokens is not None:
span.set_attribute("gen_ai.usage.reasoning_tokens", reasoning_tokens)
def traced_tts(func: Optional[Callable] = None, *, name: Optional[str] = None) -> Callable: def traced_tts(func: Optional[Callable] = None, *, name: Optional[str] = None) -> Callable:
"""Trace TTS service methods with TTS-specific attributes. """Trace TTS service methods with TTS-specific attributes.
@@ -715,7 +746,7 @@ def traced_gemini_live(operation: str) -> Callable:
else: else:
operation_attrs["tool.result_status"] = "completed" operation_attrs["tool.result_status"] = "completed"
except json.JSONDecodeError as e: except json.JSONDecodeError:
operation_attrs["tool.result"] = ( operation_attrs["tool.result"] = (
f"Invalid JSON: {str(result_content)[:500]}" f"Invalid JSON: {str(result_content)[:500]}"
) )