Cleanup on aisle METRICS. Note: See below, this is a breaking change

1. Fleshed out MetricsFrames and broke it into a proper set of types
2. Add model_name as a property to the AIService so that it can be
   automatically included in metrics and also remove that
   overhead from all the various services themselves

Breaking change!

Because of the types improvements, the MetricsFrame type has
changed. Each frame will have a list of metrics simlilar to before
except each item in the list will only contain one type of metric:
"ttfb", "tokens", "characters", or "processing". Previously these
fields would be in every entry but set to None if they didn't apply.

While this changes internal handling of the MetricsFrame, it does NOT
break the RTVI/daily messaging of metrics. That format remains the same.

Also. Remember to use model_name for accessing a service's current
model and set_model_name for setting it.
This commit is contained in:
mattie ruth backman
2024-09-17 14:49:08 -04:00
parent ed409d0460
commit a4edb3dab1
21 changed files with 190 additions and 98 deletions

View File

@@ -10,6 +10,7 @@ import os
import sys import sys
from pipecat.frames.frames import Frame, LLMMessagesFrame, MetricsFrame from pipecat.frames.frames import Frame, LLMMessagesFrame, MetricsFrame
from pipecat.metrics.metrics import TTFBMetricsData, ProcessingMetricsData, LLMUsageMetricsData, TTSUsageMetricsData
from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask from pipecat.pipeline.task import PipelineParams, PipelineTask
@@ -37,8 +38,19 @@ logger.add(sys.stderr, level="DEBUG")
class MetricsLogger(FrameProcessor): class MetricsLogger(FrameProcessor):
async def process_frame(self, frame: Frame, direction: FrameDirection): async def process_frame(self, frame: Frame, direction: FrameDirection):
if isinstance(frame, MetricsFrame): if isinstance(frame, MetricsFrame):
print( for d in frame.data:
f"!!! MetricsFrame: {frame}, ttfb: {frame.ttfb}, processing: {frame.processing}, tokens: {frame.tokens}, characters: {frame.characters}") if isinstance(d, TTFBMetricsData):
print(f"!!! MetricsFrame: {frame}, ttfb: {d.value}")
elif isinstance(d, ProcessingMetricsData):
print(f"!!! MetricsFrame: {frame}, processing: {d.value}")
elif isinstance(d, LLMUsageMetricsData):
tokens = d.value
print(
f"!!! MetricsFrame: {frame}, tokens: {
tokens.prompt_tokens}, characters: {
tokens.completion_tokens}")
elif isinstance(d, TTSUsageMetricsData):
print(f"!!! MetricsFrame: {frame}, characters: {d.value}")
await self.push_frame(frame, direction) await self.push_frame(frame, direction)

View File

@@ -4,11 +4,12 @@
# SPDX-License-Identifier: BSD 2-Clause License # SPDX-License-Identifier: BSD 2-Clause License
# #
from typing import Any, List, Mapping, Optional, Tuple from typing import Any, List, Optional, Tuple
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pipecat.clocks.base_clock import BaseClock from pipecat.clocks.base_clock import BaseClock
from pipecat.metrics.metrics import MetricsData
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.time import nanoseconds_to_str from pipecat.utils.time import nanoseconds_to_str
from pipecat.utils.utils import obj_count, obj_id from pipecat.utils.utils import obj_count, obj_id
@@ -333,10 +334,8 @@ class BotInterruptionFrame(SystemFrame):
class MetricsFrame(SystemFrame): class MetricsFrame(SystemFrame):
"""Emitted by processor that can compute metrics like latencies. """Emitted by processor that can compute metrics like latencies.
""" """
ttfb: List[Mapping[str, Any]] | None = None data: List[MetricsData]
processing: List[Mapping[str, Any]] | None = None
tokens: List[Mapping[str, Any]] | None = None
characters: List[Mapping[str, Any]] | None = None
# #
# Control frames # Control frames

View File

View File

@@ -0,0 +1,31 @@
from typing import Optional
from pydantic import BaseModel
class MetricsData(BaseModel):
processor: str
model: Optional[str] = None
class TTFBMetricsData(MetricsData):
value: float
class ProcessingMetricsData(MetricsData):
value: float
class LLMTokenUsage(BaseModel):
prompt_tokens: int
completion_tokens: int
total_tokens: int
cache_read_input_tokens: Optional[int] = None
cache_creation_input_tokens: Optional[int] = None
class LLMUsageMetricsData(MetricsData):
value: LLMTokenUsage
class TTSUsageMetricsData(MetricsData):
value: int

View File

@@ -20,6 +20,7 @@ from pipecat.frames.frames import (
MetricsFrame, MetricsFrame,
StartFrame, StartFrame,
StopTaskFrame) StopTaskFrame)
from pipecat.metrics.metrics import TTFBMetricsData, ProcessingMetricsData
from pipecat.pipeline.base_pipeline import BasePipeline from pipecat.pipeline.base_pipeline import BasePipeline
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.utils.utils import obj_count, obj_id from pipecat.utils.utils import obj_count, obj_id
@@ -118,9 +119,11 @@ class PipelineTask:
def _initial_metrics_frame(self) -> MetricsFrame: def _initial_metrics_frame(self) -> MetricsFrame:
processors = self._pipeline.processors_with_metrics() processors = self._pipeline.processors_with_metrics()
ttfb = [{"processor": p.name, "value": 0.0} for p in processors] data = []
processing = [{"processor": p.name, "value": 0.0} for p in processors] for p in processors:
return MetricsFrame(ttfb=ttfb, processing=processing) data.append(TTFBMetricsData(processor=p.name, value=0.0))
data.append(ProcessingMetricsData(processor=p.name, value=0.0))
return MetricsFrame(data=data)
async def _process_down_queue(self): async def _process_down_queue(self):
self._clock.start() self._clock.start()

View File

@@ -19,6 +19,13 @@ from pipecat.frames.frames import (
StartInterruptionFrame, StartInterruptionFrame,
StopInterruptionFrame, StopInterruptionFrame,
SystemFrame) SystemFrame)
from pipecat.metrics.metrics import (
LLMTokenUsage,
LLMUsageMetricsData,
MetricsData,
ProcessingMetricsData,
TTFBMetricsData,
TTSUsageMetricsData)
from pipecat.utils.utils import obj_count, obj_id from pipecat.utils.utils import obj_count, obj_id
from loguru import logger from loguru import logger
@@ -31,11 +38,20 @@ class FrameDirection(Enum):
class FrameProcessorMetrics: class FrameProcessorMetrics:
def __init__(self, name: str): def __init__(self, name: str):
self._name = name self._core_metrics_data = MetricsData(processor=name)
self._start_ttfb_time = 0 self._start_ttfb_time = 0
self._start_processing_time = 0 self._start_processing_time = 0
self._should_report_ttfb = True self._should_report_ttfb = True
def _processor_name(self):
return self._core_metrics_data.processor
def _model_name(self):
return self._core_metrics_data.model
def set_core_metrics_data(self, data: MetricsData):
self._core_metrics_data = data
async def start_ttfb_metrics(self, report_only_initial_ttfb): async def start_ttfb_metrics(self, report_only_initial_ttfb):
if self._should_report_ttfb: if self._should_report_ttfb:
self._start_ttfb_time = time.time() self._start_ttfb_time = time.time()
@@ -46,13 +62,13 @@ class FrameProcessorMetrics:
return None return None
value = time.time() - self._start_ttfb_time value = time.time() - self._start_ttfb_time
logger.debug(f"{self._name} TTFB: {value}") logger.debug(f"{self._processor_name()} TTFB: {value}")
ttfb = { ttfb = TTFBMetricsData(
"processor": self._name, processor=self._processor_name(),
"value": value value=value,
} model=self._model_name())
self._start_ttfb_time = 0 self._start_ttfb_time = 0
return MetricsFrame(ttfb=[ttfb]) return MetricsFrame(data=[ttfb])
async def start_processing_metrics(self): async def start_processing_metrics(self):
self._start_processing_time = time.time() self._start_processing_time = time.time()
@@ -62,26 +78,28 @@ class FrameProcessorMetrics:
return None return None
value = time.time() - self._start_processing_time value = time.time() - self._start_processing_time
logger.debug(f"{self._name} processing time: {value}") logger.debug(f"{self._processor_name()} processing time: {value}")
processing = { processing = ProcessingMetricsData(
"processor": self._name, processor=self._processor_name(), value=value, model=self._model_name())
"value": value
}
self._start_processing_time = 0 self._start_processing_time = 0
return MetricsFrame(processing=[processing]) return MetricsFrame(data=[processing])
async def start_llm_usage_metrics(self, tokens: dict): async def start_llm_usage_metrics(self, tokens: LLMTokenUsage):
logger.debug( logger.debug(
f"{self._name} prompt tokens: {tokens['prompt_tokens']}, completion tokens: {tokens['completion_tokens']}") f"{self._processor_name()} prompt tokens: {tokens.prompt_tokens}, completion tokens: {tokens.completion_tokens}")
return MetricsFrame(tokens=[tokens]) value = LLMUsageMetricsData(
processor=self._processor_name(),
model=self._model_name(),
value=tokens)
return MetricsFrame(data=[value])
async def start_tts_usage_metrics(self, text: str): async def start_tts_usage_metrics(self, text: str):
characters = { characters = TTSUsageMetricsData(
"processor": self._name, processor=self._processor_name(),
"value": len(text), model=self._model_name(),
} value=len(text))
logger.debug(f"{self._name} usage characters: {characters['value']}") logger.debug(f"{self._processor_name()} usage characters: {characters.value}")
return MetricsFrame(characters=[characters]) return MetricsFrame(data=[characters])
class FrameProcessor: class FrameProcessor:
@@ -140,6 +158,9 @@ class FrameProcessor:
def can_generate_metrics(self) -> bool: def can_generate_metrics(self) -> bool:
return False return False
def set_core_metrics_data(self, data: MetricsData):
self._metrics.set_core_metrics_data(data)
async def start_ttfb_metrics(self): async def start_ttfb_metrics(self):
if self.can_generate_metrics() and self.metrics_enabled: if self.can_generate_metrics() and self.metrics_enabled:
await self._metrics.start_ttfb_metrics(self._report_only_initial_ttfb) await self._metrics.start_ttfb_metrics(self._report_only_initial_ttfb)
@@ -160,7 +181,7 @@ class FrameProcessor:
if frame: if frame:
await self.push_frame(frame) await self.push_frame(frame)
async def start_llm_usage_metrics(self, tokens: dict): async def start_llm_usage_metrics(self, tokens: LLMTokenUsage):
if self.can_generate_metrics() and self.usage_metrics_enabled: if self.can_generate_metrics() and self.usage_metrics_enabled:
frame = await self._metrics.start_llm_usage_metrics(tokens) frame = await self._metrics.start_llm_usage_metrics(tokens)
if frame: if frame:

View File

@@ -32,6 +32,7 @@ from pipecat.frames.frames import (
UserImageRequestFrame, UserImageRequestFrame,
VisionImageRawFrame VisionImageRawFrame
) )
from pipecat.metrics.metrics import MetricsData
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.audio import calculate_audio_volume from pipecat.utils.audio import calculate_audio_volume
@@ -46,6 +47,15 @@ from loguru import logger
class AIService(FrameProcessor): class AIService(FrameProcessor):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._model_name: str = ""
@property
def model_name(self) -> str:
return self._model_name
def set_model_name(self, model: str):
self._model_name = model
self.set_core_metrics_data(MetricsData(processor=self.name, model=self._model_name))
async def start(self, frame: StartFrame): async def start(self, frame: StartFrame):
pass pass
@@ -158,7 +168,7 @@ class TTSService(AIService):
@abstractmethod @abstractmethod
async def set_model(self, model: str): async def set_model(self, model: str):
pass self.set_model_name(model)
@abstractmethod @abstractmethod
async def set_voice(self, voice: str): async def set_voice(self, voice: str):
@@ -367,7 +377,7 @@ class STTService(AIService):
@abstractmethod @abstractmethod
async def set_model(self, model: str): async def set_model(self, model: str):
pass self.set_model_name(model)
@abstractmethod @abstractmethod
async def set_language(self, language: Language): async def set_language(self, language: Language):

View File

@@ -29,6 +29,7 @@ from pipecat.frames.frames import (
FunctionCallInProgressFrame, FunctionCallInProgressFrame,
StartInterruptionFrame StartInterruptionFrame
) )
from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.ai_services import LLMService from pipecat.services.ai_services import LLMService
from pipecat.processors.aggregators.openai_llm_context import ( from pipecat.processors.aggregators.openai_llm_context import (
@@ -84,7 +85,7 @@ class AnthropicLLMService(LLMService):
**kwargs): **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._client = AsyncAnthropic(api_key=api_key) self._client = AsyncAnthropic(api_key=api_key)
self._model = model self.set_model_name(model)
self._max_tokens = max_tokens self._max_tokens = max_tokens
self._enable_prompt_caching_beta = enable_prompt_caching_beta self._enable_prompt_caching_beta = enable_prompt_caching_beta
@@ -137,7 +138,7 @@ class AnthropicLLMService(LLMService):
tools=context.tools or [], tools=context.tools or [],
system=context.system, system=context.system,
messages=messages, messages=messages,
model=self._model, model=self.model_name,
max_tokens=self._max_tokens, max_tokens=self._max_tokens,
stream=True) stream=True)
@@ -231,7 +232,7 @@ class AnthropicLLMService(LLMService):
context = AnthropicLLMContext.from_image_frame(frame) context = AnthropicLLMContext.from_image_frame(frame)
elif isinstance(frame, LLMModelUpdateFrame): elif isinstance(frame, LLMModelUpdateFrame):
logger.debug(f"Switching LLM model to: [{frame.model}]") logger.debug(f"Switching LLM model to: [{frame.model}]")
self._model = frame.model self.set_model_name(frame.model)
elif isinstance(frame, LLMEnablePromptCachingFrame): elif isinstance(frame, LLMEnablePromptCachingFrame):
logger.debug(f"Setting enable prompt caching to: [{frame.enable}]") logger.debug(f"Setting enable prompt caching to: [{frame.enable}]")
self._enable_prompt_caching_beta = frame.enable self._enable_prompt_caching_beta = frame.enable
@@ -251,15 +252,13 @@ class AnthropicLLMService(LLMService):
cache_creation_input_tokens: int, cache_creation_input_tokens: int,
cache_read_input_tokens: int): cache_read_input_tokens: int):
if prompt_tokens or completion_tokens or cache_creation_input_tokens or cache_read_input_tokens: if prompt_tokens or completion_tokens or cache_creation_input_tokens or cache_read_input_tokens:
tokens = { tokens = LLMTokenUsage(
"processor": self.name, prompt_tokens=prompt_tokens,
"model": self._model, completion_tokens=completion_tokens,
"prompt_tokens": prompt_tokens, cache_creation_input_tokens=cache_creation_input_tokens,
"completion_tokens": completion_tokens, cache_read_input_tokens=cache_read_input_tokens,
"cache_creation_input_tokens": cache_creation_input_tokens, total_tokens=prompt_tokens + completion_tokens
"cache_read_input_tokens": cache_read_input_tokens, )
"total_tokens": prompt_tokens + completion_tokens
}
await self.start_llm_usage_metrics(tokens) await self.start_llm_usage_metrics(tokens)

View File

@@ -22,6 +22,8 @@ from pipecat.frames.frames import (
TTSStoppedFrame, TTSStoppedFrame,
TranscriptionFrame, TranscriptionFrame,
URLImageRawFrame) URLImageRawFrame)
from pipecat.metrics.metrics import TTSUsageMetricsData
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.ai_services import STTService, TTSService, ImageGenService from pipecat.services.ai_services import STTService, TTSService, ImageGenService
from pipecat.services.openai import BaseOpenAILLMService from pipecat.services.openai import BaseOpenAILLMService
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
@@ -190,7 +192,7 @@ class AzureImageGenServiceREST(ImageGenService):
self._api_key = api_key self._api_key = api_key
self._azure_endpoint = endpoint self._azure_endpoint = endpoint
self._api_version = api_version self._api_version = api_version
self._model = model self.set_model_name(model)
self._image_size = image_size self._image_size = image_size
self._aiohttp_session = aiohttp_session self._aiohttp_session = aiohttp_session

View File

@@ -89,7 +89,7 @@ class CartesiaTTSService(AsyncWordTTSService):
self._cartesia_version = cartesia_version self._cartesia_version = cartesia_version
self._url = url self._url = url
self._voice_id = voice_id self._voice_id = voice_id
self._model_id = model_id self.set_model_name(model_id)
self._output_format = { self._output_format = {
"container": "raw", "container": "raw",
"encoding": encoding, "encoding": encoding,
@@ -105,8 +105,8 @@ class CartesiaTTSService(AsyncWordTTSService):
return True return True
async def set_model(self, model: str): async def set_model(self, model: str):
await super().set_model(model)
logger.debug(f"Switching TTS model to: [{model}]") logger.debug(f"Switching TTS model to: [{model}]")
self._model_id = model
async def set_voice(self, voice: str): async def set_voice(self, voice: str):
logger.debug(f"Switching TTS voice to: [{voice}]") logger.debug(f"Switching TTS voice to: [{voice}]")
@@ -155,6 +155,11 @@ class CartesiaTTSService(AsyncWordTTSService):
except Exception as e: except Exception as e:
logger.error(f"{self} error closing websocket: {e}") logger.error(f"{self} error closing websocket: {e}")
def _get_websocket(self):
if self._websocket:
return self._websocket
raise Exception("Websocket not connected")
async def _handle_interruption(self, frame: StartInterruptionFrame, direction: FrameDirection): async def _handle_interruption(self, frame: StartInterruptionFrame, direction: FrameDirection):
await super()._handle_interruption(frame, direction) await super()._handle_interruption(frame, direction)
await self.stop_all_metrics() await self.stop_all_metrics()
@@ -169,7 +174,7 @@ class CartesiaTTSService(AsyncWordTTSService):
"transcript": "", "transcript": "",
"continue": False, "continue": False,
"context_id": self._context_id, "context_id": self._context_id,
"model_id": self._model_id, "model_id": self.model_name,
"voice": { "voice": {
"mode": "id", "mode": "id",
"id": self._voice_id "id": self._voice_id
@@ -182,7 +187,7 @@ class CartesiaTTSService(AsyncWordTTSService):
async def _receive_task_handler(self): async def _receive_task_handler(self):
try: try:
async for message in self._websocket: async for message in self._get_websocket():
msg = json.loads(message) msg = json.loads(message)
if not msg or msg["context_id"] != self._context_id: if not msg or msg["context_id"] != self._context_id:
continue continue
@@ -235,7 +240,7 @@ class CartesiaTTSService(AsyncWordTTSService):
"transcript": text + " ", "transcript": text + " ",
"continue": True, "continue": True,
"context_id": self._context_id, "context_id": self._context_id,
"model_id": self._model_id, "model_id": self.model_name,
"voice": { "voice": {
"mode": "id", "mode": "id",
"id": self._voice_id "id": self._voice_id
@@ -245,7 +250,7 @@ class CartesiaTTSService(AsyncWordTTSService):
"add_timestamps": True, "add_timestamps": True,
} }
try: try:
await self._websocket.send(json.dumps(msg)) await self._get_websocket().send(json.dumps(msg))
await self.start_tts_usage_metrics(text) await self.start_tts_usage_metrics(text)
except Exception as e: except Exception as e:
logger.error(f"{self} error sending message: {e}") logger.error(f"{self} error sending message: {e}")

View File

@@ -135,6 +135,7 @@ class DeepgramSTTService(STTService):
self._connection.on(LiveTranscriptionEvents.Transcript, self._on_message) self._connection.on(LiveTranscriptionEvents.Transcript, self._on_message)
async def set_model(self, model: str): async def set_model(self, model: str):
await super().set_model(model)
logger.debug(f"Switching STT model to: [{model}]") logger.debug(f"Switching STT model to: [{model}]")
self._live_options.model = model self._live_options.model = model
await self._disconnect() await self._disconnect()

View File

@@ -107,7 +107,7 @@ class ElevenLabsTTSService(AsyncWordTTSService):
self._api_key = api_key self._api_key = api_key
self._voice_id = voice_id self._voice_id = voice_id
self._model = model self.set_model_name(model)
self._url = url self._url = url
self._params = params self._params = params
@@ -122,8 +122,8 @@ class ElevenLabsTTSService(AsyncWordTTSService):
return True return True
async def set_model(self, model: str): async def set_model(self, model: str):
await super().set_model(model)
logger.debug(f"Switching TTS model to: [{model}]") logger.debug(f"Switching TTS model to: [{model}]")
self._model = model
await self._disconnect() await self._disconnect()
await self._connect() await self._connect()
@@ -160,7 +160,7 @@ class ElevenLabsTTSService(AsyncWordTTSService):
async def _connect(self): async def _connect(self):
try: try:
voice_id = self._voice_id voice_id = self._voice_id
model = self._model model = self.model_name
output_format = self._params.output_format output_format = self._params.output_format
url = f"{self._url}/v1/text-to-speech/{voice_id}/stream-input?model_id={model}&output_format={output_format}" url = f"{self._url}/v1/text-to-speech/{voice_id}/stream-input?model_id={model}&output_format={output_format}"
self._websocket = await websockets.connect(url) self._websocket = await websockets.connect(url)

View File

@@ -46,7 +46,7 @@ class FalImageGenService(ImageGenService):
**kwargs **kwargs
): ):
super().__init__(**kwargs) super().__init__(**kwargs)
self._model = model self.set_model_name(model)
self._params = params self._params = params
self._aiohttp_session = aiohttp_session self._aiohttp_session = aiohttp_session
if key: if key:
@@ -56,7 +56,7 @@ class FalImageGenService(ImageGenService):
logger.debug(f"Generating image from prompt: {prompt}") logger.debug(f"Generating image from prompt: {prompt}")
response = await fal_client.run_async( response = await fal_client.run_async(
self._model, self.model_name,
arguments={"prompt": prompt, **self._params.model_dump(exclude_none=True)} arguments={"prompt": prompt, **self._params.model_dump(exclude_none=True)}
) )

View File

@@ -22,4 +22,4 @@ class FireworksLLMService(BaseOpenAILLMService):
*, *,
model: str = "accounts/fireworks/models/firefunction-v1", model: str = "accounts/fireworks/models/firefunction-v1",
base_url: str = "https://api.fireworks.ai/inference/v1"): base_url: str = "https://api.fireworks.ai/inference/v1"):
super().__init__(model, base_url) super().__init__(model=model, base_url=base_url)

View File

@@ -50,6 +50,7 @@ class GoogleLLMService(LLMService):
return True return True
def _create_client(self, model: str): def _create_client(self, model: str):
self.set_model_name(model)
self._client = gai.GenerativeModel(model) self._client = gai.GenerativeModel(model)
def _get_messages_from_openai_context( def _get_messages_from_openai_context(

View File

@@ -54,6 +54,8 @@ class MoondreamService(VisionService):
): ):
super().__init__(**kwargs) super().__init__(**kwargs)
self.set_model_name(model)
if not use_cpu: if not use_cpu:
device, dtype = detect_device() device, dtype = detect_device()
else: else:
@@ -73,7 +75,7 @@ class MoondreamService(VisionService):
async def run_vision(self, frame: VisionImageRawFrame) -> AsyncGenerator[Frame, None]: async def run_vision(self, frame: VisionImageRawFrame) -> AsyncGenerator[Frame, None]:
if not self._model: if not self._model:
logger.error(f"{self} error: Moondream model not available") logger.error(f"{self} error: Moondream model not available ({self.model_name})")
yield ErrorFrame("Moondream model not available") yield ErrorFrame("Moondream model not available")
return return

View File

@@ -33,6 +33,7 @@ from pipecat.frames.frames import (
FunctionCallInProgressFrame, FunctionCallInProgressFrame,
StartInterruptionFrame StartInterruptionFrame
) )
from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.aggregators.llm_response import LLMUserContextAggregator, LLMAssistantContextAggregator from pipecat.processors.aggregators.llm_response import LLMUserContextAggregator, LLMAssistantContextAggregator
from pipecat.processors.aggregators.openai_llm_context import ( from pipecat.processors.aggregators.openai_llm_context import (
@@ -83,7 +84,7 @@ class BaseOpenAILLMService(LLMService):
def __init__(self, *, model: str, api_key=None, base_url=None, **kwargs): def __init__(self, *, model: str, api_key=None, base_url=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._model: str = model self.set_model_name(model)
self._client = self.create_client(api_key=api_key, base_url=base_url, **kwargs) self._client = self.create_client(api_key=api_key, base_url=base_url, **kwargs)
def create_client(self, api_key=None, base_url=None, **kwargs): def create_client(self, api_key=None, base_url=None, **kwargs):
@@ -104,7 +105,7 @@ class BaseOpenAILLMService(LLMService):
context: OpenAILLMContext, context: OpenAILLMContext,
messages: List[ChatCompletionMessageParam]) -> AsyncStream[ChatCompletionChunk]: messages: List[ChatCompletionMessageParam]) -> AsyncStream[ChatCompletionChunk]:
chunks = await self._client.chat.completions.create( chunks = await self._client.chat.completions.create(
model=self._model, model=self.model_name,
stream=True, stream=True,
messages=messages, messages=messages,
tools=context.tools, tools=context.tools,
@@ -148,13 +149,11 @@ class BaseOpenAILLMService(LLMService):
async for chunk in chunk_stream: async for chunk in chunk_stream:
if chunk.usage: if chunk.usage:
tokens = { tokens = LLMTokenUsage(
"processor": self.name, prompt_tokens=chunk.usage.prompt_tokens,
"model": self._model, completion_tokens=chunk.usage.completion_tokens,
"prompt_tokens": chunk.usage.prompt_tokens, total_tokens=chunk.usage.total_tokens
"completion_tokens": chunk.usage.completion_tokens, )
"total_tokens": chunk.usage.total_tokens
}
await self.start_llm_usage_metrics(tokens) await self.start_llm_usage_metrics(tokens)
if len(chunk.choices) == 0: if len(chunk.choices) == 0:
@@ -223,7 +222,7 @@ class BaseOpenAILLMService(LLMService):
context = OpenAILLMContext.from_image_frame(frame) context = OpenAILLMContext.from_image_frame(frame)
elif isinstance(frame, LLMModelUpdateFrame): elif isinstance(frame, LLMModelUpdateFrame):
logger.debug(f"Switching LLM model to: [{frame.model}]") logger.debug(f"Switching LLM model to: [{frame.model}]")
self._model = frame.model self.set_model_name(frame.model)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
@@ -273,7 +272,7 @@ class OpenAIImageGenService(ImageGenService):
model: str = "dall-e-3", model: str = "dall-e-3",
): ):
super().__init__() super().__init__()
self._model = model self.set_model_name(model)
self._image_size = image_size self._image_size = image_size
self._client = AsyncOpenAI(api_key=api_key) self._client = AsyncOpenAI(api_key=api_key)
self._aiohttp_session = aiohttp_session self._aiohttp_session = aiohttp_session
@@ -283,7 +282,7 @@ class OpenAIImageGenService(ImageGenService):
image = await self._client.images.generate( image = await self._client.images.generate(
prompt=prompt, prompt=prompt,
model=self._model, model=self.model_name,
n=1, n=1,
size=self._image_size size=self._image_size
) )
@@ -325,7 +324,7 @@ class OpenAITTSService(TTSService):
super().__init__(sample_rate=sample_rate, **kwargs) super().__init__(sample_rate=sample_rate, **kwargs)
self._voice: ValidVoice = VALID_VOICES.get(voice, "alloy") self._voice: ValidVoice = VALID_VOICES.get(voice, "alloy")
self._model = model self.set_model_name(model)
self._sample_rate = sample_rate self._sample_rate = sample_rate
self._client = AsyncOpenAI(api_key=api_key) self._client = AsyncOpenAI(api_key=api_key)
@@ -348,7 +347,7 @@ class OpenAITTSService(TTSService):
async with self._client.audio.speech.with_streaming_response.create( async with self._client.audio.speech.with_streaming_response.create(
input=text, input=text,
model=self._model, model=self.model_name,
voice=self._voice, voice=self._voice,
response_format="pcm", response_format="pcm",
) as r: ) as r:

View File

@@ -60,7 +60,7 @@ class OpenPipeLLMService(BaseOpenAILLMService):
context: OpenAILLMContext, context: OpenAILLMContext,
messages: List[ChatCompletionMessageParam]) -> AsyncStream[ChatCompletionChunk]: messages: List[ChatCompletionMessageParam]) -> AsyncStream[ChatCompletionChunk]:
chunks = await self._client.chat.completions.create( chunks = await self._client.chat.completions.create(
model=self._model, model=self.model_name,
stream=True, stream=True,
messages=messages, messages=messages,
openpipe={ openpipe={

View File

@@ -18,9 +18,7 @@ from pipecat.frames.frames import (
Frame, Frame,
LLMModelUpdateFrame, LLMModelUpdateFrame,
TextFrame, TextFrame,
VisionImageRawFrame,
UserImageRequestFrame, UserImageRequestFrame,
UserImageRawFrame,
LLMMessagesFrame, LLMMessagesFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
@@ -28,6 +26,7 @@ from pipecat.frames.frames import (
FunctionCallInProgressFrame, FunctionCallInProgressFrame,
StartInterruptionFrame StartInterruptionFrame
) )
from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.ai_services import LLMService from pipecat.services.ai_services import LLMService
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext, OpenAILLMContextFrame from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext, OpenAILLMContextFrame
@@ -69,7 +68,7 @@ class TogetherLLMService(LLMService):
**kwargs): **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self._client = AsyncTogether(api_key=api_key) self._client = AsyncTogether(api_key=api_key)
self._model = model self.set_model_name(model)
self._max_tokens = max_tokens self._max_tokens = max_tokens
def can_generate_metrics(self) -> bool: def can_generate_metrics(self) -> bool:
@@ -95,7 +94,7 @@ class TogetherLLMService(LLMService):
stream = await self._client.chat.completions.create( stream = await self._client.chat.completions.create(
messages=context.messages, messages=context.messages,
model=self._model, model=self.model_name,
max_tokens=self._max_tokens, max_tokens=self._max_tokens,
stream=True, stream=True,
) )
@@ -108,13 +107,11 @@ class TogetherLLMService(LLMService):
async for chunk in stream: async for chunk in stream:
# logger.debug(f"Together LLM event: {chunk}") # logger.debug(f"Together LLM event: {chunk}")
if chunk.usage: if chunk.usage:
tokens = { tokens = LLMTokenUsage(
"processor": self.name, prompt_tokens=chunk.usage.prompt_tokens,
"model": self._model, completion_tokens=chunk.usage.completion_tokens,
"prompt_tokens": chunk.usage.prompt_tokens, total_tokens=chunk.usage.total_tokens
"completion_tokens": chunk.usage.completion_tokens, )
"total_tokens": chunk.usage.total_tokens
}
await self.start_llm_usage_metrics(tokens) await self.start_llm_usage_metrics(tokens)
if len(chunk.choices) == 0: if len(chunk.choices) == 0:
@@ -156,7 +153,7 @@ class TogetherLLMService(LLMService):
context = TogetherLLMContext.from_messages(frame.messages) context = TogetherLLMContext.from_messages(frame.messages)
elif isinstance(frame, LLMModelUpdateFrame): elif isinstance(frame, LLMModelUpdateFrame):
logger.debug(f"Switching LLM model to: [{frame.model}]") logger.debug(f"Switching LLM model to: [{frame.model}]")
self._model = frame.model self.set_model_name(frame.model)
else: else:
await self.push_frame(frame, direction) await self.push_frame(frame, direction)

View File

@@ -52,7 +52,7 @@ class WhisperSTTService(SegmentedSTTService):
super().__init__(**kwargs) super().__init__(**kwargs)
self._device: str = device self._device: str = device
self._compute_type = compute_type self._compute_type = compute_type
self._model_name: str | Model = model self.set_model_name(model if isinstance(model, str) else model.value)
self._no_speech_prob = no_speech_prob self._no_speech_prob = no_speech_prob
self._model: WhisperModel | None = None self._model: WhisperModel | None = None
self._load() self._load()
@@ -65,7 +65,7 @@ class WhisperSTTService(SegmentedSTTService):
this model is being run, it will take time to download.""" this model is being run, it will take time to download."""
logger.debug("Loading Whisper model...") logger.debug("Loading Whisper model...")
self._model = WhisperModel( self._model = WhisperModel(
self._model_name.value if isinstance(self._model_name, Enum) else self._model_name, self.model_name,
device=self._device, device=self._device,
compute_type=self._compute_type) compute_type=self._compute_type)
logger.debug("Loaded Whisper model") logger.debug("Loaded Whisper model")

View File

@@ -35,6 +35,7 @@ from pipecat.frames.frames import (
TransportMessageFrame, TransportMessageFrame,
UserImageRawFrame, UserImageRawFrame,
UserImageRequestFrame) UserImageRequestFrame)
from pipecat.metrics.metrics import LLMUsageMetricsData, ProcessingMetricsData, TTFBMetricsData, TTSUsageMetricsData
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.transports.base_input import BaseInputTransport from pipecat.transports.base_input import BaseInputTransport
@@ -731,14 +732,23 @@ class DailyOutputTransport(BaseOutputTransport):
async def send_metrics(self, frame: MetricsFrame): async def send_metrics(self, frame: MetricsFrame):
metrics = {} metrics = {}
if frame.ttfb: for d in frame.data:
metrics["ttfb"] = frame.ttfb if isinstance(d, TTFBMetricsData):
if frame.processing: if "ttfb" not in metrics:
metrics["processing"] = frame.processing metrics["ttfb"] = []
if frame.tokens: metrics["ttfb"].append(d.model_dump())
metrics["tokens"] = frame.tokens elif isinstance(d, ProcessingMetricsData):
if frame.characters: if "processing" not in metrics:
metrics["characters"] = frame.characters metrics["processing"] = []
metrics["processing"].append(d.model_dump())
elif isinstance(d, LLMUsageMetricsData):
if "tokens" not in metrics:
metrics["tokens"] = []
metrics["tokens"].append(d.value.model_dump(exclude_none=True))
elif isinstance(d, TTSUsageMetricsData):
if "characters" not in metrics:
metrics["characters"] = []
metrics["characters"].append(d.model_dump())
message = DailyTransportMessageFrame(message={ message = DailyTransportMessageFrame(message={
"type": "pipecat-metrics", "type": "pipecat-metrics",