Merge branch 'pipecat-ai:main' into telemetry-fix-system-message

This commit is contained in:
Kinshuk Bairagi
2026-03-20 18:36:31 +05:30
committed by GitHub
447 changed files with 10044 additions and 3690 deletions

View File

@@ -0,0 +1,254 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""OpenAI Responses API adapter for Pipecat."""
import copy
from typing import Any, Dict, List, Optional, TypedDict
from loguru import logger
from openai._types import NotGiven as OpenAINotGiven
from openai.types.responses import FunctionToolParam, ResponseInputItemParam
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.processors.aggregators.llm_context import (
LLMContext,
LLMContextMessage,
LLMSpecificMessage,
NotGiven,
)
class OpenAIResponsesLLMInvocationParams(TypedDict, total=False):
"""Context-based parameters for invoking OpenAI Responses API."""
input: List[ResponseInputItemParam]
tools: List[FunctionToolParam] | OpenAINotGiven
instructions: str
class OpenAIResponsesLLMAdapter(BaseLLMAdapter[OpenAIResponsesLLMInvocationParams]):
"""OpenAI Responses API adapter for Pipecat.
Handles:
- Converting LLMContext messages to Responses API input items
- Converting Pipecat's standardized tools schema to Responses API function tool format
- Extracting and sanitizing messages from the LLM context for logging
"""
def __init__(self):
"""Initialize the adapter."""
super().__init__()
self._warned_system_instruction = False
@property
def id_for_llm_specific_messages(self) -> str:
"""Get the identifier used in LLMSpecificMessage instances."""
return "openai_responses"
def get_llm_invocation_params(
self,
context: LLMContext,
*,
system_instruction: Optional[str] = None,
) -> OpenAIResponsesLLMInvocationParams:
"""Get Responses API invocation parameters from a universal LLM context.
Args:
context: The LLM context containing messages, tools, etc.
system_instruction: Optional system instruction from service settings.
Returns:
Dictionary of parameters for the Responses API.
"""
messages = self.get_messages(context)
input_items = self._convert_messages_to_input(messages)
params: OpenAIResponsesLLMInvocationParams = {
"input": input_items,
"tools": self.from_standard_tools(context.tools),
}
if system_instruction:
# Compatibility: The Responses API requires at least one input
# message when instructions are provided. Contexts that worked with
# OpenAILLMService (system_instruction + empty messages) need the
# instructions converted to an initial developer message.
#
# NOTE: if/when we support `previous_response_id` and/or
# `conversation_id`, we'll need to revisit this logic, as it'll
# be legit to provide instructions without input items. Worth
# noting that OpenAI's docs suggest these parameters are primarily
# for development convenience rather than performance (the model
# still processes the full context), and come with the tradeoff
# of requiring OpenAI-side 30-day conversation storage, which may
# not be desirable for many users. But it could give folks an easy
# way to store/switch between conversations without needing to
# manage that storage themselves.
if not input_items:
params["input"] = [{"role": "developer", "content": system_instruction}]
else:
params["instructions"] = system_instruction
return params
def to_provider_tools_format(self, tools_schema: ToolsSchema) -> List[FunctionToolParam]:
"""Convert function schemas to Responses API function tool format.
Args:
tools_schema: The Pipecat tools schema to convert.
Returns:
List of Responses API function tool definitions.
"""
functions_schema = tools_schema.standard_tools
result = []
for func in functions_schema:
d = func.to_default_dict()
tool: FunctionToolParam = {
"type": "function",
"name": d["name"],
"parameters": d.get("parameters", {}),
"strict": d.get("strict", None),
}
if "description" in d:
tool["description"] = d["description"]
result.append(tool)
return result
def get_messages_for_logging(self, context: LLMContext) -> List[Dict[str, Any]]:
"""Get messages from context in a format ready for logging.
Removes or truncates sensitive data like image content for safe logging.
Args:
context: The LLM context containing messages.
Returns:
List of messages in a format ready for logging.
"""
msgs = []
for message in self.get_messages(context):
msg = copy.deepcopy(message)
if "content" in msg:
if isinstance(msg["content"], list):
for item in msg["content"]:
if item.get("type") == "image_url":
if item["image_url"]["url"].startswith("data:image/"):
item["image_url"]["url"] = "data:image/..."
if item.get("type") == "input_audio":
item["input_audio"]["data"] = "..."
msgs.append(msg)
return msgs
def _convert_messages_to_input(
self, messages: List[LLMContextMessage]
) -> List[ResponseInputItemParam]:
"""Convert LLMContext messages to Responses API input items.
Args:
messages: Messages from the LLMContext.
Returns:
List of Responses API input items.
"""
result: List[ResponseInputItemParam] = []
is_first = True
for message in messages:
if isinstance(message, LLMSpecificMessage):
result.append(message.message)
is_first = False
continue
role = message.get("role")
if role == "system":
if is_first and not self._warned_system_instruction:
logger.warning(
"System messages in LLMContext are converted to 'developer' role for the "
"Responses API. Consider using settings.system_instruction instead, which "
"maps to the 'instructions' parameter."
)
self._warned_system_instruction = True
content = message.get("content", "")
if isinstance(content, list):
content = self._convert_multimodal_content(content)
result.append({"role": "developer", "content": content})
elif role == "user":
content = message.get("content", "")
if isinstance(content, list):
content = self._convert_multimodal_content(content)
result.append({"role": "user", "content": content})
elif role == "assistant":
tool_calls = message.get("tool_calls")
if tool_calls:
for tc in tool_calls:
func = tc.get("function", {})
result.append(
{
"type": "function_call",
"call_id": tc.get("id", ""),
"name": func.get("name", ""),
"arguments": func.get("arguments", ""),
}
)
else:
content = message.get("content", "")
if isinstance(content, list):
content = self._convert_multimodal_content(content)
result.append({"role": "assistant", "content": content})
elif role == "tool":
content = message.get("content", "")
if not isinstance(content, str):
content = str(content)
result.append(
{
"type": "function_call_output",
"call_id": message.get("tool_call_id", ""),
"output": content,
}
)
is_first = False
return result
def _convert_multimodal_content(self, content: list) -> list:
"""Convert multimodal content parts to Responses API format.
Args:
content: List of content parts from the LLMContext message.
Returns:
List of content parts in Responses API format.
"""
result = []
for part in content:
part_type = part.get("type")
if part_type == "text":
result.append({"type": "input_text", "text": part.get("text", "")})
elif part_type == "image_url":
image_url_obj = part.get("image_url", {})
result.append(
{
"type": "input_image",
"image_url": image_url_obj.get("url", ""),
"detail": image_url_obj.get("detail", "auto"),
}
)
else:
# Pass through other types as-is. Note: "input_audio" is not
# yet supported by the Responses API (coming soon per OpenAI
# docs) but the LLMContext format already matches the expected
# shape, so it should work once support is enabled.
result.append(part)
return result

View File

@@ -0,0 +1,152 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Perplexity LLM adapter for Pipecat.
Perplexity's API uses an OpenAI-compatible interface but enforces stricter
constraints on conversation history structure:
1. **Strict role alternation** — Messages must alternate between "user"/"tool"
and "assistant" roles. Consecutive messages with the same role (e.g. two
"user" messages in a row) are rejected with:
``"messages must be an alternating sequence of user/tool and assistant messages"``
2. **No non-initial system messages** — "system" messages are only allowed at
the start of the conversation. A system message after a non-system message
causes:
``"only the initial message can have the system role"``
3. **Last message must be user/tool** — The final message in the conversation
must have role "user" or "tool". A trailing "assistant" message causes:
``"the last message must have the user or tool role"``
This adapter transforms the message list to satisfy all three constraints before
the messages are sent to Perplexity's API.
"""
import copy
from typing import List
from openai.types.chat import ChatCompletionMessageParam
from pipecat.adapters.services.open_ai_adapter import OpenAILLMAdapter, OpenAILLMInvocationParams
from pipecat.processors.aggregators.llm_context import LLMContext
class PerplexityLLMAdapter(OpenAILLMAdapter):
"""Adapter that transforms messages to satisfy Perplexity's API constraints.
Perplexity's API is stricter than OpenAI about message structure. This
adapter extends ``OpenAILLMAdapter`` and applies message transformations
to ensure compliance with Perplexity's constraints (role alternation,
no non-initial system messages, last message must be user/tool).
The transformations are applied in ``get_llm_invocation_params`` after the
parent adapter extracts messages from the LLM context, and before
``build_chat_completion_params`` prepends ``system_instruction``.
"""
def get_llm_invocation_params(self, context: LLMContext) -> OpenAILLMInvocationParams:
"""Get OpenAI-compatible invocation parameters with Perplexity message fixes applied.
Args:
context: The LLM context containing messages, tools, etc.
Returns:
Dictionary of parameters for Perplexity's ChatCompletion API, with
messages transformed to satisfy Perplexity's constraints.
"""
params = super().get_llm_invocation_params(context)
params["messages"] = self._transform_messages(list(params["messages"]))
return params
def _transform_messages(
self, messages: List[ChatCompletionMessageParam]
) -> List[ChatCompletionMessageParam]:
"""Transform messages to satisfy Perplexity's API constraints.
Applies three transformation steps in order:
1. **Convert non-initial system messages to user** — Any system message
after the initial system message block is converted to role "user",
since Perplexity rejects system messages after a non-system message.
2. **Merge consecutive same-role messages** — After the above
conversions, adjacent messages with the same role are merged using
list-of-dicts content format. This ensures strict role alternation
(e.g. a converted system→user message adjacent to an existing user
message gets merged).
3. **Remove trailing assistant messages** — If the last message is
"assistant", remove it. OpenAI appears to silently ignore trailing
assistant messages server-side, so removing them preserves equivalent
behavior while satisfying Perplexity's "last message must be
user/tool" constraint.
Note: we intentionally do *not* convert a trailing system message to
"user". That would make the transformation unstable across calls —
Perplexity appears to have statefulness/caching within a conversation,
so a message that was sent as "user" in one call but becomes "system"
in the next (once more messages are appended) causes errors. If the
context consists entirely of system messages, the Perplexity API call
will fail, but that mistake will be caught right away.
Args:
messages: List of message dicts with "role" and "content" keys.
Returns:
Transformed list of message dicts satisfying Perplexity's constraints.
"""
if not messages:
return messages
messages = copy.deepcopy(messages)
# Step 1: Convert non-initial system messages to "user".
# Perplexity allows system messages at the start, but rejects them
# after any non-system message.
in_initial_system_block = True
for i in range(len(messages)):
if messages[i].get("role") == "system":
if not in_initial_system_block:
messages[i]["role"] = "user"
else:
in_initial_system_block = False
# Step 2: Merge consecutive same-role messages.
# After system→user conversions above, we may have adjacent same-role
# messages that violate Perplexity's strict alternation requirement.
# Skip consecutive system messages at the start — Perplexity allows those.
i = 0
while i < len(messages) - 1:
current = messages[i]
next_msg = messages[i + 1]
if current["role"] == next_msg["role"] == "system":
# Perplexity allows multiple initial system messages, don't merge
i += 1
elif current["role"] == next_msg["role"]:
# Convert string content to list-of-dicts format for merging
if isinstance(current.get("content"), str):
current["content"] = [{"type": "text", "text": current["content"]}]
if isinstance(next_msg.get("content"), str):
next_msg["content"] = [{"type": "text", "text": next_msg["content"]}]
# Merge content from next message into current
if isinstance(current.get("content"), list) and isinstance(
next_msg.get("content"), list
):
current["content"].extend(next_msg["content"])
messages.pop(i + 1)
else:
i += 1
# Step 3: Remove trailing assistant messages.
# Perplexity requires the last message to be "user" or "tool".
# OpenAI appears to silently ignore trailing assistant messages
# server-side, so removing them preserves equivalent behavior.
while messages and messages[-1].get("role") == "assistant":
messages.pop()
return messages

View File

@@ -23,6 +23,7 @@ from typing import List, Optional, Tuple
import numpy as np
from aic_sdk import (
Model,
ParameterOutOfRangeError,
ProcessorAsync,
ProcessorConfig,
ProcessorParameter,
@@ -220,6 +221,7 @@ class AICFilter(BaseAudioFilter):
model_id: Optional[str] = None,
model_path: Optional[Path] = None,
model_download_dir: Optional[Path] = None,
enhancement_level: Optional[float] = None,
) -> None:
"""Initialize the AIC filter.
@@ -231,9 +233,12 @@ class AICFilter(BaseAudioFilter):
model_id is ignored and no download occurs.
model_download_dir: Directory for downloading models as a Path object.
Defaults to a cache directory in user's home folder.
enhancement_level: Optional overall enhancement strength (0.0..1.0).
If None, the model default is used.
Raises:
ValueError: If neither model_id nor model_path is provided.
ValueError: If neither model_id nor model_path is provided, or if
enhancement_level is out of range.
"""
# Set SDK ID for telemetry identification (6 = pipecat)
set_sdk_id(6)
@@ -244,14 +249,18 @@ class AICFilter(BaseAudioFilter):
"See https://artifacts.ai-coustics.io/ for available models."
)
if enhancement_level is not None and not 0.0 <= enhancement_level <= 1.0:
raise ValueError("'enhancement_level' must be between 0.0 and 1.0.")
self._license_key = license_key
self._model_id = model_id
self._model_path = model_path
self._model_download_dir = model_download_dir or (
Path.home() / ".cache" / "pipecat" / "aic-models"
)
self._enhancement_level = enhancement_level
self._bypass = False
self._sample_rate = 0
self._aic_ready = False
self._frames_per_block = 0
@@ -325,6 +334,26 @@ class AICFilter(BaseAudioFilter):
sensitivity=sensitivity,
)
def _apply_enhancement_level(self):
"""Apply enhancement_level if configured and supported by the active model."""
if self._processor_ctx is None or self._enhancement_level is None:
return
try:
self._processor_ctx.set_parameter(
ProcessorParameter.EnhancementLevel, self._enhancement_level
)
except ParameterOutOfRangeError as e:
logger.warning(f"AIC EnhancementLevel set_parameter out-of-range: {e}")
self._enhancement_level = None
def _apply_bypass(self):
"""Apply bypass parameter to the active processor."""
if self._processor_ctx is None:
return
self._processor_ctx.set_parameter(ProcessorParameter.Bypass, 1.0 if self._bypass else 0.0)
async def start(self, sample_rate: int):
"""Initialize the filter with the transport's sample rate.
@@ -373,14 +402,19 @@ class AICFilter(BaseAudioFilter):
self._processor_ctx = self._processor.get_processor_context()
self._vad_ctx = self._processor.get_vad_context()
# Apply initial parameters
self._processor_ctx.set_parameter(ProcessorParameter.Bypass, 1.0 if self._bypass else 0.0)
# Apply initial control parameters
self._apply_bypass()
self._apply_enhancement_level()
# Log processor information
logger.debug(f"ai-coustics filter started:")
logger.debug(f" Model ID: {self._model.get_id()}")
logger.debug(f" Sample rate: {self._sample_rate} Hz")
logger.debug(f" Frames per chunk: {self._frames_per_block}")
if self._enhancement_level is not None:
logger.debug(f" Enhancement level: {self._enhancement_level}")
else:
logger.debug(" Enhancement level not configured; using the model's default behavior.")
logger.debug(f" Optimal sample rate: {self._model.get_optimal_sample_rate()} Hz")
logger.debug(
f" Optimal number of frames for {self._sample_rate} Hz: "
@@ -425,9 +459,8 @@ class AICFilter(BaseAudioFilter):
self._bypass = not frame.enable
if self._processor_ctx is not None:
try:
self._processor_ctx.set_parameter(
ProcessorParameter.Bypass, 1.0 if self._bypass else 0.0
)
self._apply_bypass()
self._apply_enhancement_level()
except Exception as e: # noqa: BLE001
logger.error(f"AIC set_parameter failed: {e}")

View File

@@ -10,6 +10,7 @@ This module provides a smart turn analyzer that uses CoreML models for
local end-of-turn detection without requiring network connectivity.
"""
import warnings
from typing import Any, Dict
import numpy as np
@@ -35,6 +36,10 @@ class LocalCoreMLSmartTurnAnalyzer(BaseSmartTurn):
Provides end-of-turn detection using locally-stored CoreML models,
enabling offline operation without network dependencies. Optimized
for Apple Silicon and other CoreML-compatible hardware.
.. deprecated:: 0.0.106
LocalCoreMLSmartTurnAnalyzer is deprecated and will be removed in a future version.
Use LocalSmartTurnAnalyzerV3 instead.
"""
def __init__(self, *, smart_turn_model_path: str, **kwargs):
@@ -50,6 +55,15 @@ class LocalCoreMLSmartTurnAnalyzer(BaseSmartTurn):
"""
super().__init__(**kwargs)
with warnings.catch_warnings():
warnings.simplefilter("always")
warnings.warn(
"LocalCoreMLSmartTurnAnalyzer is deprecated and will be removed in a future "
"version. Use LocalSmartTurnAnalyzerV3 instead.",
DeprecationWarning,
stacklevel=2,
)
if not smart_turn_model_path:
logger.error("smart_turn_model_path is not set.")
raise Exception("smart_turn_model_path must be provided.")

View File

@@ -36,7 +36,7 @@ class LocalSmartTurnAnalyzer(BaseSmartTurn):
enabling offline operation without network dependencies. Uses
Wav2Vec2-BERT architecture for audio sequence classification.
.. deprecated:: 0.98.0
.. deprecated:: 0.0.98
LocalSmartTurnAnalyzer is deprecated and will be removed in a future version.
Use LocalSmartTurnAnalyzerV3 instead.
"""

View File

@@ -10,6 +10,7 @@ This module provides a smart turn analyzer that uses PyTorch models for
local end-of-turn detection without requiring network connectivity.
"""
import warnings
from typing import Any, Dict
import numpy as np
@@ -41,6 +42,10 @@ class LocalSmartTurnAnalyzerV2(BaseSmartTurn):
Provides end-of-turn detection using locally-stored PyTorch models,
enabling offline operation without network dependencies. Uses
Wav2Vec2 architecture for audio sequence classification.
.. deprecated:: 0.0.106
LocalSmartTurnAnalyzerV2 is deprecated and will be removed in a future version.
Use LocalSmartTurnAnalyzerV3 instead.
"""
def __init__(self, *, smart_turn_model_path: str, **kwargs):
@@ -53,6 +58,15 @@ class LocalSmartTurnAnalyzerV2(BaseSmartTurn):
"""
super().__init__(**kwargs)
with warnings.catch_warnings():
warnings.simplefilter("always")
warnings.warn(
"LocalSmartTurnAnalyzerV2 is deprecated and will be removed in a future version. "
"Use LocalSmartTurnAnalyzerV3 instead.",
DeprecationWarning,
stacklevel=2,
)
if not smart_turn_model_path:
# Define the path to the pretrained model on Hugging Face
smart_turn_model_path = "pipecat-ai/smart-turn-v2"

View File

@@ -274,8 +274,16 @@ class OutputImageRawFrame(DataFrame, ImageRawFrame):
An image that will be shown by the transport. If the transport supports
multiple video destinations (e.g. multiple video tracks) the destination
name can be specified in transport_destination.
Parameters:
sync_with_audio: If True, the image is queued with audio frames so
it is only displayed after all preceding audio has been sent.
Defaults to False (image is displayed immediately when the output
transport receives it).
"""
sync_with_audio: bool = field(default=False, init=False)
def __str__(self):
pts = format_pts(self.pts)
return f"{self.name}(pts: {pts}, destination: {self.transport_destination}, size: {self.size}, format: {self.format})"
@@ -1001,7 +1009,8 @@ class OutputDTMFFrame(DTMFFrame, DataFrame):
specify where the DTMF keypress should be sent.
"""
pass
def __str__(self):
return f"{self.name}(tone: {self.button})"
#
@@ -1658,7 +1667,8 @@ class AssistantImageRawFrame(OutputImageRawFrame):
class InputDTMFFrame(DTMFFrame, SystemFrame):
"""DTMF keypress input frame from transport."""
pass
def __str__(self):
return f"{self.name}(tone: {self.button.value})"
@dataclass
@@ -1742,7 +1752,7 @@ class ServiceSwitcherRequestMetadataFrame(ControlFrame):
@dataclass
class TaskFrame(SystemFrame):
class TaskFrame(ControlFrame):
"""Base frame for task frames.
This is a base class for frames that are meant to be sent and handled
@@ -1756,7 +1766,21 @@ class TaskFrame(SystemFrame):
@dataclass
class EndTaskFrame(TaskFrame):
class TaskSystemFrame(SystemFrame):
"""Base frame for task system frames.
This is a base class for frames that are meant to be sent and handled
upstream by the pipeline task. This might result in a corresponding frame
sent downstream (e.g. `InterruptionTaskFrame` / `InterruptionFrame` or
`EndTaskFrame` / `EndFrame`).
"""
pass
@dataclass
class EndTaskFrame(TaskFrame, UninterruptibleFrame):
"""Frame to request graceful pipeline task closure.
This is used to notify the pipeline task that the pipeline should be
@@ -1774,7 +1798,20 @@ class EndTaskFrame(TaskFrame):
@dataclass
class CancelTaskFrame(TaskFrame):
class StopTaskFrame(TaskFrame, UninterruptibleFrame):
"""Frame to request pipeline task stop while keeping processors running.
This is used to notify the pipeline task that it should be stopped as
soon as possible (flushing all the queued frames) but that the pipeline
processors should be kept in a running state. This frame should be pushed
upstream.
"""
pass
@dataclass
class CancelTaskFrame(TaskSystemFrame):
"""Frame to request immediate pipeline task cancellation.
This is used to notify the pipeline task that the pipeline should be
@@ -1792,20 +1829,7 @@ class CancelTaskFrame(TaskFrame):
@dataclass
class StopTaskFrame(TaskFrame):
"""Frame to request pipeline task stop while keeping processors running.
This is used to notify the pipeline task that it should be stopped as
soon as possible (flushing all the queued frames) but that the pipeline
processors should be kept in a running state. This frame should be pushed
upstream.
"""
pass
@dataclass
class InterruptionTaskFrame(TaskFrame):
class InterruptionTaskFrame(TaskSystemFrame):
"""Frame indicating the pipeline should be interrupted.
This frame should be pushed upstream to indicate the pipeline should be
@@ -2154,10 +2178,15 @@ class ServiceUpdateSettingsFrame(ControlFrame, UninterruptibleFrame):
delta: :class:`~pipecat.services.settings.ServiceSettings` delta-mode
object describing the fields to change.
service: Optional target service instance. When provided, only that
service will apply the settings; other services will forward the
frame unchanged.
"""
settings: Mapping[str, Any] = field(default_factory=dict)
delta: Optional["ServiceSettings"] = None
service: Optional["FrameProcessor"] = None
@dataclass

View File

@@ -9,7 +9,11 @@
from typing import Any, List, Optional, Type
from pipecat.adapters.schemas.direct_function import DirectFunction
from pipecat.pipeline.service_switcher import ServiceSwitcher, StrategyType
from pipecat.pipeline.service_switcher import (
ServiceSwitcher,
ServiceSwitcherStrategyManual,
StrategyType,
)
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.services.llm_service import LLMService
@@ -19,18 +23,20 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]):
Example::
llm_switcher = LLMSwitcher(
llms=[openai_llm, anthropic_llm],
strategy_type=ServiceSwitcherStrategyManual
)
llm_switcher = LLMSwitcher(llms=[openai_llm, anthropic_llm])
"""
def __init__(self, llms: List[LLMService], strategy_type: Type[StrategyType]):
def __init__(
self,
llms: List[LLMService],
strategy_type: Type[StrategyType] = ServiceSwitcherStrategyManual,
):
"""Initialize the service switcher with a list of LLMs and a switching strategy.
Args:
llms: List of LLM services to switch between.
strategy_type: The strategy class to use for switching between LLMs.
Defaults to ``ServiceSwitcherStrategyManual``.
"""
super().__init__(llms, strategy_type)
@@ -52,17 +58,19 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]):
"""
return self.strategy.active_service
async def run_inference(self, context: LLMContext) -> Optional[str]:
async def run_inference(self, context: LLMContext, **kwargs) -> Optional[str]:
"""Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context, using the currently active LLM.
Args:
context: The LLM context containing conversation history.
**kwargs: Additional arguments forwarded to the active LLM's run_inference
(e.g. max_tokens, system_instruction).
Returns:
The LLM's response as a string, or None if no response is generated.
"""
if self.active_llm:
return await self.active_llm.run_inference(context=context)
return await self.active_llm.run_inference(context=context, **kwargs)
return None
def register_function(
@@ -72,6 +80,7 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]):
start_callback=None,
*,
cancel_on_interruption: bool = True,
timeout_secs: Optional[float] = None,
):
"""Register a function handler for LLM function calls, on all LLMs, active or not.
@@ -88,6 +97,7 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]):
cancel_on_interruption: Whether to cancel this function call when an
interruption occurs. Defaults to True.
timeout_secs: Optional timeout in seconds for the function call.
"""
for llm in self.llms:
llm.register_function(
@@ -95,6 +105,7 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]):
handler=handler,
start_callback=start_callback,
cancel_on_interruption=cancel_on_interruption,
timeout_secs=timeout_secs,
)
def register_direct_function(
@@ -102,6 +113,7 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]):
handler: DirectFunction,
*,
cancel_on_interruption: bool = True,
timeout_secs: Optional[float] = None,
):
"""Register a direct function handler for LLM function calls, on all LLMs, active or not.
@@ -109,9 +121,11 @@ class LLMSwitcher(ServiceSwitcher[StrategyType]):
handler: The direct function to register. Must follow DirectFunction protocol.
cancel_on_interruption: Whether to cancel this function call when an
interruption occurs. Defaults to True.
timeout_secs: Optional timeout in seconds for the function call.
"""
for llm in self.llms:
llm.register_direct_function(
handler=handler,
cancel_on_interruption=cancel_on_interruption,
timeout_secs=timeout_secs,
)

View File

@@ -143,6 +143,19 @@ class ParallelPipeline(BasePipeline):
await super().process_frame(frame, direction)
# Parallel pipeline synchronized frames.
#
# - StartFrame: If a fast branch completes first, processors in
# other branches that haven't received StartFrame yet could
# receive other frames before it, causing errors.
#
# - EndFrame: If EndFrame escapes from a fast branch, downstream
# processors (e.g. output transport) begin shutting down while
# other branches still have frames to flush, causing lost output.
#
# - CancelFrame: PipelineTask waits for CancelFrame to reach the
# pipeline sink. If it escapes from a fast branch while slower
# branches are still running, the task considers cancellation
# complete prematurely.
if isinstance(frame, (StartFrame, EndFrame, CancelFrame)):
self._frame_counter[frame.id] = len(self._pipelines)
self._synchronizing = True
@@ -179,8 +192,13 @@ class ParallelPipeline(BasePipeline):
# Only push the frame when all pipelines have processed it.
if frame_counter == 0:
self._synchronizing = False
await self._parallel_push_frame(frame, direction)
await self._flush_buffered_frames()
# StartFrame should always go before any other frame.
if isinstance(frame, StartFrame):
await self._parallel_push_frame(frame, direction)
await self._flush_buffered_frames()
else:
await self._flush_buffered_frames()
await self._parallel_push_frame(frame, direction)
await self.resume_processing_system_frames()
await self.resume_processing_frames()
else:
@@ -188,7 +206,6 @@ class ParallelPipeline(BasePipeline):
async def _flush_buffered_frames(self):
"""Flush frames that were buffered during lifecycle frame synchronization."""
frames = self._buffered_frames
self._buffered_frames = []
for frame, direction in frames:
while len(self._buffered_frames) > 0:
frame, direction = self._buffered_frames.pop(0)
await self.push_frame(frame, direction)

View File

@@ -6,10 +6,12 @@
"""Service switcher for switching between different services at runtime, with different switching strategies."""
from abc import abstractmethod
from typing import Any, Generic, List, Optional, Type, TypeVar
from loguru import logger
from pipecat.frames.frames import (
ErrorFrame,
Frame,
ManuallySwitchServiceFrame,
ServiceMetadataFrame,
@@ -69,13 +71,13 @@ class ServiceSwitcherStrategy(BaseObject):
"""Return the currently active service."""
return self._active_service
@abstractmethod
async def handle_frame(
self, frame: ServiceSwitcherFrame, direction: FrameDirection
) -> Optional[FrameProcessor]:
"""Handle a frame that controls service switching.
Subclasses implement this to decide whether a switch should occur.
The base implementation returns ``None`` for all frames. Subclasses
override this to implement specific switching behaviors.
Args:
frame: The frame to handle.
@@ -84,7 +86,41 @@ class ServiceSwitcherStrategy(BaseObject):
Returns:
The newly active service if a switch occurred, or None otherwise.
"""
pass
return None
async def handle_error(self, error: ErrorFrame) -> Optional[FrameProcessor]:
"""Handle an error from the active service.
Called by ``ServiceSwitcher`` when a non-fatal ``ErrorFrame`` is pushed
upstream by the currently active service. Subclasses can override this
to implement automatic failover.
Args:
error: The error frame pushed by the active service.
Returns:
The newly active service if a switch occurred, or None otherwise.
"""
return None
async def _set_active_if_available(self, service: FrameProcessor) -> Optional[FrameProcessor]:
"""Set the active service to the given one, if it is in the list of available services.
If it's not in the list, the request is ignored, as it may have been
intended for another ServiceSwitcher in the pipeline.
Args:
service: The service to set as active.
Returns:
The newly active service, or None if the service was not found.
"""
if service in self.services:
self._active_service = service
await service.queue_frame(ServiceSwitcherRequestMetadataFrame(service=service))
await self._call_event_handler("on_service_switched", service)
return service
return None
class ServiceSwitcherStrategyManual(ServiceSwitcherStrategy):
@@ -118,23 +154,54 @@ class ServiceSwitcherStrategyManual(ServiceSwitcherStrategy):
return None
async def _set_active_if_available(self, service: FrameProcessor) -> Optional[FrameProcessor]:
"""Set the active service to the given one, if it is in the list of available services.
If it's not in the list, the request is ignored, as it may have been
intended for another ServiceSwitcher in the pipeline.
class ServiceSwitcherStrategyFailover(ServiceSwitcherStrategyManual):
"""A strategy that automatically switches to a backup service on failure.
When the active service produces a non-fatal error, this strategy switches
to the next available service in the list. Recovery and fallback policies
are left to application code via the ``on_service_switched`` event.
Event handlers available:
- on_service_switched: Called when the active service changes.
Example::
switcher = ServiceSwitcher(
services=[primary_stt, backup_stt],
strategy_type=ServiceSwitcherStrategyFailover,
)
@switcher.strategy.event_handler("on_service_switched")
async def on_switched(strategy, service):
# App decides when/how to recover the failed service
...
"""
async def handle_error(self, error: ErrorFrame) -> Optional[FrameProcessor]:
"""Handle an error from the active service by failing over.
Switches to the next service in the list. The failed service remains
in the list and can be switched back to manually or via application
logic in the ``on_service_switched`` event handler.
Args:
service: The service to set as active.
error: The error frame pushed by the active service.
Returns:
The newly active service, or None if the service was not found.
The newly active service if a switch occurred, or None if no
other service is available.
"""
if service in self.services:
self._active_service = service
await self._call_event_handler("on_service_switched", service)
return service
return None
logger.warning(f"Service {self._active_service.name} reported an error: {error.error}")
if len(self._services) <= 1:
logger.error("No other service available to switch to")
return None
current_idx = self._services.index(self._active_service)
next_idx = (current_idx + 1) % len(self._services)
return await self._set_active_if_available(self._services[next_idx])
StrategyType = TypeVar("StrategyType", bound=ServiceSwitcherStrategy)
@@ -150,18 +217,20 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
Example::
switcher = ServiceSwitcher(
services=[stt_1, stt_2],
strategy_type=ServiceSwitcherStrategyManual,
)
switcher = ServiceSwitcher(services=[stt_1, stt_2])
"""
def __init__(self, services: List[FrameProcessor], strategy_type: Type[StrategyType]):
def __init__(
self,
services: List[FrameProcessor],
strategy_type: Type[StrategyType] = ServiceSwitcherStrategyManual,
):
"""Initialize the service switcher with a list of services and a switching strategy.
Args:
services: List of frame processors to switch between.
strategy_type: The strategy class to use for switching between services.
Defaults to ``ServiceSwitcherStrategyManual``.
"""
_strategy = strategy_type(services)
super().__init__(*self._make_pipeline_definitions(services, _strategy))
@@ -227,6 +296,10 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
all the filters let it pass, and `StartFrame` causes the service to
generate `ServiceMetadataFrame`.
Non-fatal ``ErrorFrame`` instances are forwarded to the strategy via
``handle_error`` so strategies like ``ServiceSwitcherStrategyFailover``
can perform failover. The error frame is still propagated upstream so
that application-level error handlers can observe it.
"""
# Consume ServiceSwitcherRequestMetadataFrame once the targeted service
# has handled it (i.e. the active service).
@@ -239,6 +312,10 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
if frame.service_name != self.strategy.active_service.name:
return
# Let the strategy react to non-fatal errors from the active service.
if isinstance(frame, ErrorFrame) and not frame.fatal:
await self.strategy.handle_error(frame)
await super().push_frame(frame, direction)
async def process_frame(self, frame: Frame, direction: FrameDirection):
@@ -255,9 +332,5 @@ class ServiceSwitcher(ParallelPipeline, Generic[StrategyType]):
# frame. If we switched, we just swallow the frame.
if not service:
await super().process_frame(frame, direction)
# If we switched to a new service, request its metadata.
if service:
await service.queue_frame(ServiceSwitcherRequestMetadataFrame(service=service))
else:
await super().process_frame(frame, direction)

View File

@@ -4,15 +4,21 @@
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Synchronous parallel pipeline implementation for concurrent frame processing.
"""Synchronized parallel pipeline that holds output until all branches finish.
This module provides a pipeline that processes frames through multiple parallel
pipelines simultaneously, synchronizing their output to maintain frame ordering
and prevent duplicate processing.
A SyncParallelPipeline fans each inbound frame out to multiple parallel pipelines
and waits for every pipeline to finish processing before releasing any of the
resulting output frames. This ensures that all frames produced in response to a
single input frame are emitted together.
System frames (except EndFrame) are exempt from this synchronization — they pass
straight through without waiting, since they are expected to race ahead of
regular data frames.
"""
import asyncio
from dataclasses import dataclass
from enum import Enum
from itertools import chain
from typing import List
@@ -24,22 +30,42 @@ from pipecat.pipeline.pipeline import Pipeline
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, FrameProcessorSetup
class FrameOrder(Enum):
"""Controls the order in which synchronized frames are pushed downstream.
When multiple parallel pipelines produce output for the same input frame,
this setting determines the order in which those output frames are pushed.
Attributes:
ARRIVAL: Frames are pushed in the order they arrive from any pipeline.
This is the default and matches the behavior of prior versions.
PIPELINE: Frames are pushed in pipeline definition order — all frames
from the first pipeline are pushed, then all frames from the second
pipeline, and so on. Useful when the relative ordering between
pipelines matters (e.g. ensuring image frames precede audio frames).
"""
ARRIVAL = "arrival"
PIPELINE = "pipeline"
@dataclass
class SyncFrame(ControlFrame):
"""Control frame used to synchronize parallel pipeline processing.
"""Sentinel frame used to detect when a parallel pipeline has finished processing.
This frame is sent through parallel pipelines to determine when the
internal pipelines have finished processing a batch of frames.
After sending a real frame into a parallel pipeline, a SyncFrame is sent
behind it. When the SyncFrame emerges from the pipeline's output, we know
all output frames for the preceding input have been produced.
"""
pass
class SyncParallelPipelineSource(FrameProcessor):
"""Source processor for synchronous parallel pipeline processing.
"""Bookend processor placed at the start of each parallel pipeline.
Routes frames to parallel pipelines and collects upstream responses
for synchronization purposes.
Forwards downstream frames into the pipeline and captures upstream frames
into a queue so the parent SyncParallelPipeline can release them later.
"""
def __init__(self, upstream_queue: asyncio.Queue):
@@ -68,10 +94,11 @@ class SyncParallelPipelineSource(FrameProcessor):
class SyncParallelPipelineSink(FrameProcessor):
"""Sink processor for synchronous parallel pipeline processing.
"""Bookend processor placed at the end of each parallel pipeline.
Collects downstream frames from parallel pipelines and routes
upstream frames back through the pipeline.
Captures downstream output frames into a queue so the parent
SyncParallelPipeline can release them later, and forwards upstream
frames back through the pipeline.
"""
def __init__(self, downstream_queue: asyncio.Queue):
@@ -100,29 +127,44 @@ class SyncParallelPipelineSink(FrameProcessor):
class SyncParallelPipeline(BasePipeline):
"""Pipeline that processes frames through multiple parallel pipelines synchronously.
"""Fans each input frame to parallel pipelines then holds output until every pipeline finishes.
Creates multiple parallel processing paths that all receive the same input frames
and produces synchronized output. Each parallel path is a separate pipeline that
processes frames independently, with synchronization points to ensure consistent
ordering and prevent duplicate frame processing.
For each inbound frame the pipeline:
The pipeline uses SyncFrame control frames to coordinate between parallel paths
and ensure all paths have completed processing before moving to the next frame.
1. Sends the frame into every parallel pipeline.
2. Sends a ``SyncFrame`` sentinel behind it in each pipeline.
3. Waits until every pipeline has produced its ``SyncFrame``, meaning all
output for that input is ready.
4. Releases the collected output frames (deduplicating by frame id, since
the same frame may emerge from more than one branch).
System frames (except ``EndFrame``) bypass this mechanism entirely — they are
forwarded through each pipeline and pushed immediately, since system frames
are expected to race ahead of regular data frames.
By default, output frames are pushed in the order they arrive from any pipeline
(``FrameOrder.ARRIVAL``). Set ``frame_order=FrameOrder.PIPELINE`` to push frames
in pipeline definition order instead — all output from the first pipeline, then
the second, and so on.
"""
def __init__(self, *args):
def __init__(self, *args, frame_order: FrameOrder = FrameOrder.ARRIVAL):
"""Initialize the synchronous parallel pipeline.
Args:
*args: Variable number of processor lists, each representing a parallel pipeline path.
Each argument should be a list of FrameProcessor instances.
*args: Variable number of processor lists, each representing a parallel
pipeline path. Each argument should be a list of FrameProcessor instances.
frame_order: Controls the order in which synchronized output frames are
pushed. ``FrameOrder.ARRIVAL`` (default) pushes frames in the order they arrive.
``FrameOrder.PIPELINE`` pushes all frames from the first pipeline
before the second, and so on.
Raises:
Exception: If no arguments are provided.
TypeError: If any argument is not a list of processors.
"""
super().__init__()
self._frame_order = frame_order
if len(args) == 0:
raise Exception(f"SyncParallelPipeline needs at least one argument")
@@ -184,7 +226,7 @@ class SyncParallelPipeline(BasePipeline):
Returns:
The list of entry processors.
"""
return self._sources
return [s["processor"] for s in self._sources]
def processors_with_metrics(self) -> List[FrameProcessor]:
"""Collect processors that can generate metrics from all parallel pipelines.
@@ -209,11 +251,11 @@ class SyncParallelPipeline(BasePipeline):
await asyncio.gather(*[p.cleanup() for p in self._pipelines])
async def process_frame(self, frame: Frame, direction: FrameDirection):
"""Process frames through all parallel pipelines with synchronization.
"""Send a frame through all parallel pipelines and release output once all finish.
Distributes frames to all parallel pipelines and synchronizes their output
to maintain proper ordering and prevent duplicate processing. Uses SyncFrame
control frames to coordinate between parallel paths.
System frames (except EndFrame) skip synchronization and pass straight
through. All other frames are fanned out to every pipeline, and output is
held until every pipeline signals completion (via SyncFrame).
Args:
frame: The frame to process.
@@ -221,60 +263,102 @@ class SyncParallelPipeline(BasePipeline):
"""
await super().process_frame(frame, direction)
# SystemFrames (but not EndFrame) are simply passed through all
# internal pipelines without draining queued output. This avoids
# the race condition where a SystemFrame's wait_for_sync steals
# frames from a concurrent non-SystemFrame's wait_for_sync.
if isinstance(frame, SystemFrame) and not isinstance(frame, EndFrame):
if direction == FrameDirection.UPSTREAM:
for s in self._sinks:
await s["processor"].process_frame(frame, direction)
elif direction == FrameDirection.DOWNSTREAM:
for s in self._sources:
await s["processor"].process_frame(frame, direction)
await self.push_frame(frame, direction)
return
use_pipeline_order = self._frame_order == FrameOrder.PIPELINE
# The last processor of each pipeline needs to be synchronous otherwise
# this element won't work. Since, we know it should be synchronous we
# this element won't work. Since we know it should be synchronous we
# push a SyncFrame. Since frames are ordered we know this frame will be
# pushed after the synchronous processor has pushed its data allowing us
# to synchrnonize all the internal pipelines by waiting for the
# to synchronize all the internal pipelines by waiting for the
# SyncFrame in all of them.
#
# In ARRIVAL mode, output frames are put onto a shared main_queue as
# they arrive. In PIPELINE mode, they are accumulated in a per-pipeline
# list and returned so the caller can drain them in definition order.
async def wait_for_sync(
obj, main_queue: asyncio.Queue, frame: Frame, direction: FrameDirection
):
) -> list[Frame]:
processor = obj["processor"]
queue = obj["queue"]
output_frames: list[Frame] = []
await processor.process_frame(frame, direction)
if isinstance(frame, (SystemFrame, EndFrame)):
if isinstance(frame, EndFrame):
new_frame = await queue.get()
if isinstance(new_frame, (SystemFrame, EndFrame)):
await main_queue.put(new_frame)
else:
while not isinstance(new_frame, (SystemFrame, EndFrame)):
if isinstance(new_frame, EndFrame):
if use_pipeline_order:
output_frames.append(new_frame)
else:
await main_queue.put(new_frame)
else:
while not isinstance(new_frame, EndFrame):
if use_pipeline_order:
output_frames.append(new_frame)
else:
await main_queue.put(new_frame)
queue.task_done()
new_frame = await queue.get()
else:
await processor.process_frame(SyncFrame(), direction)
new_frame = await queue.get()
while not isinstance(new_frame, SyncFrame):
await main_queue.put(new_frame)
if use_pipeline_order:
output_frames.append(new_frame)
else:
await main_queue.put(new_frame)
queue.task_done()
new_frame = await queue.get()
return output_frames
if direction == FrameDirection.UPSTREAM:
# If we get an upstream frame we process it in each sink.
await asyncio.gather(
frames_per_pipeline = await asyncio.gather(
*[wait_for_sync(s, self._up_queue, frame, direction) for s in self._sinks]
)
elif direction == FrameDirection.DOWNSTREAM:
# If we get a downstream frame we process it in each source.
await asyncio.gather(
frames_per_pipeline = await asyncio.gather(
*[wait_for_sync(s, self._down_queue, frame, direction) for s in self._sources]
)
seen_ids = set()
while not self._up_queue.empty():
frame = await self._up_queue.get()
if frame.id not in seen_ids:
await self.push_frame(frame, FrameDirection.UPSTREAM)
seen_ids.add(frame.id)
self._up_queue.task_done()
if use_pipeline_order:
# Push frames in pipeline definition order, deduplicating by id.
seen_ids = set()
for pipeline_frames in frames_per_pipeline:
for f in pipeline_frames:
if f.id not in seen_ids:
await self.push_frame(f, direction)
seen_ids.add(f.id)
else:
# ARRIVAL mode: drain the shared queues in the order frames arrived.
seen_ids = set()
while not self._up_queue.empty():
frame = await self._up_queue.get()
if frame.id not in seen_ids:
await self.push_frame(frame, FrameDirection.UPSTREAM)
seen_ids.add(frame.id)
self._up_queue.task_done()
seen_ids = set()
while not self._down_queue.empty():
frame = await self._down_queue.get()
if frame.id not in seen_ids:
await self.push_frame(frame, FrameDirection.DOWNSTREAM)
seen_ids.add(frame.id)
self._down_queue.task_done()
seen_ids = set()
while not self._down_queue.empty():
frame = await self._down_queue.get()
if frame.id not in seen_ids:
await self.push_frame(frame, FrameDirection.DOWNSTREAM)
seen_ids.add(frame.id)
self._down_queue.task_done()

View File

@@ -876,22 +876,22 @@ class PipelineTask(BasePipelineTask):
if isinstance(frame, EndTaskFrame):
# Tell the task we should end nicely.
logger.debug(f"{self}: received end task frame {frame}")
logger.debug(f"{self}: received end task frame upstream {frame}")
await self.queue_frame(EndFrame(reason=frame.reason))
elif isinstance(frame, CancelTaskFrame):
# Tell the task we should end right away.
logger.debug(f"{self}: received cancel task frame {frame}")
logger.debug(f"{self}: received cancel task frame upstream {frame}")
await self.queue_frame(CancelFrame(reason=frame.reason))
elif isinstance(frame, StopTaskFrame):
# Tell the task we should stop nicely.
logger.debug(f"{self}: received stop task frame {frame}")
logger.debug(f"{self}: received stop task frame upstream {frame}")
await self.queue_frame(StopFrame())
elif isinstance(frame, InterruptionTaskFrame):
# Tell the task we should interrupt the pipeline. Note that we are
# bypassing the push queue and directly queue into the
# pipeline. This is in case the push task is blocked waiting for a
# pipeline-ending frame to finish traversing the pipeline.
logger.debug(f"{self}: received interruption task frame {frame}")
logger.debug(f"{self}: received interruption task frame upstream {frame}")
await self._pipeline.queue_frame(InterruptionFrame())
elif isinstance(frame, ErrorFrame):
await self._call_event_handler("on_pipeline_error", frame)
@@ -934,6 +934,18 @@ class PipelineTask(BasePipelineTask):
self._pipeline_end_event.set()
elif isinstance(frame, HeartbeatFrame):
await self._heartbeat_queue.put(frame)
elif isinstance(frame, EndTaskFrame):
logger.debug(f"{self}: received end task frame downstream {frame}")
await self.queue_frame(EndTaskFrame(reason=frame.reason), FrameDirection.UPSTREAM)
elif isinstance(frame, StopTaskFrame):
logger.debug(f"{self}: received stop task frame downstream {frame}")
await self.queue_frame(StopTaskFrame(), FrameDirection.UPSTREAM)
elif isinstance(frame, CancelTaskFrame):
logger.debug(f"{self}: received cancel task frame downstream {frame}")
await self.queue_frame(CancelTaskFrame(reason=frame.reason), FrameDirection.UPSTREAM)
elif isinstance(frame, InterruptionTaskFrame):
logger.debug(f"{self}: received interruption task frame downstream {frame}")
await self.queue_frame(InterruptionTaskFrame(), FrameDirection.UPSTREAM)
async def _heartbeat_push_handler(self):
"""Push heartbeat frames at regular intervals."""

View File

@@ -75,6 +75,7 @@ from pipecat.processors.aggregators.llm_context_summarizer import (
SummaryAppliedEvent,
)
from pipecat.processors.frame_processor import FrameCallback, FrameDirection, FrameProcessor
from pipecat.services.settings import LLMSettings
from pipecat.turns.user_idle_controller import UserIdleController
from pipecat.turns.user_mute import BaseUserMuteStrategy
from pipecat.turns.user_start import BaseUserTurnStartStrategy, UserTurnStartedParams
@@ -446,6 +447,9 @@ class LLMUserAggregator(LLMContextAggregator):
self._user_turn_controller.add_event_handler(
"on_user_turn_stop_timeout", self._on_user_turn_stop_timeout
)
self._user_turn_controller.add_event_handler(
"on_reset_aggregation", self._on_reset_aggregation
)
self._user_idle_controller = UserIdleController(
user_idle_timeout=self._params.user_idle_timeout
@@ -561,10 +565,10 @@ class LLMUserAggregator(LLMContextAggregator):
# Enable the feature on the LLM with config
await self.push_frame(
LLMUpdateSettingsFrame(
settings={
"filter_incomplete_user_turns": True,
"user_turn_completion_config": config,
}
delta=LLMSettings(
filter_incomplete_user_turns=True,
user_turn_completion_config=config,
)
)
)
@@ -747,6 +751,12 @@ class LLMUserAggregator(LLMContextAggregator):
await self._maybe_emit_user_turn_stopped(strategy)
async def _on_reset_aggregation(
self, controller: UserTurnController, strategy: BaseUserTurnStartStrategy
):
logger.debug(f"{self}: Resetting aggregation (strategy: {strategy})")
await self.reset()
async def _on_user_turn_stop_timeout(self, controller):
await self._call_event_handler("on_user_turn_stop_timeout")

View File

@@ -6,6 +6,9 @@
"""Wake phrase detection filter for Pipecat transcription processing.
.. deprecated:: 0.0.106
Use :class:`~pipecat.turns.user_start.WakePhraseUserTurnStartStrategy` instead.
This module provides a frame processor that filters transcription frames,
only allowing them through after wake phrases have been detected. Includes
keepalive functionality to maintain conversation flow after wake detection.
@@ -13,6 +16,7 @@ keepalive functionality to maintain conversation flow after wake detection.
import re
import time
import warnings
from enum import Enum
from typing import List
@@ -25,6 +29,11 @@ from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
class WakeCheckFilter(FrameProcessor):
"""Frame processor that filters transcription frames based on wake phrase detection.
.. deprecated:: 0.0.106
Use :class:`~pipecat.turns.user_start.WakePhraseUserTurnStartStrategy` instead,
which integrates with the user turn strategy system and supports configurable
timeouts and single-activation mode.
This filter monitors transcription frames for configured wake phrases and only
passes frames through after a wake phrase has been detected. Maintains a
keepalive timeout to allow continued conversation after wake detection.
@@ -65,12 +74,21 @@ class WakeCheckFilter(FrameProcessor):
def __init__(self, wake_phrases: List[str], keepalive_timeout: float = 3):
"""Initialize the wake phrase filter.
.. deprecated:: 0.0.106
Use :class:`~pipecat.turns.user_start.WakePhraseUserTurnStartStrategy` instead.
Args:
wake_phrases: List of wake phrases to detect in transcriptions.
keepalive_timeout: Duration in seconds to keep passing frames after
wake detection. Defaults to 3 seconds.
"""
super().__init__()
warnings.warn(
"WakeCheckFilter is deprecated since v0.0.106. "
"Use WakePhraseUserTurnStartStrategy instead.",
DeprecationWarning,
stacklevel=2,
)
self._participant_states = {}
self._keepalive_timeout = keepalive_timeout
self._wake_patterns = []

View File

@@ -79,14 +79,17 @@ async def configure(
aiohttp_session: aiohttp.ClientSession,
*,
api_key: Optional[str] = None,
room_exp_duration: Optional[float] = 2.0,
token_exp_duration: Optional[float] = 2.0,
room_exp_duration: float = 2.0,
token_exp_duration: float = 2.0,
sip_caller_phone: Optional[str] = None,
sip_enable_video: Optional[bool] = False,
sip_num_endpoints: Optional[int] = 1,
sip_enable_video: bool = False,
sip_num_endpoints: int = 1,
enable_dialout: bool = False,
sip_codecs: Optional[Dict[str, List[str]]] = None,
sip_provider: Optional[str] = None,
room_geo: Optional[str] = None,
room_properties: Optional[DailyRoomProperties] = None,
token_properties: Optional["DailyMeetingTokenProperties"] = None,
token_properties: Optional[DailyMeetingTokenProperties] = None,
) -> DailyRoomConfig:
"""Configure Daily room URL and token with optional SIP capabilities.
@@ -103,8 +106,14 @@ async def configure(
When provided, enables SIP functionality and returns SipRoomConfig.
sip_enable_video: Whether video is enabled for SIP.
sip_num_endpoints: Number of allowed SIP endpoints.
enable_dialout: Whether to enable outbound dialing (PSTN or SIP) on the room.
Requires dial-out entitlement on your Daily account.
sip_codecs: Codecs to support for audio and video. If None, uses Daily defaults.
Example: {"audio": ["OPUS"], "video": ["H264"]}
sip_provider: SIP provider name (e.g., "daily"). Only used when
sip_caller_phone is provided and room_properties is not.
room_geo: Daily room geographic region (e.g., "us-east-1"). Only used
when room_properties is not provided.
room_properties: Optional DailyRoomProperties to use instead of building from
individual parameters. When provided, this overrides room_exp_duration and
SIP-related parameters. If not provided, properties are built from the
@@ -153,7 +162,10 @@ async def configure(
sip_caller_phone is not None,
sip_enable_video is not False,
sip_num_endpoints != 1,
enable_dialout is not False,
sip_codecs is not None,
sip_provider is not None,
room_geo is not None,
]
)
if individual_params_provided:
@@ -176,6 +188,8 @@ async def configure(
aiohttp_session=aiohttp_session,
)
token_expiry_seconds: float = token_exp_duration * 60 * 60
# Check for existing room URL (only in standard mode)
existing_room_url = os.getenv("DAILY_ROOM_URL")
if existing_room_url and not sip_enabled:
@@ -184,11 +198,12 @@ async def configure(
room_url = existing_room_url
# Create token and return standard format
expiry_time: float = token_exp_duration * 60 * 60
token_params = None
if token_properties:
token_params = DailyMeetingTokenParams(properties=token_properties)
token = await daily_rest_helper.get_token(room_url, expiry_time, params=token_params)
token = await daily_rest_helper.get_token(
room_url, token_expiry_seconds, params=token_params
)
return DailyRoomConfig(room_url=room_url, token=token)
# Create a new room
@@ -207,6 +222,9 @@ async def configure(
eject_at_room_exp=True,
)
if room_geo:
room_properties.geo = room_geo
# Add SIP configuration if enabled
if sip_enabled:
sip_params = DailyRoomSipParams(
@@ -215,9 +233,10 @@ async def configure(
sip_mode="dial-in",
num_endpoints=sip_num_endpoints,
codecs=sip_codecs,
provider=sip_provider,
)
room_properties.sip = sip_params
room_properties.enable_dialout = True # Enable outbound calls if needed
room_properties.enable_dialout = enable_dialout
room_properties.start_video_off = not sip_enable_video # Voice-only by default
# Create room parameters
@@ -229,7 +248,6 @@ async def configure(
logger.info(f"Created Daily room: {room_url}")
# Create meeting token
token_expiry_seconds = token_exp_duration * 60 * 60
token_params = None
if token_properties:
token_params = DailyMeetingTokenParams(properties=token_properties)

View File

@@ -10,6 +10,7 @@ Provides the foundation for all AI services in the Pipecat framework, including
model management, settings handling, and frame processing lifecycle methods.
"""
import warnings
from typing import Any, AsyncGenerator, Dict
from loguru import logger
@@ -130,6 +131,43 @@ class AIService(FrameProcessor):
return changed
def _warn_init_param_moved_to_settings(
self,
param_name: str,
settings_field: str | None = None,
stacklevel: int = 3,
):
"""Warn that an ``__init__`` param has moved to ``Settings``.
Emits a ``DeprecationWarning`` directing users to the canonical
``settings=ServiceClass.Settings(field=...)`` API.
Args:
param_name: Name of the deprecated ``__init__`` parameter.
settings_field: The corresponding field on the ``Settings``
dataclass, if different from *param_name*. When ``None``
the message omits the field hint.
stacklevel: Stack depth for the warning. Default ``3`` targets
the caller's caller (i.e. user code that instantiated the
service).
"""
label = f"{type(self).__name__}.Settings"
if settings_field:
msg = (
f"The `{param_name}` parameter is deprecated. "
f"Use `settings={label}({settings_field}=...)` instead. "
f"If both are provided, `settings` takes precedence."
)
else:
msg = (
f"The `{param_name}` parameter is deprecated. "
f"Use `settings={label}(...)` instead. "
f"If both are provided, `settings` takes precedence."
)
with warnings.catch_warnings():
warnings.simplefilter("always")
warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel)
def _warn_unhandled_updated_settings(self, unhandled):
"""Log a warning for settings changes that won't take effect at runtime.

View File

@@ -58,7 +58,7 @@ from pipecat.processors.aggregators.openai_llm_context import (
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.llm_service import FunctionCallFromLLM, LLMService
from pipecat.services.settings import NOT_GIVEN as _NOT_GIVEN
from pipecat.services.settings import LLMSettings, _NotGiven, _warn_deprecated_param, is_given
from pipecat.services.settings import LLMSettings, _NotGiven, is_given
from pipecat.utils.tracing.service_decorators import traced_llm
try:
@@ -98,18 +98,20 @@ class AnthropicLLMSettings(LLMSettings):
"""
enable_prompt_caching: bool | _NotGiven = field(default_factory=lambda: _NOT_GIVEN)
thinking: AnthropicThinkingConfig | _NotGiven = field(default_factory=lambda: _NOT_GIVEN)
thinking: Union["AnthropicLLMService.ThinkingConfig", _NotGiven] = field(
default_factory=lambda: _NOT_GIVEN
)
@classmethod
def from_mapping(cls, settings):
"""Convert a plain dict to settings, coercing thinking dicts.
For backward compatibility, a ``thinking`` value that is a plain dict
is converted to a :class:`AnthropicThinkingConfig`.
is converted to a :class:`AnthropicLLMService.ThinkingConfig`.
"""
instance = super().from_mapping(settings)
if is_given(instance.thinking) and isinstance(instance.thinking, dict):
instance.thinking = AnthropicThinkingConfig(**instance.thinking)
instance.thinking = AnthropicLLMService.ThinkingConfig(**instance.thinking)
return instance
@@ -160,7 +162,7 @@ class AnthropicLLMService(LLMService):
"""
Settings = AnthropicLLMSettings
_settings: AnthropicLLMSettings
_settings: Settings
# Overriding the default adapter to use the Anthropic one.
adapter_class = AnthropicLLMAdapter
@@ -172,7 +174,7 @@ class AnthropicLLMService(LLMService):
"""Input parameters for Anthropic model inference.
.. deprecated:: 0.0.105
Use ``AnthropicLLMSettings`` instead. Pass settings directly via the
Use ``AnthropicLLMService.Settings`` instead. Pass settings directly via the
``settings`` parameter of :class:`AnthropicLLMService`.
Parameters:
@@ -199,7 +201,9 @@ class AnthropicLLMService(LLMService):
temperature: Optional[float] = Field(default_factory=lambda: NOT_GIVEN, ge=0.0, le=1.0)
top_k: Optional[int] = Field(default_factory=lambda: NOT_GIVEN, ge=0)
top_p: Optional[float] = Field(default_factory=lambda: NOT_GIVEN, ge=0.0, le=1.0)
thinking: Optional[AnthropicThinkingConfig] = Field(default_factory=lambda: NOT_GIVEN)
thinking: Optional["AnthropicLLMService.ThinkingConfig"] = Field(
default_factory=lambda: NOT_GIVEN
)
extra: Optional[Dict[str, Any]] = Field(default_factory=dict)
def model_post_init(self, __context):
@@ -220,7 +224,7 @@ class AnthropicLLMService(LLMService):
api_key: str,
model: Optional[str] = None,
params: Optional[InputParams] = None,
settings: Optional[AnthropicLLMSettings] = None,
settings: Optional[Settings] = None,
client=None,
retry_timeout_secs: Optional[float] = 5.0,
retry_on_timeout: Optional[bool] = False,
@@ -233,12 +237,12 @@ class AnthropicLLMService(LLMService):
model: Model name to use.
.. deprecated:: 0.0.105
Use ``settings=AnthropicLLMSettings(model=...)`` instead.
Use ``settings=AnthropicLLMService.Settings(model=...)`` instead.
params: Optional model parameters for inference.
.. deprecated:: 0.0.105
Use ``settings=AnthropicLLMSettings(...)`` instead.
Use ``settings=AnthropicLLMService.Settings(...)`` instead.
settings: Runtime-updatable settings for this service. When both
deprecated parameters and *settings* are provided, *settings*
@@ -249,7 +253,7 @@ class AnthropicLLMService(LLMService):
**kwargs: Additional arguments passed to parent LLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AnthropicLLMSettings(
default_settings = self.Settings(
model="claude-sonnet-4-6",
system_instruction=None,
max_tokens=4096,
@@ -268,12 +272,12 @@ class AnthropicLLMService(LLMService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", AnthropicLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", AnthropicLLMSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.max_tokens = params.max_tokens
default_settings.temperature = params.temperature
@@ -346,7 +350,10 @@ class AnthropicLLMService(LLMService):
return response
async def run_inference(
self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None
self,
context: LLMContext | OpenAILLMContext,
max_tokens: Optional[int] = None,
system_instruction: Optional[str] = None,
) -> Optional[str]:
"""Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context.
@@ -354,6 +361,8 @@ class AnthropicLLMService(LLMService):
context: The LLM context containing conversation history.
max_tokens: Optional maximum number of tokens to generate. If provided,
overrides the service's default max_tokens setting.
system_instruction: Optional system instruction to use for this inference.
If provided, overrides any system instruction in the context.
Returns:
The LLM's response as a string, or None if no response is generated.
@@ -375,6 +384,15 @@ class AnthropicLLMService(LLMService):
system = getattr(context, "system", NOT_GIVEN)
tools = context.tools or []
# Override system instruction if provided
if system_instruction is not None:
if system and system is not NOT_GIVEN:
logger.warning(
f"{self}: Both system_instruction and a system message in context are set."
" Using system_instruction."
)
system = system_instruction
# Build params using the same method as streaming completions
params = {
"model": self._settings.model,
@@ -1228,7 +1246,7 @@ class AnthropicAssistantContextAggregator(LLMAssistantContextAggregator):
frame: Frame containing function call result.
"""
if frame.result:
result = json.dumps(frame.result)
result = json.dumps(frame.result, ensure_ascii=False)
await self._update_function_call_result(frame.function_name, frame.tool_call_id, result)
else:
await self._update_function_call_result(

View File

@@ -125,7 +125,7 @@ class AssemblyAIConnectionParams(BaseModel):
"""Configuration parameters for AssemblyAI WebSocket connection.
.. deprecated:: 0.0.105
Use ``settings=AssemblyAISTTSettings(foo=...)`` instead.
Use ``settings=AssemblyAISTTService.Settings(foo=...)`` instead.
Parameters:
sample_rate: Audio sample rate in Hz. Defaults to 16000.

View File

@@ -32,7 +32,7 @@ from pipecat.frames.frames import (
VADUserStoppedSpeakingFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven
from pipecat.services.stt_latency import ASSEMBLYAI_TTFS_P99
from pipecat.services.stt_service import WebsocketSTTService
from pipecat.transcriptions.language import Language
@@ -129,7 +129,7 @@ class AssemblyAISTTService(WebsocketSTTService):
"""
Settings = AssemblyAISTTSettings
_settings: AssemblyAISTTSettings
_settings: Settings
def __init__(
self,
@@ -143,7 +143,7 @@ class AssemblyAISTTService(WebsocketSTTService):
vad_force_turn_endpoint: bool = True,
should_interrupt: bool = True,
speaker_format: Optional[str] = None,
settings: Optional[AssemblyAISTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = ASSEMBLYAI_TTFS_P99,
**kwargs,
):
@@ -154,7 +154,7 @@ class AssemblyAISTTService(WebsocketSTTService):
language: Language code for transcription. Defaults to English (Language.EN).
.. deprecated:: 0.0.105
Use ``settings=AssemblyAISTTSettings(language=...)`` instead.
Use ``settings=AssemblyAISTTService.Settings(language=...)`` instead.
api_endpoint_base_url: WebSocket endpoint URL. Defaults to AssemblyAI's streaming endpoint.
sample_rate: Audio sample rate in Hz. Defaults to 16000.
@@ -162,7 +162,7 @@ class AssemblyAISTTService(WebsocketSTTService):
connection_params: Connection configuration parameters.
.. deprecated:: 0.0.105
Use ``settings=AssemblyAISTTSettings(...)`` instead.
Use ``settings=AssemblyAISTTService.Settings(...)`` instead.
vad_force_turn_endpoint: Controls turn detection mode.
When True (Pipecat mode, default): Forces AssemblyAI to return finals ASAP
@@ -190,7 +190,7 @@ class AssemblyAISTTService(WebsocketSTTService):
**kwargs: Additional arguments passed to parent STTService class.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AssemblyAISTTSettings(
default_settings = self.Settings(
model="u3-rt-pro",
language=Language.EN,
formatted_finals=True,
@@ -208,12 +208,12 @@ class AssemblyAISTTService(WebsocketSTTService):
# 2. Apply direct init arg overrides (deprecated)
if language is not None:
_warn_deprecated_param("language", AssemblyAISTTSettings, "language")
self._warn_init_param_moved_to_settings("language", "language")
default_settings.language = language
# 3. Apply connection_params overrides (deprecated) — only if settings not provided
if connection_params is not None:
_warn_deprecated_param("connection_params", AssemblyAISTTSettings)
self._warn_init_param_moved_to_settings("connection_params")
if not settings:
sample_rate = connection_params.sample_rate
encoding = connection_params.encoding
@@ -299,7 +299,7 @@ class AssemblyAISTTService(WebsocketSTTService):
self._user_speaking = False
def _configure_pipecat_turn_mode(self, settings: AssemblyAISTTSettings, is_u3_pro: bool):
def _configure_pipecat_turn_mode(self, settings: Settings, is_u3_pro: bool):
"""Configure settings for Pipecat turn detection mode.
When vad_force_turn_endpoint is enabled, force AssemblyAI to return
@@ -353,7 +353,7 @@ class AssemblyAISTTService(WebsocketSTTService):
"""
return True
async def _update_settings(self, delta: AssemblyAISTTSettings) -> dict[str, Any]:
async def _update_settings(self, delta: Settings) -> dict[str, Any]:
"""Apply a settings delta and reconnect to apply changes.
Args:

View File

@@ -26,7 +26,7 @@ from pipecat.frames.frames import (
TTSStoppedFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import TTSSettings, _warn_deprecated_param
from pipecat.services.settings import TTSSettings
from pipecat.services.tts_service import TextAggregationMode, TTSService, WebsocketTTSService
from pipecat.transcriptions.language import Language, resolve_language
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -86,13 +86,13 @@ class AsyncAITTSService(WebsocketTTSService):
"""
Settings = AsyncAITTSSettings
_settings: AsyncAITTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Async TTS configuration.
.. deprecated:: 0.0.105
Use ``AsyncAITTSSettings`` directly via the ``settings`` parameter instead.
Use ``AsyncAITTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
language: Language to use for synthesis.
@@ -112,7 +112,7 @@ class AsyncAITTSService(WebsocketTTSService):
encoding: str = "pcm_s16le",
container: str = "raw",
params: Optional[InputParams] = None,
settings: Optional[AsyncAITTSSettings] = None,
settings: Optional[Settings] = None,
aggregate_sentences: Optional[bool] = None,
text_aggregation_mode: Optional[TextAggregationMode] = None,
**kwargs,
@@ -125,14 +125,14 @@ class AsyncAITTSService(WebsocketTTSService):
https://docs.async.com/list-voices-16699698e0
.. deprecated:: 0.0.105
Use ``settings=AsyncAITTSSettings(voice=...)`` instead.
Use ``settings=AsyncAITTSService.Settings(voice=...)`` instead.
version: Async API version.
url: WebSocket URL for Async TTS API.
model: TTS model to use (e.g., "async_flash_v1.0").
.. deprecated:: 0.0.105
Use ``settings=AsyncAITTSSettings(model=...)`` instead.
Use ``settings=AsyncAITTSService.Settings(model=...)`` instead.
sample_rate: Audio sample rate.
encoding: Audio encoding format.
@@ -140,7 +140,7 @@ class AsyncAITTSService(WebsocketTTSService):
params: Additional input parameters for voice customization.
.. deprecated:: 0.0.105
Use ``settings=AsyncAITTSSettings(...)`` instead.
Use ``settings=AsyncAITTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -153,7 +153,7 @@ class AsyncAITTSService(WebsocketTTSService):
**kwargs: Additional arguments passed to the parent service.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AsyncAITTSSettings(
default_settings = self.Settings(
model="async_flash_v1.0",
voice=None,
language=None,
@@ -161,19 +161,17 @@ class AsyncAITTSService(WebsocketTTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", AsyncAITTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if model is not None:
_warn_deprecated_param("model", AsyncAITTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", AsyncAITTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.language = (
self.language_to_service_language(params.language) if params.language else None
)
default_settings.language = params.language
# 4. Apply settings delta (canonical API, always wins)
if settings is not None:
@@ -487,13 +485,13 @@ class AsyncAIHttpTTSService(TTSService):
"""
Settings = AsyncAITTSSettings
_settings: AsyncAITTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Async API.
.. deprecated:: 0.0.105
Use ``AsyncAITTSSettings`` directly via the ``settings`` parameter instead.
Use ``AsyncAIHttpTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
language: Language to use for synthesis.
@@ -514,7 +512,7 @@ class AsyncAIHttpTTSService(TTSService):
encoding: str = "pcm_s16le",
container: str = "raw",
params: Optional[InputParams] = None,
settings: Optional[AsyncAITTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Async TTS service.
@@ -524,13 +522,13 @@ class AsyncAIHttpTTSService(TTSService):
voice_id: ID of the voice to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=AsyncAITTSSettings(voice=...)`` instead.
Use ``settings=AsyncAIHttpTTSService.Settings(voice=...)`` instead.
aiohttp_session: An aiohttp session for making HTTP requests.
model: TTS model to use (e.g., "async_flash_v1.0").
.. deprecated:: 0.0.105
Use ``settings=AsyncAITTSSettings(model=...)`` instead.
Use ``settings=AsyncAIHttpTTSService.Settings(model=...)`` instead.
url: Base URL for Async API.
version: API version string for Async API.
@@ -540,14 +538,14 @@ class AsyncAIHttpTTSService(TTSService):
params: Additional input parameters for voice customization.
.. deprecated:: 0.0.105
Use ``settings=AsyncAITTSSettings(...)`` instead.
Use ``settings=AsyncAIHttpTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to the parent TTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AsyncAITTSSettings(
default_settings = self.Settings(
model="async_flash_v1.0",
voice=None,
language=None,
@@ -555,19 +553,17 @@ class AsyncAIHttpTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", AsyncAITTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if model is not None:
_warn_deprecated_param("model", AsyncAITTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", AsyncAITTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.language = (
self.language_to_service_language(params.language) if params.language else None
)
default_settings.language = params.language
# 4. Apply settings delta (canonical API, always wins)
if settings is not None:

View File

@@ -55,7 +55,7 @@ from pipecat.processors.aggregators.openai_llm_context import (
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.llm_service import LLMService
from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven
from pipecat.utils.tracing.service_decorators import traced_llm
try:
@@ -691,7 +691,7 @@ class AWSBedrockAssistantContextAggregator(LLMAssistantContextAggregator):
frame: The function call result frame to handle.
"""
if frame.result:
result = json.dumps(frame.result)
result = json.dumps(frame.result, ensure_ascii=False)
await self._update_function_call_result(frame.function_name, frame.tool_call_id, result)
else:
await self._update_function_call_result(
@@ -747,7 +747,7 @@ class AWSBedrockLLMService(LLMService):
"""
Settings = AWSBedrockLLMSettings
_settings: AWSBedrockLLMSettings
_settings: Settings
# Overriding the default adapter to use the Anthropic one.
adapter_class = AWSBedrockLLMAdapter
@@ -756,7 +756,7 @@ class AWSBedrockLLMService(LLMService):
"""Input parameters for AWS Bedrock LLM service.
.. deprecated:: 0.0.105
Use ``AWSBedrockLLMSettings`` instead. Pass settings directly via the
Use ``AWSBedrockLLMService.Settings`` instead. Pass settings directly via the
``settings`` parameter of :class:`AWSBedrockLLMService`.
Parameters:
@@ -784,7 +784,7 @@ class AWSBedrockLLMService(LLMService):
aws_session_token: Optional[str] = None,
aws_region: Optional[str] = None,
params: Optional[InputParams] = None,
settings: Optional[AWSBedrockLLMSettings] = None,
settings: Optional[Settings] = None,
stop_sequences: Optional[List[str]] = None,
client_config: Optional[Config] = None,
retry_timeout_secs: Optional[float] = 5.0,
@@ -797,7 +797,7 @@ class AWSBedrockLLMService(LLMService):
model: The AWS Bedrock model identifier to use.
.. deprecated:: 0.0.105
Use ``settings=AWSBedrockLLMSettings(model=...)`` instead.
Use ``settings=AWSBedrockLLMService.Settings(model=...)`` instead.
aws_access_key: AWS access key ID. If None, uses default credentials.
aws_secret_key: AWS secret access key. If None, uses default credentials.
@@ -806,7 +806,7 @@ class AWSBedrockLLMService(LLMService):
params: Model parameters and configuration.
.. deprecated:: 0.0.105
Use ``settings=AWSBedrockLLMSettings(...)`` instead.
Use ``settings=AWSBedrockLLMService.Settings(...)`` instead.
settings: Runtime-updatable settings for this service. When both
deprecated parameters and *settings* are provided, *settings*
@@ -814,7 +814,7 @@ class AWSBedrockLLMService(LLMService):
stop_sequences: List of strings that stop generation.
.. deprecated:: 0.0.105
Use ``settings=AWSBedrockLLMSettings(stop_sequences=...)`` instead.
Use ``settings=AWSBedrockLLMService.Settings(stop_sequences=...)`` instead.
client_config: Custom boto3 client configuration.
retry_timeout_secs: Request timeout in seconds for retry logic.
@@ -822,7 +822,7 @@ class AWSBedrockLLMService(LLMService):
**kwargs: Additional arguments passed to parent LLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AWSBedrockLLMSettings(
default_settings = self.Settings(
model="us.amazon.nova-lite-v1:0",
system_instruction=None,
max_tokens=None,
@@ -841,15 +841,15 @@ class AWSBedrockLLMService(LLMService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", AWSBedrockLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if stop_sequences is not None:
_warn_deprecated_param("stop_sequences", AWSBedrockLLMSettings, "stop_sequences")
self._warn_init_param_moved_to_settings("stop_sequences", "stop_sequences")
default_settings.stop_sequences = stop_sequences
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", AWSBedrockLLMSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.max_tokens = params.max_tokens
default_settings.temperature = params.temperature
@@ -923,7 +923,10 @@ class AWSBedrockLLMService(LLMService):
return inference_config
async def run_inference(
self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None
self,
context: LLMContext | OpenAILLMContext,
max_tokens: Optional[int] = None,
system_instruction: Optional[str] = None,
) -> Optional[str]:
"""Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context.
@@ -931,6 +934,8 @@ class AWSBedrockLLMService(LLMService):
context: The LLM context containing conversation history.
max_tokens: Optional maximum number of tokens to generate. If provided,
overrides the service's default max_tokens setting.
system_instruction: Optional system instruction to use for this inference.
If provided, overrides any system instruction in the context.
Returns:
The LLM's response as a string, or None if no response is generated.
@@ -947,6 +952,15 @@ class AWSBedrockLLMService(LLMService):
messages = context.messages
system = getattr(context, "system", None) # [{"text": "system message"}]
# Override system instruction if provided
if system_instruction is not None:
if system:
logger.warning(
f"{self}: Both system_instruction and a system message in context are set."
" Using system_instruction."
)
system = [{"text": system_instruction}]
# Prepare request parameters using the same method as streaming
inference_config = self._build_inference_config()

View File

@@ -27,8 +27,8 @@ from pydantic import BaseModel, Field
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.adapters.services.aws_nova_sonic_adapter import AWSNovaSonicLLMAdapter, Role
from pipecat.frames.frames import (
AggregatedTextFrame,
AggregationType,
BotStoppedSpeakingFrame,
CancelFrame,
EndFrame,
Frame,
@@ -60,7 +60,7 @@ from pipecat.processors.aggregators.openai_llm_context import (
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.llm_service import LLMService
from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven
from pipecat.utils.time import time_now_iso8601
try:
@@ -150,7 +150,7 @@ class Params(BaseModel):
"""Configuration parameters for AWS Nova Sonic.
.. deprecated:: 0.0.105
Use ``settings=AWSNovaSonicLLMSettings(...)`` for inference settings
Use ``settings=AWSNovaSonicLLMService.Settings(...)`` for inference settings
and ``audio_config=AudioConfig(...)`` for audio configuration.
Parameters:
@@ -247,7 +247,7 @@ class AWSNovaSonicLLMService(LLMService):
"""
Settings = AWSNovaSonicLLMSettings
_settings: AWSNovaSonicLLMSettings
_settings: Settings
# Override the default adapter to use the AWSNovaSonicLLMAdapter one
adapter_class = AWSNovaSonicLLMAdapter
@@ -263,7 +263,7 @@ class AWSNovaSonicLLMService(LLMService):
voice_id: str = "matthew",
params: Optional[Params] = None,
audio_config: Optional[AudioConfig] = None,
settings: Optional[AWSNovaSonicLLMSettings] = None,
settings: Optional[Settings] = None,
system_instruction: Optional[str] = None,
tools: Optional[ToolsSchema] = None,
send_transcription_frames: bool = True,
@@ -282,7 +282,7 @@ class AWSNovaSonicLLMService(LLMService):
model: Model identifier. Defaults to "amazon.nova-2-sonic-v1:0".
.. deprecated:: 0.0.105
Use ``settings=AWSNovaSonicLLMSettings(model=...)`` instead.
Use ``settings=AWSNovaSonicLLMService.Settings(model=...)`` instead.
voice_id: Voice ID for speech synthesis.
Note that some voices are designed for use with a specific language.
@@ -291,12 +291,12 @@ class AWSNovaSonicLLMService(LLMService):
- Nova Sonic (the older model): see https://docs.aws.amazon.com/nova/latest/userguide/available-voices.html.
.. deprecated:: 0.0.105
Use ``settings=AWSNovaSonicLLMSettings(voice=...)`` instead.
Use ``settings=AWSNovaSonicLLMService.Settings(voice=...)`` instead.
params: Model parameters for audio configuration and inference.
.. deprecated:: 0.0.105
Use ``settings=AWSNovaSonicLLMSettings(...)`` for inference
Use ``settings=AWSNovaSonicLLMService.Settings(...)`` for inference
settings and ``audio_config=AudioConfig(...)`` for audio
configuration.
@@ -308,7 +308,7 @@ class AWSNovaSonicLLMService(LLMService):
system_instruction: System-level instruction for the model.
.. deprecated:: 0.0.105
Use ``settings=AWSNovaSonicLLMSettings(system_instruction=...)`` instead.
Use ``settings=AWSNovaSonicLLMService.Settings(system_instruction=...)`` instead.
tools: Available tools/functions for the model to use.
send_transcription_frames: Whether to emit transcription frames.
@@ -319,7 +319,7 @@ class AWSNovaSonicLLMService(LLMService):
**kwargs: Additional arguments passed to the parent LLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AWSNovaSonicLLMSettings(
default_settings = self.Settings(
model="amazon.nova-2-sonic-v1:0",
system_instruction=None,
voice="matthew",
@@ -337,15 +337,13 @@ class AWSNovaSonicLLMService(LLMService):
# 2. Apply direct init arg overrides (deprecated)
if model != "amazon.nova-2-sonic-v1:0":
_warn_deprecated_param("model", AWSNovaSonicLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if voice_id != "matthew":
_warn_deprecated_param("voice_id", AWSNovaSonicLLMSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if system_instruction is not None:
_warn_deprecated_param(
"system_instruction", AWSNovaSonicLLMSettings, "system_instruction"
)
self._warn_init_param_moved_to_settings("system_instruction", "system_instruction")
default_settings.system_instruction = system_instruction
# 3. Apply params overrides — only if settings not provided
@@ -356,7 +354,7 @@ class AWSNovaSonicLLMService(LLMService):
warnings.simplefilter("always")
warnings.warn(
"The `params` parameter is deprecated. "
"Use `settings=AWSNovaSonicLLMSettings(...)` for inference settings "
"Use `settings=self.Settings(...)` for inference settings "
"(temperature, max_tokens, top_p, endpointing_sensitivity) "
"and `audio_config=AudioConfig(...)` for audio configuration "
"(sample rates, sample sizes, channel counts).",
@@ -426,18 +424,16 @@ class AWSNovaSonicLLMService(LLMService):
self._input_audio_content_name: Optional[str] = None
self._content_being_received: Optional[CurrentContent] = None
self._assistant_is_responding = False
self._may_need_repush_assistant_text = False
self._ready_to_send_context = False
self._handling_bot_stopped_speaking = False
self._triggering_assistant_response = False
self._waiting_for_trigger_transcription = False
self._disconnecting = False
self._connected_time: Optional[float] = None
self._wants_connection = False
self._user_text_buffer = ""
self._assistant_text_buffer = ""
self._completed_tool_calls = set()
self._audio_input_started = False
self._pending_speculative_text: Optional[str] = None
file_path = files("pipecat.services.aws.nova_sonic").joinpath("ready.wav")
with wave.open(file_path.open("rb"), "rb") as wav_file:
@@ -447,7 +443,7 @@ class AWSNovaSonicLLMService(LLMService):
# settings
#
async def _update_settings(self, delta: AWSNovaSonicLLMSettings) -> dict[str, Any]:
async def _update_settings(self, delta: Settings) -> dict[str, Any]:
"""Apply a settings delta.
Settings are stored but not applied to the active connection.
@@ -507,11 +503,13 @@ class AWSNovaSonicLLMService(LLMService):
async def reset_conversation(self):
"""Reset the conversation state while preserving context.
Handles bot stopped speaking event, disconnects from the service,
and reconnects with the preserved context.
Cleans up any in-progress assistant response, disconnects from the
service, and reconnects with the preserved context.
"""
logger.debug("Resetting conversation")
await self._handle_bot_stopped_speaking(delay_to_catch_trailing_assistant_text=False)
if self._assistant_is_responding:
self._assistant_is_responding = False
await self._report_assistant_response_ended()
# Grab context to carry through disconnect/reconnect
context = self._context
@@ -542,8 +540,6 @@ class AWSNovaSonicLLMService(LLMService):
await self._handle_context(context)
elif isinstance(frame, InputAudioRawFrame):
await self._handle_input_audio_frame(frame)
elif isinstance(frame, BotStoppedSpeakingFrame):
await self._handle_bot_stopped_speaking(delay_to_catch_trailing_assistant_text=True)
elif isinstance(frame, InterruptionFrame):
await self._handle_interruption_frame()
@@ -571,49 +567,8 @@ class AWSNovaSonicLLMService(LLMService):
await self._send_user_audio_event(frame.audio)
async def _handle_bot_stopped_speaking(self, delay_to_catch_trailing_assistant_text: bool):
# Protect against back-to-back BotStoppedSpeaking calls, which I've observed
if self._handling_bot_stopped_speaking:
return
self._handling_bot_stopped_speaking = True
async def finalize_assistant_response():
if self._assistant_is_responding:
# Consider the assistant finished with their response (possibly after a short delay,
# to allow for any trailing FINAL assistant text block to come in that need to make
# it into context).
#
# TODO: ideally we could base this solely on the LLM output events, but I couldn't
# figure out a reliable way to determine when we've gotten our last FINAL text block
# after the LLM is done talking.
#
# First I looked at stopReason, but it doesn't seem like the last FINAL text block
# is reliably marked END_TURN (sometimes the *first* one is, but not the last...
# bug?)
#
# Then I considered schemes where we tally or match up SPECULATIVE text blocks with
# FINAL text blocks to know how many or which FINAL blocks to expect, but user
# interruptions throw a wrench in these schemes: depending on the exact timing of
# the interruption, we should or shouldn't expect some FINAL blocks.
if delay_to_catch_trailing_assistant_text:
# This delay length is a balancing act between "catching" trailing assistant
# text that is quite delayed but not waiting so long that user text comes in
# first and results in a bit of context message order scrambling.
await asyncio.sleep(1.25)
self._assistant_is_responding = False
await self._report_assistant_response_ended()
self._handling_bot_stopped_speaking = False
# Finalize the assistant response, either now or after a delay
if delay_to_catch_trailing_assistant_text:
self.create_task(finalize_assistant_response())
else:
await finalize_assistant_response()
async def _handle_interruption_frame(self):
if self._assistant_is_responding:
self._may_need_repush_assistant_text = True
pass
#
# LLM communication: lifecycle
@@ -773,17 +728,15 @@ class AWSNovaSonicLLMService(LLMService):
self._input_audio_content_name = None
self._content_being_received = None
self._assistant_is_responding = False
self._may_need_repush_assistant_text = False
self._ready_to_send_context = False
self._handling_bot_stopped_speaking = False
self._triggering_assistant_response = False
self._waiting_for_trigger_transcription = False
self._disconnecting = False
self._connected_time = None
self._user_text_buffer = ""
self._assistant_text_buffer = ""
self._completed_tool_calls = set()
self._audio_input_started = False
self._pending_speculative_text = None
logger.info("Finished disconnecting")
except Exception as e:
@@ -1046,7 +999,9 @@ class AWSNovaSonicLLMService(LLMService):
"toolResult": {
"promptName": self._prompt_name,
"contentName": content_name,
"content": json.dumps(result) if isinstance(result, dict) else result,
"content": json.dumps(result, ensure_ascii=False)
if isinstance(result, dict)
else result,
}
}
}
@@ -1153,10 +1108,11 @@ class AWSNovaSonicLLMService(LLMService):
self._content_being_received = content
if content.role == Role.ASSISTANT:
if content.type == ContentType.AUDIO:
# Note that an assistant response can comprise of multiple audio blocks
if not self._assistant_is_responding:
# The assistant has started responding.
if content.type == ContentType.TEXT:
if (
content.text_stage == TextStage.SPECULATIVE
and not self._assistant_is_responding
):
self._assistant_is_responding = True
await self._report_user_transcription_ended() # Consider user turn over
await self._report_assistant_response_started()
@@ -1232,18 +1188,30 @@ class AWSNovaSonicLLMService(LLMService):
if content.role == Role.ASSISTANT:
if content.type == ContentType.TEXT:
# Ignore non-final text, and the "interrupted" message (which isn't meaningful text)
if content.text_stage == TextStage.FINAL and stop_reason != "INTERRUPTED":
if self._assistant_is_responding:
# Text added to the ongoing assistant response
await self._report_assistant_response_text_added(content.text_content)
if stop_reason != "INTERRUPTED":
if content.text_stage == TextStage.SPECULATIVE:
await self._report_llm_text(content.text_content)
elif self._assistant_is_responding:
# TEXT INTERRUPTED with no audio means the user interrupted
# before audio started. End the response here since no AUDIO
# contentEnd will arrive.
self._assistant_is_responding = False
await self._report_assistant_response_ended()
elif content.type == ContentType.AUDIO:
# Emit deferred TTSTextFrame after all audio chunks have been sent
await self._report_tts_text()
if stop_reason in ("END_TURN", "INTERRUPTED"):
# END_TURN: normal completion. INTERRUPTED: user interrupted
# mid-audio. Both mean no more audio for this turn.
self._assistant_is_responding = False
await self._report_assistant_response_ended()
elif content.role == Role.USER:
if content.type == ContentType.TEXT:
if content.text_stage == TextStage.FINAL:
# User transcription text added
await self._report_user_transcription_text_added(content.text_content)
async def _handle_completion_end_event(self, event_json):
async def _handle_completion_end_event(self, _):
pass
#
@@ -1256,29 +1224,40 @@ class AWSNovaSonicLLMService(LLMService):
async def _report_assistant_response_started(self):
logger.debug("Assistant response started")
# Report the start of the assistant response.
await self.push_frame(LLMFullResponseStartFrame())
# Report that equivalent of TTS (this is a speech-to-speech model) started
await self.push_frame(TTSStartedFrame())
async def _report_assistant_response_text_added(self, text):
if not self._context: # should never happen
return
async def _report_llm_text(self, text):
"""Push speculative assistant text and defer TTSTextFrame.
logger.debug(f"Assistant response text added: {text}")
Speculative text arrives before each audio chunk, providing real-time
text that is synchronized with what the bot is saying. LLMTextFrame and
AggregatedTextFrame are pushed immediately for real-time text display.
TTSTextFrame emission is deferred to audio contentEnd so it aligns with
audio playout timing.
"""
logger.debug(f"Assistant speculative text: {text}")
# Report the text of the assistant response.
await self._push_assistant_response_text_frames(text)
llm_text_frame = LLMTextFrame(text)
llm_text_frame.append_to_context = False
await self.push_frame(llm_text_frame)
# HACK: here we're also buffering the assistant text ourselves as a
# backup rather than relying solely on the assistant context aggregator
# to do it, because the text arrives from Nova Sonic only after all the
# assistant audio frames have been pushed, meaning that if an
# interruption frame were to arrive we would lose all of it (the text
# frames sitting in the queue would be wiped).
self._assistant_text_buffer += text
aggregated_text_frame = AggregatedTextFrame(text, aggregated_by=AggregationType.SENTENCE)
aggregated_text_frame.append_to_context = False
await self.push_frame(aggregated_text_frame)
self._pending_speculative_text = text
async def _report_tts_text(self):
if self._pending_speculative_text:
tts_text_frame = TTSTextFrame(
self._pending_speculative_text, aggregated_by=AggregationType.SENTENCE
)
tts_text_frame.includes_inter_frame_spaces = True
await self.push_frame(tts_text_frame)
self._pending_speculative_text = None
async def _report_assistant_response_ended(self):
if not self._context: # should never happen
@@ -1286,54 +1265,12 @@ class AWSNovaSonicLLMService(LLMService):
logger.debug("Assistant response ended")
# If an interruption frame arrived while the assistant was responding
# we may have lost all of the assistant text (see HACK, above), so
# re-push it downstream to the aggregator now.
if self._may_need_repush_assistant_text:
# Just in case, check that assistant text hasn't already made it
# into the context (sometimes it does, despite the interruption).
messages = self._context.get_messages()
last_message = messages[-1] if messages else None
if (
not last_message
or last_message.get("role") != "assistant"
or last_message.get("content") != self._assistant_text_buffer
):
# We also need to re-push the LLMFullResponseStartFrame since the
# TTSTextFrame would be ignored otherwise (the interruption frame
# would have cleared the assistant aggregator state).
await self.push_frame(LLMFullResponseStartFrame())
await self._push_assistant_response_text_frames(self._assistant_text_buffer)
self._may_need_repush_assistant_text = False
# Report the end of the assistant response.
await self.push_frame(LLMFullResponseEndFrame())
# Report that equivalent of TTS (this is a speech-to-speech model) stopped.
await self.push_frame(TTSStoppedFrame())
# Clear out the buffered assistant text
self._assistant_text_buffer = ""
async def _push_assistant_response_text_frames(self, text: str):
# In a typical "cascade" LLM + TTS setup, LLMTextFrames would not
# proceed beyond the TTS service. Therefore, since a speech-to-speech
# service like Nova Sonic combines both LLM and TTS functionality, you
# would think we wouldn't need to push LLMTextFrames at all. However,
# RTVI relies on LLMTextFrames being pushed to trigger its
# "bot-llm-text" event. So here we push an LLMTextFrame, too, but avoid
# appending it to context to avoid context message duplication.
# Push LLMTextFrame
llm_text_frame = LLMTextFrame(text)
llm_text_frame.append_to_context = False
await self.push_frame(llm_text_frame)
# Push TTSTextFrame
tts_text_frame = TTSTextFrame(text, aggregated_by=AggregationType.SENTENCE)
tts_text_frame.includes_inter_frame_spaces = True
await self.push_frame(tts_text_frame)
#
# user transcription reporting
#
@@ -1363,6 +1300,12 @@ class AWSNovaSonicLLMService(LLMService):
if not self._context: # should never happen
return
# Nothing to report if no user speech was transcribed (e.g. the prompt
# was text-only, which is the case on the first user turn when the bot
# starts the conversation).
if not self._user_text_buffer:
return
logger.debug(f"User transcription ended")
# Report to the upstream user context aggregator that some new user

View File

@@ -29,7 +29,7 @@ from pipecat.frames.frames import (
TranscriptionFrame,
)
from pipecat.services.aws.utils import build_event_message, decode_event, get_presigned_url
from pipecat.services.settings import STTSettings, _warn_deprecated_param
from pipecat.services.settings import STTSettings
from pipecat.services.stt_latency import AWS_TRANSCRIBE_TTFS_P99
from pipecat.services.stt_service import WebsocketSTTService
from pipecat.transcriptions.language import Language, resolve_language
@@ -61,7 +61,7 @@ class AWSTranscribeSTTService(WebsocketSTTService):
"""
Settings = AWSTranscribeSTTSettings
_settings: AWSTranscribeSTTSettings
_settings: Settings
def __init__(
self,
@@ -72,7 +72,7 @@ class AWSTranscribeSTTService(WebsocketSTTService):
region: Optional[str] = None,
sample_rate: Optional[int] = None,
language: Optional[Language] = None,
settings: Optional[AWSTranscribeSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = AWS_TRANSCRIBE_TTFS_P99,
**kwargs,
):
@@ -89,7 +89,7 @@ class AWSTranscribeSTTService(WebsocketSTTService):
language: Language for transcription.
.. deprecated:: 0.0.105
Use ``settings=AWSTranscribeSTTSettings(language=...)`` instead.
Use ``settings=AWSTranscribeSTTService.Settings(language=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -98,15 +98,15 @@ class AWSTranscribeSTTService(WebsocketSTTService):
**kwargs: Additional arguments passed to parent STTService class.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AWSTranscribeSTTSettings(
default_settings = self.Settings(
model=None,
language=self.language_to_service_language(Language.EN),
language=Language.EN,
)
# 2. Apply direct init arg overrides (deprecated)
if language is not None:
_warn_deprecated_param("language", AWSTranscribeSTTSettings, "language")
default_settings.language = self.language_to_service_language(language)
self._warn_init_param_moved_to_settings("language", "language")
default_settings.language = language
# 3. (No step 3, as there's no params object to apply)

View File

@@ -23,7 +23,7 @@ from pipecat.frames.frames import (
Frame,
TTSAudioRawFrame,
)
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import TTSService
from pipecat.transcriptions.language import Language, resolve_language
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -149,13 +149,13 @@ class AWSPollyTTSService(TTSService):
"""
Settings = AWSPollyTTSSettings
_settings: AWSPollyTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for AWS Polly TTS configuration.
.. deprecated:: 0.0.105
Use ``AWSPollyTTSSettings`` directly via the ``settings`` parameter instead.
Use ``AWSPollyTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
engine: TTS engine to use ('standard', 'neural', etc.).
@@ -183,7 +183,7 @@ class AWSPollyTTSService(TTSService):
voice_id: Optional[str] = None,
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
settings: Optional[AWSPollyTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initializes the AWS Polly TTS service.
@@ -196,20 +196,20 @@ class AWSPollyTTSService(TTSService):
voice_id: Voice ID to use for synthesis. Defaults to 'Joanna'.
.. deprecated:: 0.0.105
Use ``settings=AWSPollyTTSSettings(voice=...)`` instead.
Use ``settings=AWSPollyTTSService.Settings(voice=...)`` instead.
sample_rate: Audio sample rate. If None, uses service default.
params: Additional input parameters for voice customization.
.. deprecated:: 0.0.105
Use ``settings=AWSPollyTTSSettings(...)`` instead.
Use ``settings=AWSPollyTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to parent TTSService class.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AWSPollyTTSSettings(
default_settings = self.Settings(
model=None,
voice="Joanna",
language="en-US",
@@ -222,19 +222,15 @@ class AWSPollyTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", AWSPollyTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", AWSPollyTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.engine = params.engine
default_settings.language = (
self.language_to_service_language(params.language)
if params.language
else "en-US"
)
default_settings.language = params.language if params.language else "en-US"
default_settings.pitch = params.pitch
default_settings.rate = params.rate
default_settings.volume = params.volume

View File

@@ -20,7 +20,7 @@ from PIL import Image
from pipecat.frames.frames import ErrorFrame, Frame, URLImageRawFrame
from pipecat.services.image_service import ImageGenService
from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven
@dataclass
@@ -44,7 +44,7 @@ class AzureImageGenServiceREST(ImageGenService):
"""
Settings = AzureImageGenSettings
_settings: AzureImageGenSettings
_settings: Settings
def __init__(
self,
@@ -55,7 +55,7 @@ class AzureImageGenServiceREST(ImageGenService):
model: Optional[str] = None,
aiohttp_session: aiohttp.ClientSession,
api_version="2023-06-01-preview",
settings: Optional[AzureImageGenSettings] = None,
settings: Optional[Settings] = None,
):
"""Initialize the AzureImageGenServiceREST.
@@ -63,14 +63,14 @@ class AzureImageGenServiceREST(ImageGenService):
image_size: Size specification for generated images (e.g., "1024x1024").
.. deprecated:: 0.0.105
Use ``settings=AzureImageGenSettings(image_size=...)`` instead.
Use ``settings=AzureImageGenServiceREST.Settings(image_size=...)`` instead.
api_key: Azure OpenAI API key for authentication.
endpoint: Azure OpenAI endpoint URL.
model: The image generation model to use.
.. deprecated:: 0.0.105
Use ``settings=AzureImageGenSettings(model=...)`` instead.
Use ``settings=AzureImageGenServiceREST.Settings(model=...)`` instead.
aiohttp_session: Shared aiohttp session for HTTP requests.
api_version: Azure API version string. Defaults to "2023-06-01-preview".
@@ -78,18 +78,18 @@ class AzureImageGenServiceREST(ImageGenService):
parameters, ``settings`` values take precedence.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AzureImageGenSettings(
default_settings = self.Settings(
model=None,
image_size=None,
)
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", AzureImageGenSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if image_size is not None:
_warn_deprecated_param("image_size", AzureImageGenSettings, "image_size")
self._warn_init_param_moved_to_settings("image_size", "image_size")
default_settings.image_size = image_size
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -12,13 +12,12 @@ from typing import Optional
from loguru import logger
from openai import AsyncAzureOpenAI
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class AzureLLMSettings(OpenAILLMSettings):
class AzureLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for AzureLLMService."""
pass
@@ -40,7 +39,7 @@ class AzureLLMService(OpenAILLMService):
endpoint: str,
model: Optional[str] = None,
api_version: str = "2024-09-01-preview",
settings: Optional[AzureLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Azure LLM service.
@@ -48,10 +47,10 @@ class AzureLLMService(OpenAILLMService):
Args:
api_key: The API key for accessing Azure OpenAI.
endpoint: The Azure endpoint URL.
model: The model identifier to use. Defaults to "gpt-4o".
model: The model identifier to use. Defaults to "gpt-4.1".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=AzureLLMService.Settings(model=...)`` instead.
api_version: Azure API version. Defaults to "2024-09-01-preview".
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -59,11 +58,11 @@ class AzureLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AzureLLMSettings(model="gpt-4o")
default_settings = self.Settings(model="gpt-4.1")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", AzureLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)

View File

@@ -10,7 +10,7 @@ from dataclasses import dataclass
from loguru import logger
from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMService, OpenAIRealtimeLLMSettings
from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMService
try:
from websockets.asyncio.client import connect as websocket_connect
@@ -21,7 +21,7 @@ except ModuleNotFoundError as e:
@dataclass
class AzureRealtimeLLMSettings(OpenAIRealtimeLLMSettings):
class AzureRealtimeLLMSettings(OpenAIRealtimeLLMService.Settings):
"""Settings for AzureRealtimeLLMService."""
pass
@@ -36,7 +36,7 @@ class AzureRealtimeLLMService(OpenAIRealtimeLLMService):
"""
Settings = AzureRealtimeLLMSettings
_settings: AzureRealtimeLLMSettings
_settings: Settings
def __init__(
self,

View File

@@ -26,7 +26,7 @@ from pipecat.frames.frames import (
TranscriptionFrame,
)
from pipecat.services.azure.common import language_to_azure_language
from pipecat.services.settings import STTSettings, _warn_deprecated_param
from pipecat.services.settings import STTSettings
from pipecat.services.stt_latency import AZURE_TTFS_P99
from pipecat.services.stt_service import STTService
from pipecat.transcriptions.language import Language
@@ -67,18 +67,18 @@ class AzureSTTService(STTService):
"""
Settings = AzureSTTSettings
_settings: AzureSTTSettings
_settings: Settings
def __init__(
self,
*,
api_key: str,
region: str,
region: Optional[str] = None,
language: Optional[Language] = Language.EN_US,
sample_rate: Optional[int] = None,
private_endpoint: Optional[str] = None,
endpoint_id: Optional[str] = None,
settings: Optional[AzureSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = AZURE_TTFS_P99,
**kwargs,
):
@@ -87,10 +87,11 @@ class AzureSTTService(STTService):
Args:
api_key: Azure Cognitive Services subscription key.
region: Azure region for the Speech service (e.g., 'eastus').
Required unless ``private_endpoint`` is provided.
language: Language for speech recognition. Defaults to English (US).
.. deprecated:: 0.0.105
Use ``settings=AzureSTTSettings(language=...)`` instead.
Use ``settings=AzureSTTService.Settings(language=...)`` instead.
sample_rate: Audio sample rate in Hz. If None, uses service default.
private_endpoint: Private endpoint for STT behind firewall.
@@ -103,15 +104,15 @@ class AzureSTTService(STTService):
**kwargs: Additional arguments passed to parent STTService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AzureSTTSettings(
default_settings = self.Settings(
model=None,
language=language_to_azure_language(Language.EN_US),
language=Language.EN_US,
)
# 2. Apply direct init arg overrides (deprecated)
if language is not None and language != Language.EN_US:
_warn_deprecated_param("language", AzureSTTSettings, "language")
default_settings.language = language_to_azure_language(language)
self._warn_init_param_moved_to_settings("language", "language")
default_settings.language = language
# 3. (No step 3, as there's no params object to apply)
@@ -126,21 +127,29 @@ class AzureSTTService(STTService):
**kwargs,
)
speech_config_kwargs: dict[str, Any] = {
"subscription": api_key,
"speech_recognition_language": default_settings.language
or language_to_azure_language(Language.EN_US),
}
recognition_language = default_settings.language or language_to_azure_language(
Language.EN_US
)
if not region and not private_endpoint:
raise ValueError("Either 'region' or 'private_endpoint' must be provided.")
if private_endpoint:
if region:
logger.warning(
"Both 'region' and 'private_endpoint' provided; 'region' will be ignored."
)
speech_config_kwargs["endpoint"] = private_endpoint
self._speech_config = SpeechConfig(
subscription=api_key,
endpoint=private_endpoint,
speech_recognition_language=recognition_language,
)
else:
speech_config_kwargs["region"] = region
self._speech_config = SpeechConfig(**speech_config_kwargs)
self._speech_config = SpeechConfig(
subscription=api_key,
region=region,
speech_recognition_language=recognition_language,
)
if endpoint_id:
self._speech_config.endpoint_id = endpoint_id

View File

@@ -25,7 +25,7 @@ from pipecat.frames.frames import (
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.azure.common import language_to_azure_language
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import TextAggregationMode, TTSService
from pipecat.transcriptions.language import Language
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -97,7 +97,8 @@ class AzureBaseTTSService:
This is a mixin class and should be used alongside TTSService or its subclasses.
"""
_settings: AzureTTSSettings
Settings = AzureTTSSettings
_settings: Settings
# Define SSML escape mappings based on SSML reserved characters
# See - https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-structure
@@ -113,7 +114,7 @@ class AzureBaseTTSService:
"""Input parameters for Azure TTS voice configuration.
.. deprecated:: 0.0.105
Use ``settings=AzureTTSSettings(...)`` instead.
Use ``settings=AzureBaseTTSService.Settings(...)`` instead.
Parameters:
emphasis: Emphasis level for speech ("strong", "moderate", "reduced").
@@ -256,7 +257,7 @@ class AzureTTSService(TTSService, AzureBaseTTSService):
voice: Optional[str] = None,
sample_rate: Optional[int] = None,
params: Optional[AzureBaseTTSService.InputParams] = None,
settings: Optional[AzureTTSSettings] = None,
settings: Optional[Settings] = None,
aggregate_sentences: Optional[bool] = None,
text_aggregation_mode: Optional[TextAggregationMode] = None,
**kwargs,
@@ -269,13 +270,13 @@ class AzureTTSService(TTSService, AzureBaseTTSService):
voice: Voice name to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=AzureTTSSettings(voice=...)`` instead.
Use ``settings=AzureTTSService.Settings(voice=...)`` instead.
sample_rate: Audio sample rate in Hz. If None, uses service default.
params: Voice and synthesis parameters configuration.
.. deprecated:: 0.0.105
Use ``settings=AzureTTSSettings(...)`` instead.
Use ``settings=AzureTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -288,7 +289,7 @@ class AzureTTSService(TTSService, AzureBaseTTSService):
**kwargs: Additional arguments passed to parent WordTTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AzureTTSSettings(
default_settings = self.Settings(
model=None,
voice="en-US-SaraNeural",
language="en-US",
@@ -303,19 +304,15 @@ class AzureTTSService(TTSService, AzureBaseTTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice is not None:
_warn_deprecated_param("voice", AzureTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice", "voice")
default_settings.voice = voice
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", AzureTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.emphasis = params.emphasis
default_settings.language = (
self.language_to_service_language(params.language)
if params.language
else "en-US"
)
default_settings.language = params.language if params.language else "en-US"
default_settings.pitch = params.pitch
default_settings.rate = params.rate
default_settings.role = params.role
@@ -761,7 +758,7 @@ class AzureHttpTTSService(TTSService, AzureBaseTTSService):
voice: Optional[str] = None,
sample_rate: Optional[int] = None,
params: Optional[AzureBaseTTSService.InputParams] = None,
settings: Optional[AzureTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Azure HTTP TTS service.
@@ -772,20 +769,20 @@ class AzureHttpTTSService(TTSService, AzureBaseTTSService):
voice: Voice name to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=AzureTTSSettings(voice=...)`` instead.
Use ``settings=AzureHttpTTSService.Settings(voice=...)`` instead.
sample_rate: Audio sample rate in Hz. If None, uses service default.
params: Voice and synthesis parameters configuration.
.. deprecated:: 0.0.105
Use ``settings=AzureTTSSettings(...)`` instead.
Use ``settings=AzureHttpTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to parent TTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AzureTTSSettings(
default_settings = self.Settings(
model=None,
voice="en-US-SaraNeural",
language="en-US",
@@ -800,19 +797,15 @@ class AzureHttpTTSService(TTSService, AzureBaseTTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice is not None:
_warn_deprecated_param("voice", AzureTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice", "voice")
default_settings.voice = voice
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", AzureTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.emphasis = params.emphasis
default_settings.language = (
self.language_to_service_language(params.language)
if params.language
else "en-US"
)
default_settings.language = params.language if params.language else "en-US"
default_settings.pitch = params.pitch
default_settings.rate = params.rate
default_settings.role = params.role

View File

@@ -30,7 +30,7 @@ from pipecat.frames.frames import (
StartFrame,
TTSAudioRawFrame,
)
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import TTSService
from pipecat.transcriptions.language import Language, resolve_language
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -138,10 +138,13 @@ class CambTTSSettings(TTSSettings):
"""Settings for CambTTSService.
Parameters:
voice: Camb.ai voice ID. Overrides ``TTSSettings.voice`` (str) because
Camb.ai uses integer voice IDs.
user_instructions: Custom instructions for mars-instruct model only.
Ignored for other models. Max 1000 characters.
"""
voice: int | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
user_instructions: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
@@ -158,24 +161,31 @@ class CambTTSService(TTSService):
Example::
# Basic usage with mars-flash (fast)
tts = CambTTSService(api_key="your-api-key", model="mars-flash")
tts = CambTTSService(
api_key="your-api-key",
settings=CambTTSService.Settings(
model="mars-flash"
)
)
# High quality with mars-pro
tts = CambTTSService(
api_key="your-api-key",
voice_id=12345,
model="mars-pro",
settings=CambTTSService.Settings(
voice=12345,
model="mars-pro",
)
)
"""
Settings = CambTTSSettings
_settings: CambTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Camb.ai TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=CambTTSSettings(...)`` instead.
Use ``settings=CambTTSService.Settings(...)`` instead.
Parameters:
language: Language for synthesis (BCP-47 format). Defaults to English.
@@ -200,7 +210,7 @@ class CambTTSService(TTSService):
timeout: float = 60.0,
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
settings: Optional[CambTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Camb.ai TTS service.
@@ -210,12 +220,12 @@ class CambTTSService(TTSService):
voice_id: Voice ID to use.
.. deprecated:: 0.0.105
Use ``settings=CambTTSSettings(voice=...)`` instead.
Use ``settings=CambTTSService.Settings(voice=...)`` instead.
model: TTS model to use. Options: "mars-flash" (fast), "mars-pro" (high quality).
.. deprecated:: 0.0.105
Use ``settings=CambTTSSettings(model=...)`` instead.
Use ``settings=CambTTSService.Settings(model=...)`` instead.
timeout: Request timeout in seconds. Defaults to 60.0 (minimum recommended
by Camb.ai).
@@ -223,14 +233,14 @@ class CambTTSService(TTSService):
params: Additional voice parameters. If None, uses defaults.
.. deprecated:: 0.0.105
Use ``settings=CambTTSSettings(...)`` instead.
Use ``settings=CambTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to parent TTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = CambTTSSettings(
default_settings = self.Settings(
model="mars-flash",
voice=147320,
language="en-us",
@@ -239,20 +249,18 @@ class CambTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", CambTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if voice_id is not None:
_warn_deprecated_param("voice_id", CambTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", CambTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = (
self.language_to_service_language(params.language) or "en-us"
)
default_settings.language = params.language
if params.user_instructions is not None:
default_settings.user_instructions = params.user_instructions

View File

@@ -28,7 +28,7 @@ from pipecat.frames.frames import (
VADUserStoppedSpeakingFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import STTSettings, _warn_deprecated_param
from pipecat.services.settings import STTSettings
from pipecat.services.stt_latency import CARTESIA_TTFS_P99
from pipecat.services.stt_service import WebsocketSTTService
from pipecat.transcriptions.language import Language
@@ -55,7 +55,7 @@ class CartesiaLiveOptions:
"""Configuration options for Cartesia Live STT service.
.. deprecated:: 0.0.105
Use ``settings=CartesiaSTTSettings(...)`` for model/language and
Use ``settings=CartesiaSTTService.Settings(...)`` for model/language and
direct ``__init__`` parameters for encoding/sample_rate instead.
"""
@@ -147,7 +147,7 @@ class CartesiaSTTService(WebsocketSTTService):
"""
Settings = CartesiaSTTSettings
_settings: CartesiaSTTSettings
_settings: Settings
def __init__(
self,
@@ -157,7 +157,7 @@ class CartesiaSTTService(WebsocketSTTService):
encoding: str = "pcm_s16le",
sample_rate: Optional[int] = None,
live_options: Optional[CartesiaLiveOptions] = None,
settings: Optional[CartesiaSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = CARTESIA_TTFS_P99,
**kwargs,
):
@@ -172,7 +172,7 @@ class CartesiaSTTService(WebsocketSTTService):
live_options: Configuration options for transcription service.
.. deprecated:: 0.0.105
Use ``settings=CartesiaSTTSettings(...)`` for model/language
Use ``settings=CartesiaSTTService.Settings(...)`` for model/language
and direct init parameters for encoding/sample_rate instead.
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -182,14 +182,14 @@ class CartesiaSTTService(WebsocketSTTService):
**kwargs: Additional arguments passed to parent STTService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = CartesiaSTTSettings(
default_settings = self.Settings(
model="ink-whisper",
language=Language.EN.value,
)
# 2. Apply live_options overrides — only if settings not provided
if live_options is not None:
_warn_deprecated_param("live_options", CartesiaSTTSettings)
self._warn_init_param_moved_to_settings("live_options")
if not settings:
if live_options.sample_rate and sample_rate is None:
sample_rate = live_options.sample_rate
@@ -313,7 +313,7 @@ class CartesiaSTTService(WebsocketSTTService):
"""Apply a settings delta.
Args:
delta: A :class:`STTSettings` (or ``CartesiaSTTSettings``) delta.
delta: A :class:`STTSettings` (or ``CartesiaSTTService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.

View File

@@ -25,7 +25,7 @@ from pipecat.frames.frames import (
TTSAudioRawFrame,
TTSStoppedFrame,
)
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import TextAggregationMode, TTSService, WebsocketTTSService
from pipecat.transcriptions.language import Language, resolve_language
from pipecat.utils.text.base_text_aggregator import BaseTextAggregator
@@ -211,7 +211,7 @@ class CartesiaTTSService(WebsocketTTSService):
"""
Settings = CartesiaTTSSettings
_settings: CartesiaTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Cartesia TTS configuration.
@@ -239,7 +239,7 @@ class CartesiaTTSService(WebsocketTTSService):
encoding: str = "pcm_s16le",
container: str = "raw",
params: Optional[InputParams] = None,
settings: Optional[CartesiaTTSSettings] = None,
settings: Optional[Settings] = None,
text_aggregator: Optional[BaseTextAggregator] = None,
text_aggregation_mode: Optional[TextAggregationMode] = None,
aggregate_sentences: Optional[bool] = None,
@@ -252,14 +252,14 @@ class CartesiaTTSService(WebsocketTTSService):
voice_id: ID of the voice to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=CartesiaTTSSettings(voice=...)`` instead.
Use ``settings=CartesiaTTSService.Settings(voice=...)`` instead.
cartesia_version: API version string for Cartesia service.
url: WebSocket URL for Cartesia TTS API.
model: TTS model to use (e.g., "sonic-3").
.. deprecated:: 0.0.105
Use ``settings=CartesiaTTSSettings(model=...)`` instead.
Use ``settings=CartesiaTTSService.Settings(model=...)`` instead.
sample_rate: Audio sample rate. If None, uses default.
encoding: Audio encoding format.
@@ -267,7 +267,7 @@ class CartesiaTTSService(WebsocketTTSService):
params: Additional input parameters for voice customization.
.. deprecated:: 0.0.105
Use ``settings=CartesiaTTSSettings(...)`` instead.
Use ``settings=CartesiaTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -299,28 +299,28 @@ class CartesiaTTSService(WebsocketTTSService):
# playout timing of the audio!
# 1. Initialize default_settings with hardcoded defaults
default_settings = CartesiaTTSSettings(
default_settings = self.Settings(
model="sonic-3",
voice=None,
language=language_to_cartesia_language(Language.EN),
language=Language.EN,
generation_config=None,
pronunciation_dict_id=None,
)
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", CartesiaTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if model is not None:
_warn_deprecated_param("model", CartesiaTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", CartesiaTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = self.language_to_service_language(params.language)
default_settings.language = params.language
if params.generation_config is not None:
default_settings.generation_config = params.generation_config
if params.pronunciation_dict_id is not None:
@@ -683,7 +683,7 @@ class CartesiaHttpTTSService(TTSService):
"""
Settings = CartesiaTTSSettings
_settings: CartesiaTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Cartesia HTTP TTS configuration.
@@ -712,7 +712,7 @@ class CartesiaHttpTTSService(TTSService):
encoding: str = "pcm_s16le",
container: str = "raw",
params: Optional[InputParams] = None,
settings: Optional[CartesiaTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Cartesia HTTP TTS service.
@@ -722,12 +722,12 @@ class CartesiaHttpTTSService(TTSService):
voice_id: ID of the voice to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=CartesiaTTSSettings(voice=...)`` instead.
Use ``settings=CartesiaHttpTTSService.Settings(voice=...)`` instead.
model: TTS model to use (e.g., "sonic-3").
.. deprecated:: 0.0.105
Use ``settings=CartesiaTTSSettings(model=...)`` instead.
Use ``settings=CartesiaHttpTTSService.Settings(model=...)`` instead.
base_url: Base URL for Cartesia HTTP API.
cartesia_version: API version string for Cartesia service.
@@ -739,35 +739,35 @@ class CartesiaHttpTTSService(TTSService):
params: Additional input parameters for voice customization.
.. deprecated:: 0.0.105
Use ``settings=CartesiaTTSSettings(...)`` instead.
Use ``settings=CartesiaHttpTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to the parent TTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = CartesiaTTSSettings(
default_settings = self.Settings(
model="sonic-3",
voice=None,
language=language_to_cartesia_language(Language.EN),
language=Language.EN,
generation_config=None,
pronunciation_dict_id=None,
)
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", CartesiaTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if model is not None:
_warn_deprecated_param("model", CartesiaTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", CartesiaTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = self.language_to_service_language(params.language)
default_settings.language = params.language
if params.generation_config is not None:
default_settings.generation_config = params.generation_config
if params.pronunciation_dict_id is not None:

View File

@@ -12,13 +12,12 @@ from typing import Optional
from loguru import logger
from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class CerebrasLLMSettings(OpenAILLMSettings):
class CerebrasLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for CerebrasLLMService."""
pass
@@ -32,7 +31,7 @@ class CerebrasLLMService(OpenAILLMService):
"""
Settings = CerebrasLLMSettings
_settings: CerebrasLLMSettings
_settings: Settings
def __init__(
self,
@@ -40,7 +39,7 @@ class CerebrasLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.cerebras.ai/v1",
model: Optional[str] = None,
settings: Optional[CerebrasLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Cerebras LLM service.
@@ -51,18 +50,18 @@ class CerebrasLLMService(OpenAILLMService):
model: The model identifier to use. Defaults to "gpt-oss-120b".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=CerebrasLLMService.Settings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = CerebrasLLMSettings(model="gpt-oss-120b")
default_settings = self.Settings(model="gpt-oss-120b")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", CerebrasLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)
@@ -118,6 +117,10 @@ class CerebrasLLMService(OpenAILLMService):
# Prepend system instruction if set
if self._settings.system_instruction:
messages = params.get("messages", [])
if messages and messages[0].get("role") == "system":
logger.warning(
f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended."
)
params["messages"] = [
{"role": "system", "content": self._settings.system_instruction}
] + messages

View File

@@ -28,7 +28,7 @@ from pipecat.frames.frames import (
UserStartedSpeakingFrame,
UserStoppedSpeakingFrame,
)
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven
from pipecat.services.stt_service import WebsocketSTTService
from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601
@@ -116,14 +116,14 @@ class DeepgramFluxSTTService(WebsocketSTTService):
"""
Settings = DeepgramFluxSTTSettings
_settings: DeepgramFluxSTTSettings
_settings: Settings
_CONFIGURE_FIELDS = {"keyterm", "eot_threshold", "eager_eot_threshold", "eot_timeout_ms"}
class InputParams(BaseModel):
"""Configuration parameters for Deepgram Flux API.
.. deprecated:: 0.0.105
Use ``settings=DeepgramFluxSTTSettings(...)`` instead.
Use ``settings=DeepgramFluxSTTService.Settings(...)`` instead.
Parameters:
eager_eot_threshold: Optional. EagerEndOfTurn/TurnResumed are off by default.
@@ -162,7 +162,7 @@ class DeepgramFluxSTTService(WebsocketSTTService):
tag: Optional[list] = None,
params: Optional[InputParams] = None,
should_interrupt: bool = True,
settings: Optional[DeepgramFluxSTTSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Deepgram Flux STT service.
@@ -176,7 +176,7 @@ class DeepgramFluxSTTService(WebsocketSTTService):
model: Deepgram Flux model to use for transcription.
.. deprecated:: 0.0.105
Use ``settings=DeepgramFluxSTTSettings(model=...)`` instead.
Use ``settings=DeepgramFluxSTTService.Settings(model=...)`` instead.
flux_encoding: Audio encoding format required by Flux API. Must be "linear16".
Raw signed little-endian 16-bit PCM encoding.
@@ -184,7 +184,7 @@ class DeepgramFluxSTTService(WebsocketSTTService):
params: InputParams instance containing detailed API configuration options.
.. deprecated:: 0.0.105
Use ``settings=DeepgramFluxSTTSettings(...)`` instead.
Use ``settings=DeepgramFluxSTTService.Settings(...)`` instead.
should_interrupt: Determine whether the bot should be interrupted when Flux detects that the user is speaking.
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -200,7 +200,7 @@ class DeepgramFluxSTTService(WebsocketSTTService):
stt = DeepgramFluxSTTService(
api_key="your-api-key",
settings=DeepgramFluxSTTSettings(
settings=DeepgramFluxSTTService.Settings(
model="flux-general-en",
eager_eot_threshold=0.5,
eot_threshold=0.8,
@@ -221,7 +221,7 @@ class DeepgramFluxSTTService(WebsocketSTTService):
# already try to reconnect if needed.
# 1. Initialize default_settings with hardcoded defaults
default_settings = DeepgramFluxSTTSettings(
default_settings = self.Settings(
model="flux-general-en",
language=Language.EN,
eager_eot_threshold=None,
@@ -233,12 +233,12 @@ class DeepgramFluxSTTService(WebsocketSTTService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", DeepgramFluxSTTSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", DeepgramFluxSTTSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.eager_eot_threshold = params.eager_eot_threshold
default_settings.eot_threshold = params.eot_threshold
@@ -448,7 +448,7 @@ class DeepgramFluxSTTService(WebsocketSTTService):
"""
return True
async def _update_settings(self, delta: DeepgramFluxSTTSettings) -> dict[str, Any]:
async def _update_settings(self, delta: Settings) -> dict[str, Any]:
"""Apply a settings delta.
Configure-able fields (keyterm, eot_threshold, eager_eot_threshold,

View File

@@ -32,8 +32,8 @@ from pipecat.frames.frames import (
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.aws.sagemaker.bidi_client import SageMakerBidiClient
from pipecat.services.deepgram.stt import DeepgramSTTSettings, LiveOptions
from pipecat.services.settings import STTSettings, _warn_deprecated_param, is_given
from pipecat.services.deepgram.stt import DeepgramSTTService, LiveOptions
from pipecat.services.settings import STTSettings, is_given
from pipecat.services.stt_latency import DEEPGRAM_SAGEMAKER_TTFS_P99
from pipecat.services.stt_service import STTService
from pipecat.transcriptions.language import Language
@@ -42,10 +42,10 @@ from pipecat.utils.tracing.service_decorators import traced_stt
@dataclass
class DeepgramSageMakerSTTSettings(DeepgramSTTSettings):
class DeepgramSageMakerSTTSettings(DeepgramSTTService.Settings):
"""Settings for the Deepgram SageMaker STT service.
Inherits all fields from :class:`DeepgramSTTSettings`.
Inherits all fields from :class:`DeepgramSTTService.Settings`.
"""
pass
@@ -69,7 +69,7 @@ class DeepgramSageMakerSTTService(STTService):
stt = DeepgramSageMakerSTTService(
endpoint_name="my-deepgram-endpoint",
region="us-east-2",
settings=DeepgramSageMakerSTTSettings(
settings=DeepgramSageMakerSTTService.Settings(
model="nova-3",
language="en",
interim_results=True,
@@ -79,7 +79,7 @@ class DeepgramSageMakerSTTService(STTService):
"""
Settings = DeepgramSageMakerSTTSettings
_settings: DeepgramSageMakerSTTSettings
_settings: Settings
def __init__(
self,
@@ -92,7 +92,7 @@ class DeepgramSageMakerSTTService(STTService):
sample_rate: Optional[int] = None,
mip_opt_out: Optional[bool] = None,
live_options: Optional[LiveOptions] = None,
settings: Optional[DeepgramSageMakerSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = DEEPGRAM_SAGEMAKER_TTFS_P99,
**kwargs,
):
@@ -112,7 +112,7 @@ class DeepgramSageMakerSTTService(STTService):
live_options: Legacy configuration options.
.. deprecated:: 0.0.105
Use ``settings=DeepgramSageMakerSTTSettings(...)`` for
Use ``settings=DeepgramSageMakerSTTService.Settings(...)`` for
runtime-updatable fields and direct init parameters for
connection-level config.
@@ -124,7 +124,7 @@ class DeepgramSageMakerSTTService(STTService):
**kwargs: Additional arguments passed to the parent STTService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = DeepgramSageMakerSTTSettings(
default_settings = self.Settings(
model="nova-3",
language=Language.EN,
detect_entities=False,
@@ -147,7 +147,7 @@ class DeepgramSageMakerSTTService(STTService):
# 2. Apply live_options overrides — only if settings not provided
if live_options is not None:
_warn_deprecated_param("live_options", DeepgramSageMakerSTTSettings)
self._warn_init_param_moved_to_settings("live_options")
if not settings:
# Extract init-only fields from live_options
if live_options.sample_rate is not None and sample_rate is None:
@@ -170,7 +170,7 @@ class DeepgramSageMakerSTTService(STTService):
"mip_opt_out",
}
lo_dict = {k: v for k, v in live_options.to_dict().items() if k not in init_only}
delta = DeepgramSageMakerSTTSettings.from_mapping(lo_dict)
delta = self.Settings.from_mapping(lo_dict)
default_settings.apply_update(delta)
# 3. Apply settings delta (canonical API, always wins)
@@ -216,7 +216,7 @@ class DeepgramSageMakerSTTService(STTService):
return changed
# Sync extra to fields after the update so self._settings stays unambiguous
if isinstance(self._settings, DeepgramSTTSettings):
if isinstance(self._settings, self.Settings):
self._settings._sync_extra_to_fields()
# TODO: someday we could reconnect here to apply updated settings.

View File

@@ -33,7 +33,7 @@ from pipecat.frames.frames import (
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.aws.sagemaker.bidi_client import SageMakerBidiClient
from pipecat.services.settings import TTSSettings, _warn_deprecated_param
from pipecat.services.settings import TTSSettings
from pipecat.services.tts_service import TTSService
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -63,12 +63,14 @@ class DeepgramSageMakerTTSService(TTSService):
tts = DeepgramSageMakerTTSService(
endpoint_name="my-deepgram-tts-endpoint",
region="us-east-2",
voice="aura-2-helena-en",
settings=DeepgramSageMakerTTSService.Settings(
voice="aura-2-helena-en",
)
)
"""
Settings = DeepgramSageMakerTTSSettings
_settings: DeepgramSageMakerTTSSettings
_settings: Settings
def __init__(
self,
@@ -78,7 +80,7 @@ class DeepgramSageMakerTTSService(TTSService):
voice: Optional[str] = None,
sample_rate: Optional[int] = None,
encoding: str = "linear16",
settings: Optional[DeepgramSageMakerTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Deepgram SageMaker TTS service.
@@ -90,7 +92,7 @@ class DeepgramSageMakerTTSService(TTSService):
voice: Voice model to use for synthesis. Defaults to "aura-2-helena-en".
.. deprecated:: 0.0.105
Use ``settings=DeepgramSageMakerTTSSettings(voice=...)`` instead.
Use ``settings=DeepgramSageMakerTTSService.Settings(voice=...)`` instead.
sample_rate: Audio sample rate in Hz. If None, uses the value from StartFrame.
encoding: Audio encoding format. Defaults to "linear16".
@@ -99,11 +101,11 @@ class DeepgramSageMakerTTSService(TTSService):
**kwargs: Additional arguments passed to the parent TTSService.
"""
if voice is not None:
_warn_deprecated_param("voice", DeepgramSageMakerTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice", "voice")
voice = voice or "aura-2-helena-en"
default_settings = DeepgramSageMakerTTSSettings(
default_settings = self.Settings(
model=None,
voice=voice,
language=None,

View File

@@ -29,7 +29,6 @@ from pipecat.services.settings import (
NOT_GIVEN,
STTSettings,
_NotGiven,
_warn_deprecated_param,
is_given,
)
from pipecat.services.stt_latency import DEEPGRAM_TTFS_P99
@@ -59,7 +58,7 @@ class LiveOptions:
deepgram-sdk v6.
.. deprecated:: 0.0.105
Use ``settings=DeepgramSTTSettings(...)`` for runtime-updatable fields
Use ``settings=DeepgramSTTService.Settings(...)`` for runtime-updatable fields
and direct ``__init__`` parameters for connection-level config instead.
"""
@@ -248,6 +247,45 @@ class DeepgramSTTSettings(STTSettings):
del self.extra[key]
def _derive_deepgram_urls(base_url: str) -> tuple[str, str]:
"""Derive paired WebSocket and HTTP URLs from a single base URL.
The Deepgram SDK client requires both a WebSocket URL (for streaming)
and an HTTP URL (for REST calls). This helper lets developers provide
a single ``base_url`` and consistently derives both, preserving the
security level they chose. Useful for air-gapped or private deployments
where insecure schemes (ws:// / http://) are acceptable.
Accepted inputs:
- ``wss://`` or ``https://`` — secure (paired as wss + https)
- ``ws://`` or ``http://`` — insecure (paired as ws + http)
- Bare hostname (no scheme) — defaults to secure
- Unrecognized scheme — logs a warning, defaults to secure
Args:
base_url: Host with optional scheme, port, and path.
Returns:
A (ws_url, http_url) tuple with consistent schemes.
"""
known_schemes = ("wss://", "https://", "ws://", "http://")
if "://" in base_url:
scheme, host = base_url.split("://", 1)
scheme += "://"
if scheme not in known_schemes:
logger.warning(
f"Unrecognized scheme in base_url '{base_url}', defaulting to wss:// / https://"
)
else:
scheme = ""
host = base_url
insecure = scheme in ("ws://", "http://")
ws_url = f"{'ws' if insecure else 'wss'}://{host}"
http_url = f"{'http' if insecure else 'https'}://{host}"
return ws_url, http_url
class DeepgramSTTService(STTService):
"""Deepgram speech-to-text service.
@@ -267,7 +305,7 @@ class DeepgramSTTService(STTService):
"""
Settings = DeepgramSTTSettings
_settings: DeepgramSTTSettings
_settings: Settings
def __init__(
self,
@@ -286,7 +324,7 @@ class DeepgramSTTService(STTService):
live_options: Optional[LiveOptions] = None,
addons: Optional[dict] = None,
should_interrupt: bool = True,
settings: Optional[DeepgramSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = DEEPGRAM_TTFS_P99,
**kwargs,
):
@@ -313,7 +351,7 @@ class DeepgramSTTService(STTService):
live_options: Legacy configuration options.
.. deprecated:: 0.0.105
Use ``settings=DeepgramSTTSettings(...)`` for runtime-updatable
Use ``settings=DeepgramSTTService.Settings(...)`` for runtime-updatable
fields and direct init parameters for connection-level config.
addons: Additional Deepgram features to enable.
@@ -345,7 +383,7 @@ class DeepgramSTTService(STTService):
base_url = url
# 1. Initialize default_settings with hardcoded defaults
default_settings = DeepgramSTTSettings(
default_settings = self.Settings(
model="nova-3-general",
language=Language.EN,
detect_entities=False,
@@ -370,7 +408,7 @@ class DeepgramSTTService(STTService):
# 3. Apply live_options overrides — only if settings not provided
if live_options is not None:
_warn_deprecated_param("live_options", DeepgramSTTSettings)
self._warn_init_param_moved_to_settings("live_options")
if not settings:
# Extract init-only fields from live_options
if live_options.sample_rate is not None and sample_rate is None:
@@ -402,7 +440,7 @@ class DeepgramSTTService(STTService):
"mip_opt_out",
}
lo_dict = {k: v for k, v in live_options.to_dict().items() if k not in init_only}
delta = DeepgramSTTSettings.from_mapping(lo_dict)
delta = self.Settings.from_mapping(lo_dict)
default_settings.apply_update(delta)
# 4. Apply settings delta (canonical API, always wins)
@@ -446,8 +484,7 @@ class DeepgramSTTService(STTService):
try:
from deepgram import DeepgramClientEnvironment
ws_url = base_url if base_url.startswith("wss://") else f"wss://{base_url}"
http_url = base_url if base_url.startswith("https://") else f"https://{base_url}"
ws_url, http_url = _derive_deepgram_urls(base_url)
environment = DeepgramClientEnvironment(
base=http_url,
production=ws_url,
@@ -494,7 +531,7 @@ class DeepgramSTTService(STTService):
return changed
# Sync extra to fields after the update so self._settings stays unambiguous
if isinstance(self._settings, DeepgramSTTSettings):
if isinstance(self._settings, self.Settings):
self._settings._sync_extra_to_fields()
if self._connection:
@@ -555,7 +592,15 @@ class DeepgramSTTService(STTService):
value = getattr(s, f.name)
if not is_given(value) or value is None:
continue
kwargs[f.name] = str(value).lower() if isinstance(value, bool) else str(value)
# Lists (e.g. keyterm, keywords, search, redact, replace) must be
# passed through as-is so the SDK's encode_query produces repeated
# query params (keyterm=a&keyterm=b) instead of a stringified list.
if isinstance(value, list):
kwargs[f.name] = value
elif isinstance(value, bool):
kwargs[f.name] = str(value).lower()
else:
kwargs[f.name] = str(value)
# model and language
if is_given(s.model) and s.model is not None:
@@ -581,7 +626,12 @@ class DeepgramSTTService(STTService):
# Any remaining values in extra (that didn't map to declared fields)
for key, value in s.extra.items():
if value is not None:
kwargs[key] = str(value).lower() if isinstance(value, bool) else str(value)
if isinstance(value, list):
kwargs[key] = value
elif isinstance(value, bool):
kwargs[key] = str(value).lower()
else:
kwargs[key] = str(value)
if self._addons:
for key, value in self._addons.items():

View File

@@ -22,13 +22,11 @@ from pipecat.frames.frames import (
EndFrame,
ErrorFrame,
Frame,
InterruptionFrame,
LLMFullResponseEndFrame,
StartFrame,
TTSAudioRawFrame,
TTSStoppedFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import TTSSettings, _warn_deprecated_param
from pipecat.services.settings import TTSSettings
from pipecat.services.tts_service import TTSService, WebsocketTTSService
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -59,7 +57,7 @@ class DeepgramTTSService(WebsocketTTSService):
"""
Settings = DeepgramTTSSettings
_settings: DeepgramTTSSettings
_settings: Settings
SUPPORTED_ENCODINGS = ("linear16", "mulaw", "alaw")
@@ -71,7 +69,7 @@ class DeepgramTTSService(WebsocketTTSService):
base_url: str = "wss://api.deepgram.com",
sample_rate: Optional[int] = None,
encoding: str = "linear16",
settings: Optional[DeepgramTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Deepgram WebSocket TTS service.
@@ -81,7 +79,7 @@ class DeepgramTTSService(WebsocketTTSService):
voice: Voice model to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=DeepgramTTSSettings(voice=...)`` instead.
Use ``settings=DeepgramTTSService.Settings(voice=...)`` instead.
base_url: WebSocket base URL for Deepgram API. Defaults to "wss://api.deepgram.com".
sample_rate: Audio sample rate in Hz. If None, uses service default.
@@ -99,7 +97,7 @@ class DeepgramTTSService(WebsocketTTSService):
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = DeepgramTTSSettings(
default_settings = self.Settings(
model=None,
voice="aura-2-helena-en",
language=None,
@@ -107,7 +105,7 @@ class DeepgramTTSService(WebsocketTTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice is not None:
_warn_deprecated_param("voice", DeepgramTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice", "voice")
default_settings.model = voice
default_settings.voice = voice
@@ -120,7 +118,7 @@ class DeepgramTTSService(WebsocketTTSService):
super().__init__(
sample_rate=sample_rate,
pause_frame_processing=True,
push_stop_frames=True,
push_stop_frames=False,
push_start_frame=True,
append_trailing_space=True,
settings=default_settings,
@@ -168,19 +166,6 @@ class DeepgramTTSService(WebsocketTTSService):
await super().cancel(frame)
await self._disconnect()
async def process_frame(self, frame: Frame, direction: FrameDirection):
"""Process frames with special handling for LLM response end.
Args:
frame: The frame to process.
direction: The direction of frame processing.
"""
await super().process_frame(frame, direction)
# When the LLM finishes responding, flush any remaining text in Deepgram's buffer
if isinstance(frame, (LLMFullResponseEndFrame, EndFrame)):
await self.flush_audio()
async def _connect(self):
"""Connect to Deepgram WebSocket and start receive task."""
await super()._connect()
@@ -204,7 +189,7 @@ class DeepgramTTSService(WebsocketTTSService):
"""Apply a settings delta.
Args:
delta: A :class:`TTSSettings` (or ``DeepgramTTSSettings``) delta.
delta: A :class:`TTSSettings` (or ``DeepgramTTSService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.
@@ -277,19 +262,19 @@ class DeepgramTTSService(WebsocketTTSService):
return self._websocket
raise Exception("Websocket not connected")
async def _handle_interruption(self, frame: InterruptionFrame, direction: FrameDirection):
"""Handle interruption by sending Clear message to Deepgram.
async def on_audio_context_interrupted(self, context_id: str):
"""Send Clear message to Deepgram when an audio context is interrupted.
The Clear message will clear Deepgram's internal text buffer and stop
sending audio, allowing for a new response to be generated.
"""
await super()._handle_interruption(frame, direction)
# Send Clear message to stop current audio generation
Args:
context_id: The ID of the audio context that was interrupted.
"""
await self.stop_all_metrics()
if self._websocket:
try:
clear_msg = {"type": "Clear"}
await self._websocket.send(json.dumps(clear_msg))
await self._websocket.send(json.dumps({"type": "Clear"}))
except Exception as e:
logger.error(f"{self} error sending Clear message: {e}")
@@ -298,11 +283,9 @@ class DeepgramTTSService(WebsocketTTSService):
async for message in self._get_websocket():
if isinstance(message, bytes):
# Binary message contains audio data
await self.stop_ttfb_metrics()
frame = TTSAudioRawFrame(
message, self.sample_rate, 1, context_id=self.get_active_audio_context_id()
)
await self.push_frame(frame)
ctx_id = self.get_active_audio_context_id()
frame = TTSAudioRawFrame(message, self.sample_rate, 1, context_id=ctx_id)
await self.append_to_audio_context(ctx_id, frame)
elif isinstance(message, str):
# Text message contains metadata or control messages
try:
@@ -313,12 +296,15 @@ class DeepgramTTSService(WebsocketTTSService):
logger.trace(f"Received metadata: {msg}")
elif msg_type == "Flushed":
logger.trace(f"Received Flushed: {msg}")
# Flushed indicates the end of audio generation for the current buffer
# This happens after flush_audio() is called
ctx_id = self.get_active_audio_context_id()
await self.append_to_audio_context(
ctx_id, TTSStoppedFrame(context_id=ctx_id)
)
await self.remove_audio_context(ctx_id)
elif msg_type == "Cleared":
logger.trace(f"Received Cleared: {msg}")
# Buffer has been cleared after interruption
# TTSStoppedFrame will be sent by the interruption handler
# Buffer has been cleared after interruption.
# The on_audio_context_interrupted handler already cleaned up.
elif msg_type == "Warning":
logger.warning(
f"{self} warning: {msg.get('description', 'Unknown warning')}"
@@ -359,8 +345,6 @@ class DeepgramTTSService(WebsocketTTSService):
if not self._websocket or self._websocket.state is State.CLOSED:
await self._connect()
await self.start_tts_usage_metrics(text)
# Send text message to Deepgram
# Note: We don't send Flush here - that should only be sent when the
# LLM finishes a complete response via flush_audio()
@@ -383,7 +367,7 @@ class DeepgramHttpTTSService(TTSService):
"""
Settings = DeepgramTTSSettings
_settings: DeepgramTTSSettings
_settings: Settings
def __init__(
self,
@@ -394,7 +378,7 @@ class DeepgramHttpTTSService(TTSService):
base_url: str = "https://api.deepgram.com",
sample_rate: Optional[int] = None,
encoding: str = "linear16",
settings: Optional[DeepgramTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Deepgram TTS service.
@@ -404,7 +388,7 @@ class DeepgramHttpTTSService(TTSService):
voice: Voice model to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=DeepgramTTSSettings(voice=...)`` instead.
Use ``settings=DeepgramHttpTTSService.Settings(voice=...)`` instead.
aiohttp_session: Shared aiohttp session for HTTP requests with connection pooling.
base_url: Custom base URL for Deepgram API. Defaults to "https://api.deepgram.com".
@@ -415,7 +399,7 @@ class DeepgramHttpTTSService(TTSService):
**kwargs: Additional arguments passed to parent TTSService class.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = DeepgramTTSSettings(
default_settings = self.Settings(
model=None,
voice="aura-2-helena-en",
language=None,
@@ -423,7 +407,7 @@ class DeepgramHttpTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice is not None:
_warn_deprecated_param("voice", DeepgramTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice", "voice")
default_settings.model = voice
default_settings.voice = voice

View File

@@ -12,13 +12,12 @@ from typing import Optional
from loguru import logger
from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class DeepSeekLLMSettings(OpenAILLMSettings):
class DeepSeekLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for DeepSeekLLMService."""
pass
@@ -32,7 +31,7 @@ class DeepSeekLLMService(OpenAILLMService):
"""
Settings = DeepSeekLLMSettings
_settings: DeepSeekLLMSettings
_settings: Settings
def __init__(
self,
@@ -40,7 +39,7 @@ class DeepSeekLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.deepseek.com/v1",
model: Optional[str] = None,
settings: Optional[DeepSeekLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the DeepSeek LLM service.
@@ -51,18 +50,18 @@ class DeepSeekLLMService(OpenAILLMService):
model: The model identifier to use. Defaults to "deepseek-chat".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=DeepSeekLLMService.Settings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = DeepSeekLLMSettings(model="deepseek-chat")
default_settings = self.Settings(model="deepseek-chat")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", DeepSeekLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)

View File

@@ -35,7 +35,7 @@ from pipecat.frames.frames import (
VADUserStoppedSpeakingFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven
from pipecat.services.stt_latency import ELEVENLABS_REALTIME_TTFS_P99, ELEVENLABS_TTFS_P99
from pipecat.services.stt_service import SegmentedSTTService, WebsocketSTTService
from pipecat.transcriptions.language import Language, resolve_language
@@ -217,13 +217,13 @@ class ElevenLabsSTTService(SegmentedSTTService):
"""
Settings = ElevenLabsSTTSettings
_settings: ElevenLabsSTTSettings
_settings: Settings
class InputParams(BaseModel):
"""Configuration parameters for ElevenLabs STT API.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsSTTSettings(...)`` instead.
Use ``settings=ElevenLabsSTTService.Settings(...)`` instead.
Parameters:
language: Target language for transcription.
@@ -242,7 +242,7 @@ class ElevenLabsSTTService(SegmentedSTTService):
model: Optional[str] = None,
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
settings: Optional[ElevenLabsSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = ELEVENLABS_TTFS_P99,
**kwargs,
):
@@ -255,13 +255,13 @@ class ElevenLabsSTTService(SegmentedSTTService):
model: Model ID for transcription.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsSTTSettings(model=...)`` instead.
Use ``settings=ElevenLabsSTTService.Settings(model=...)`` instead.
sample_rate: Audio sample rate in Hz. If not provided, uses the pipeline's rate.
params: Configuration parameters for the STT service.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsSTTSettings(...)`` instead.
Use ``settings=ElevenLabsSTTService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -270,23 +270,23 @@ class ElevenLabsSTTService(SegmentedSTTService):
**kwargs: Additional arguments passed to SegmentedSTTService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = ElevenLabsSTTSettings(
default_settings = self.Settings(
model="scribe_v2",
language=language_to_elevenlabs_language(Language.EN),
language=Language.EN,
tag_audio_events=None,
)
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", ElevenLabsSTTSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", ElevenLabsSTTSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = language_to_elevenlabs_language(params.language)
default_settings.language = params.language
default_settings.tag_audio_events = params.tag_audio_events
# 4. Apply settings delta (canonical API, always wins)
@@ -450,13 +450,13 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService):
"""
Settings = ElevenLabsRealtimeSTTSettings
_settings: ElevenLabsRealtimeSTTSettings
_settings: Settings
class InputParams(BaseModel):
"""Configuration parameters for ElevenLabs Realtime STT API.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsRealtimeSTTSettings(...)`` instead.
Use ``settings=ElevenLabsRealtimeSTTService.Settings(...)`` instead.
Parameters:
language_code: ISO-639-1 or ISO-639-3 language code. Leave None for auto-detection.
@@ -496,7 +496,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService):
enable_logging: bool = False,
include_language_detection: bool = False,
params: Optional[InputParams] = None,
settings: Optional[ElevenLabsRealtimeSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = ELEVENLABS_REALTIME_TTFS_P99,
**kwargs,
):
@@ -511,7 +511,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService):
model: Model ID for transcription.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsRealtimeSTTSettings(model=...)`` instead.
Use ``settings=ElevenLabsRealtimeSTTService.Settings(model=...)`` instead.
sample_rate: Audio sample rate in Hz. If not provided, uses the pipeline's rate.
include_timestamps: Whether to include word-level timestamps in transcripts.
@@ -520,7 +520,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService):
params: Configuration parameters for the STT service.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsRealtimeSTTSettings(...)`` instead.
Use ``settings=ElevenLabsRealtimeSTTService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -529,7 +529,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService):
**kwargs: Additional arguments passed to WebsocketSTTService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = ElevenLabsRealtimeSTTSettings(
default_settings = self.Settings(
model="scribe_v2_realtime",
language=None,
vad_silence_threshold_secs=None,
@@ -540,12 +540,12 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", ElevenLabsRealtimeSTTSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", ElevenLabsRealtimeSTTSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.language = params.language_code
if params.commit_strategy != CommitStrategy.MANUAL:
@@ -597,7 +597,7 @@ class ElevenLabsRealtimeSTTService(WebsocketSTTService):
"""Apply a settings delta and reconnect if anything changed.
Args:
delta: A :class:`STTSettings` (or ``ElevenLabsRealtimeSTTSettings``) delta.
delta: A :class:`STTSettings` (or ``ElevenLabsRealtimeSTTService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.

View File

@@ -44,7 +44,7 @@ from pipecat.frames.frames import (
TTSStoppedFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import (
TextAggregationMode,
TTSService,
@@ -317,13 +317,13 @@ class ElevenLabsTTSService(WebsocketTTSService):
"""
Settings = ElevenLabsTTSSettings
_settings: ElevenLabsTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for ElevenLabs TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsTTSSettings(...)`` instead.
Use ``settings=ElevenLabsTTSService.Settings(...)`` instead.
Parameters:
language: Language to use for synthesis.
@@ -364,7 +364,7 @@ class ElevenLabsTTSService(WebsocketTTSService):
enable_logging: Optional[bool] = None,
pronunciation_dictionary_locators: Optional[List[PronunciationDictionaryLocator]] = None,
params: Optional[InputParams] = None,
settings: Optional[ElevenLabsTTSSettings] = None,
settings: Optional[Settings] = None,
text_aggregation_mode: Optional[TextAggregationMode] = None,
aggregate_sentences: Optional[bool] = None,
**kwargs,
@@ -376,12 +376,12 @@ class ElevenLabsTTSService(WebsocketTTSService):
voice_id: ID of the voice to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsTTSSettings(voice=...)`` instead.
Use ``settings=ElevenLabsTTSService.Settings(voice=...)`` instead.
model: TTS model to use (e.g., "eleven_turbo_v2_5").
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsTTSSettings(model=...)`` instead.
Use ``settings=ElevenLabsTTSService.Settings(model=...)`` instead.
url: WebSocket URL for ElevenLabs TTS API.
sample_rate: Audio sample rate. If None, uses default.
@@ -393,7 +393,7 @@ class ElevenLabsTTSService(WebsocketTTSService):
params: Additional input parameters for voice customization.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsTTSSettings(...)`` instead.
Use ``settings=ElevenLabsTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -423,7 +423,7 @@ class ElevenLabsTTSService(WebsocketTTSService):
# after a short period not receiving any audio.
# 1. Initialize default_settings with hardcoded defaults
default_settings = ElevenLabsTTSSettings(
default_settings = self.Settings(
model="eleven_turbo_v2_5",
voice=None,
language=None,
@@ -437,19 +437,19 @@ class ElevenLabsTTSService(WebsocketTTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", ElevenLabsTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if model is not None:
_warn_deprecated_param("model", ElevenLabsTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
_pronunciation_dictionary_locators = pronunciation_dictionary_locators
if params is not None:
_warn_deprecated_param("params", ElevenLabsTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = self.language_to_service_language(params.language)
default_settings.language = params.language
if params.stability is not None:
default_settings.stability = params.stability
if params.similarity_boost is not None:
@@ -533,11 +533,11 @@ class ElevenLabsTTSService(WebsocketTTSService):
"""Apply a settings delta, reconnecting as needed.
Uses the declarative ``URL_FIELDS`` and ``VOICE_SETTINGS_FIELDS``
sets on :class:`ElevenLabsTTSSettings` to decide whether to
sets on :class:`ElevenLabsTTSService.Settings` to decide whether to
reconnect the WebSocket or close the current audio context.
Args:
delta: A :class:`TTSSettings` (or ``ElevenLabsTTSSettings``) delta.
delta: A :class:`TTSSettings` (or ``ElevenLabsTTSService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.
@@ -550,19 +550,19 @@ class ElevenLabsTTSService(WebsocketTTSService):
# Rebuild voice settings for next context
self._voice_settings = self._set_voice_settings()
url_changed = bool(changed.keys() & ElevenLabsTTSSettings.URL_FIELDS)
voice_settings_changed = bool(changed.keys() & ElevenLabsTTSSettings.VOICE_SETTINGS_FIELDS)
url_changed = bool(changed.keys() & self.Settings.URL_FIELDS)
voice_settings_changed = bool(changed.keys() & self.Settings.VOICE_SETTINGS_FIELDS)
if url_changed:
logger.debug(
f"URL-level setting changed ({changed.keys() & ElevenLabsTTSSettings.URL_FIELDS}), "
f"URL-level setting changed ({changed.keys() & self.Settings.URL_FIELDS}), "
f"reconnecting WebSocket"
)
await self._disconnect()
await self._connect()
elif voice_settings_changed:
logger.debug(
f"Voice settings changed ({changed.keys() & ElevenLabsTTSSettings.VOICE_SETTINGS_FIELDS}), "
f"Voice settings changed ({changed.keys() & self.Settings.VOICE_SETTINGS_FIELDS}), "
f"closing current context to apply changes"
)
audio_contexts = self.get_audio_contexts()
@@ -573,7 +573,7 @@ class ElevenLabsTTSService(WebsocketTTSService):
if not url_changed:
# Reconnect applies all settings; only warn about fields not handled
# by voice settings or URL changes.
handled = ElevenLabsTTSSettings.URL_FIELDS | ElevenLabsTTSSettings.VOICE_SETTINGS_FIELDS
handled = self.Settings.URL_FIELDS | self.Settings.VOICE_SETTINGS_FIELDS
self._warn_unhandled_updated_settings(changed.keys() - handled)
return changed
@@ -906,13 +906,13 @@ class ElevenLabsHttpTTSService(TTSService):
"""
Settings = ElevenLabsHttpTTSSettings
_settings: ElevenLabsHttpTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for ElevenLabs HTTP TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsHttpTTSSettings(...)`` instead.
Use ``settings=ElevenLabsHttpTTSService.Settings(...)`` instead.
Parameters:
language: Language to use for synthesis.
@@ -947,7 +947,7 @@ class ElevenLabsHttpTTSService(TTSService):
sample_rate: Optional[int] = None,
pronunciation_dictionary_locators: Optional[List[PronunciationDictionaryLocator]] = None,
params: Optional[InputParams] = None,
settings: Optional[ElevenLabsHttpTTSSettings] = None,
settings: Optional[Settings] = None,
text_aggregation_mode: Optional[TextAggregationMode] = None,
aggregate_sentences: Optional[bool] = None,
**kwargs,
@@ -959,13 +959,13 @@ class ElevenLabsHttpTTSService(TTSService):
voice_id: ID of the voice to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsHttpTTSSettings(voice=...)`` instead.
Use ``settings=ElevenLabsHttpTTSService.Settings(voice=...)`` instead.
aiohttp_session: aiohttp ClientSession for HTTP requests.
model: TTS model to use (e.g., "eleven_turbo_v2_5").
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsHttpTTSSettings(model=...)`` instead.
Use ``settings=ElevenLabsHttpTTSService.Settings(model=...)`` instead.
base_url: Base URL for ElevenLabs HTTP API.
sample_rate: Audio sample rate. If None, uses default.
@@ -974,7 +974,7 @@ class ElevenLabsHttpTTSService(TTSService):
params: Additional input parameters for voice customization.
.. deprecated:: 0.0.105
Use ``settings=ElevenLabsHttpTTSSettings(...)`` instead.
Use ``settings=ElevenLabsHttpTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -987,7 +987,7 @@ class ElevenLabsHttpTTSService(TTSService):
**kwargs: Additional arguments passed to the parent service.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = ElevenLabsHttpTTSSettings(
default_settings = self.Settings(
model="eleven_turbo_v2_5",
voice=None,
language=None,
@@ -1002,19 +1002,19 @@ class ElevenLabsHttpTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", ElevenLabsHttpTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if model is not None:
_warn_deprecated_param("model", ElevenLabsHttpTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
_pronunciation_dictionary_locators = pronunciation_dictionary_locators
if params is not None:
_warn_deprecated_param("params", ElevenLabsHttpTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = self.language_to_service_language(params.language)
default_settings.language = params.language
if params.optimize_streaming_latency is not None:
default_settings.optimize_streaming_latency = params.optimize_streaming_latency
if params.stability is not None:
@@ -1091,7 +1091,7 @@ class ElevenLabsHttpTTSService(TTSService):
"""Apply a settings delta and rebuild voice settings.
Args:
delta: A :class:`TTSSettings` (or ``ElevenLabsHttpTTSSettings``) delta.
delta: A :class:`TTSSettings` (or ``ElevenLabsHttpTTSService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.

View File

@@ -23,7 +23,7 @@ from pydantic import BaseModel
from pipecat.frames.frames import ErrorFrame, Frame, URLImageRawFrame
from pipecat.services.image_service import ImageGenService
from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven
@dataclass
@@ -71,13 +71,13 @@ class FalImageGenService(ImageGenService):
"""
Settings = FalImageGenSettings
_settings: FalImageGenSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Fal.ai image generation.
.. deprecated:: 0.0.105
Use ``settings=FalImageGenSettings(...)`` instead.
Use ``settings=FalImageGenService.Settings(...)`` instead.
Parameters:
seed: Random seed for reproducible generation. If None, uses random seed.
@@ -97,7 +97,7 @@ class FalImageGenService(ImageGenService):
enable_safety_checker: bool = True
format: str = "png"
_settings: FalImageGenSettings
_settings: Settings
def __init__(
self,
@@ -106,7 +106,7 @@ class FalImageGenService(ImageGenService):
aiohttp_session: aiohttp.ClientSession,
model: Optional[str] = None,
key: Optional[str] = None,
settings: Optional[FalImageGenSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the FalImageGenService.
@@ -115,13 +115,13 @@ class FalImageGenService(ImageGenService):
params: Input parameters for image generation configuration.
.. deprecated:: 0.0.105
Use ``settings=FalImageGenSettings(...)`` instead.
Use ``settings=FalImageGenService.Settings(...)`` instead.
aiohttp_session: HTTP client session for downloading generated images.
model: The Fal.ai model to use for generation. Defaults to "fal-ai/fast-sdxl".
.. deprecated:: 0.0.105
Use ``settings=FalImageGenSettings(model=...)`` instead.
Use ``settings=FalImageGenService.Settings(model=...)`` instead.
key: Optional API key for Fal.ai. If provided, sets FAL_KEY environment variable.
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -129,7 +129,7 @@ class FalImageGenService(ImageGenService):
**kwargs: Additional arguments passed to parent ImageGenService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = FalImageGenSettings(
default_settings = self.Settings(
model="fal-ai/fast-sdxl",
seed=None,
num_inference_steps=8,
@@ -142,11 +142,11 @@ class FalImageGenService(ImageGenService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", FalImageGenSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if params is not None:
_warn_deprecated_param("params", FalImageGenSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.seed = params.seed
default_settings.num_inference_steps = params.num_inference_steps

View File

@@ -20,7 +20,7 @@ from loguru import logger
from pydantic import BaseModel
from pipecat.frames.frames import ErrorFrame, Frame, TranscriptionFrame
from pipecat.services.settings import STTSettings, _warn_deprecated_param
from pipecat.services.settings import STTSettings
from pipecat.services.stt_latency import FAL_TTFS_P99
from pipecat.services.stt_service import SegmentedSTTService
from pipecat.transcriptions.language import Language, resolve_language
@@ -156,13 +156,13 @@ class FalSTTService(SegmentedSTTService):
"""
Settings = FalSTTSettings
_settings: FalSTTSettings
_settings: Settings
class InputParams(BaseModel):
"""Configuration parameters for Fal's Wizper API.
.. deprecated:: 0.0.105
Use ``settings=FalSTTSettings(...)`` instead.
Use ``settings=FalSTTService.Settings(...)`` instead.
Parameters:
language: Language of the audio input. Defaults to English.
@@ -186,7 +186,7 @@ class FalSTTService(SegmentedSTTService):
version: str = "3",
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
settings: Optional[FalSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = FAL_TTFS_P99,
**kwargs,
):
@@ -204,7 +204,7 @@ class FalSTTService(SegmentedSTTService):
params: Configuration parameters for the Wizper API.
.. deprecated:: 0.0.105
Use ``settings=FalSTTSettings(...)`` for model/language and
Use ``settings=FalSTTService.Settings(...)`` for model/language and
direct init parameters for task/chunk_level/version instead.
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -214,19 +214,19 @@ class FalSTTService(SegmentedSTTService):
**kwargs: Additional arguments passed to SegmentedSTTService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = FalSTTSettings(
default_settings = self.Settings(
model=None,
language=language_to_fal_language(Language.EN),
language=Language.EN,
)
# 2. (no deprecated direct args for this service)
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", FalSTTSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = language_to_fal_language(params.language)
default_settings.language = params.language
if params.task != "transcribe":
task = params.task
if params.chunk_level != "segment":

View File

@@ -12,13 +12,12 @@ from typing import Optional
from loguru import logger
from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class FireworksLLMSettings(OpenAILLMSettings):
class FireworksLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for FireworksLLMService."""
pass
@@ -32,7 +31,7 @@ class FireworksLLMService(OpenAILLMService):
"""
Settings = FireworksLLMSettings
_settings: FireworksLLMSettings
_settings: Settings
def __init__(
self,
@@ -40,7 +39,7 @@ class FireworksLLMService(OpenAILLMService):
api_key: str,
model: Optional[str] = None,
base_url: str = "https://api.fireworks.ai/inference/v1",
settings: Optional[FireworksLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Fireworks LLM service.
@@ -50,7 +49,7 @@ class FireworksLLMService(OpenAILLMService):
model: The model identifier to use. Defaults to "accounts/fireworks/models/firefunction-v2".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=FireworksLLMService.Settings(model=...)`` instead.
base_url: The base URL for Fireworks API. Defaults to "https://api.fireworks.ai/inference/v1".
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -58,11 +57,11 @@ class FireworksLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = FireworksLLMSettings(model="accounts/fireworks/models/firefunction-v2")
default_settings = self.Settings(model="accounts/fireworks/models/firefunction-v2")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", FireworksLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)
@@ -119,6 +118,10 @@ class FireworksLLMService(OpenAILLMService):
# Prepend system instruction if set
if self._settings.system_instruction:
messages = params.get("messages", [])
if messages and messages[0].get("role") == "system":
logger.warning(
f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended."
)
params["messages"] = [
{"role": "system", "content": self._settings.system_instruction}
] + messages

View File

@@ -27,7 +27,7 @@ from pipecat.frames.frames import (
TTSStoppedFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import InterruptibleTTSService
from pipecat.transcriptions.language import Language
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -85,13 +85,13 @@ class FishAudioTTSService(InterruptibleTTSService):
"""
Settings = FishAudioTTSSettings
_settings: FishAudioTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Fish Audio TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=FishAudioTTSSettings(...)`` instead.
Use ``settings=FishAudioTTSService.Settings(...)`` instead.
Parameters:
language: Language for synthesis. Defaults to English.
@@ -117,7 +117,7 @@ class FishAudioTTSService(InterruptibleTTSService):
output_format: FishAudioOutputFormat = "pcm",
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
settings: Optional[FishAudioTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Fish Audio TTS service.
@@ -127,7 +127,7 @@ class FishAudioTTSService(InterruptibleTTSService):
reference_id: Reference ID of the voice model to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=FishAudioTTSSettings(voice=...)`` instead.
Use ``settings=FishAudioTTSService.Settings(voice=...)`` instead.
model: Deprecated. Reference ID of the voice model to use for synthesis.
@@ -138,14 +138,14 @@ class FishAudioTTSService(InterruptibleTTSService):
model_id: Specify which Fish Audio TTS model to use (e.g. "s1").
.. deprecated:: 0.0.105
Use ``settings=FishAudioTTSSettings(model=...)`` instead.
Use ``settings=FishAudioTTSService.Settings(model=...)`` instead.
output_format: Audio output format. Defaults to "pcm".
sample_rate: Audio sample rate. If None, uses default.
params: Additional input parameters for voice customization.
.. deprecated:: 0.0.105
Use ``settings=FishAudioTTSSettings(...)`` instead.
Use ``settings=FishAudioTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -171,8 +171,8 @@ class FishAudioTTSService(InterruptibleTTSService):
reference_id = model
# 1. Initialize default_settings with hardcoded defaults
default_settings = FishAudioTTSSettings(
model="s1",
default_settings = self.Settings(
model="s2-pro",
voice=None,
language=None,
latency="balanced",
@@ -185,15 +185,15 @@ class FishAudioTTSService(InterruptibleTTSService):
# 2. Apply direct init arg overrides (deprecated)
if reference_id is not None:
_warn_deprecated_param("reference_id", FishAudioTTSSettings, "voice")
self._warn_init_param_moved_to_settings("reference_id", "voice")
default_settings.voice = reference_id
if model_id is not None:
_warn_deprecated_param("model_id", FishAudioTTSSettings, "model")
self._warn_init_param_moved_to_settings("model_id", "model")
default_settings.model = model_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", FishAudioTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.latency is not None:
default_settings.latency = params.latency
@@ -240,7 +240,7 @@ class FishAudioTTSService(InterruptibleTTSService):
Any change to voice or model triggers a WebSocket reconnect.
Args:
delta: A :class:`TTSSettings` (or ``FishAudioTTSSettings``) delta.
delta: A :class:`TTSSettings` (or ``FishAudioTTSService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.

View File

@@ -153,7 +153,7 @@ class GladiaInputParams(BaseModel):
"""Configuration parameters for the Gladia STT service.
.. deprecated:: 0.0.105
Use ``settings=GladiaSTTSettings(...)`` for runtime-updatable
Use ``settings=GladiaSTTService.Settings(...)`` for runtime-updatable
fields and direct init parameters for encoding/bit_depth/channels.
Parameters:

View File

@@ -39,7 +39,7 @@ from pipecat.services.gladia.config import (
PreProcessingConfig,
RealtimeProcessingConfig,
)
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven
from pipecat.services.stt_latency import GLADIA_TTFS_P99
from pipecat.services.stt_service import WebsocketSTTService
from pipecat.transcriptions.language import Language, resolve_language
@@ -231,7 +231,7 @@ class GladiaSTTService(WebsocketSTTService):
"""
Settings = GladiaSTTSettings
_settings: GladiaSTTSettings
_settings: Settings
# Maintain backward compatibility
InputParams = _InputParamsDescriptor()
@@ -251,7 +251,7 @@ class GladiaSTTService(WebsocketSTTService):
params: Optional[GladiaInputParams] = None,
max_buffer_size: int = 1024 * 1024 * 20, # 20MB default buffer
should_interrupt: bool = True,
settings: Optional[GladiaSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = GLADIA_TTFS_P99,
**kwargs,
):
@@ -274,12 +274,12 @@ class GladiaSTTService(WebsocketSTTService):
model: Model to use for transcription.
.. deprecated:: 0.0.105
Use ``settings=GladiaSTTSettings(model=...)`` instead.
Use ``settings=GladiaSTTService.Settings(model=...)`` instead.
params: Additional configuration parameters for Gladia service.
.. deprecated:: 0.0.105
Use ``settings=GladiaSTTSettings(...)`` for runtime-updatable
Use ``settings=GladiaSTTService.Settings(...)`` for runtime-updatable
fields and direct init parameters for encoding/bit_depth/channels.
max_buffer_size: Maximum size of audio buffer in bytes. Defaults to 20MB.
@@ -302,7 +302,7 @@ class GladiaSTTService(WebsocketSTTService):
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = GladiaSTTSettings(
default_settings = self.Settings(
model="solaria-1",
language=None,
language_config=None,
@@ -317,12 +317,12 @@ class GladiaSTTService(WebsocketSTTService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GladiaSTTSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GladiaSTTSettings)
self._warn_init_param_moved_to_settings("params")
if params.language is not None:
with warnings.catch_warnings():
warnings.simplefilter("always")
@@ -469,7 +469,7 @@ class GladiaSTTService(WebsocketSTTService):
await super().start(frame)
await self._connect()
async def _update_settings(self, delta: GladiaSTTSettings) -> dict[str, Any]:
async def _update_settings(self, delta: Settings) -> dict[str, Any]:
"""Apply settings delta.
Settings are stored but not applied to the active session.

View File

@@ -12,12 +12,12 @@ from .frames import *
from .gemini_live import *
from .image import *
from .llm import *
from .llm_openai import *
from .llm_vertex import *
from .openai import *
from .rtvi import *
from .stt import *
from .tts import *
from .vertex import *
sys.modules[__name__] = DeprecatedModuleProxy(
globals(), "google", "google.[frames,image,llm,llm_openai,llm_vertex,rtvi,stt,tts]"
globals(), "google", "google.[frames,image,llm,openai,vertex,rtvi,stt,tts]"
)

View File

@@ -1,6 +1,6 @@
from .file_api import GeminiFileAPI
from .llm import GeminiLiveLLMService
from .llm_vertex import GeminiLiveVertexLLMService
from .vertex.llm import GeminiLiveVertexLLMService
__all__ = [
"GeminiFileAPI",

View File

@@ -76,7 +76,7 @@ from pipecat.services.openai.llm import (
OpenAIAssistantContextAggregator,
OpenAIUserContextAggregator,
)
from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, LLMSettings, _NotGiven
from pipecat.transcriptions.language import Language, resolve_language
from pipecat.utils.string import match_endofsentence
from pipecat.utils.time import time_now_iso8601
@@ -553,7 +553,7 @@ class InputParams(BaseModel):
"""Input parameters for Gemini Live generation.
.. deprecated:: 0.0.105
Use ``GeminiLiveLLMSettings`` instead.
Use ``GeminiLiveLLMService.Settings`` instead.
Parameters:
frequency_penalty: Frequency penalty for generation (0.0-2.0). Defaults to None.
@@ -643,7 +643,7 @@ class GeminiLiveLLMService(LLMService):
"""
Settings = GeminiLiveLLMSettings
_settings: GeminiLiveLLMSettings
_settings: Settings
# Overriding the default adapter to use the Gemini one.
adapter_class = GeminiLLMAdapter
@@ -660,7 +660,7 @@ class GeminiLiveLLMService(LLMService):
system_instruction: Optional[str] = None,
tools: Optional[Union[List[dict], ToolsSchema]] = None,
params: Optional[InputParams] = None,
settings: Optional[GeminiLiveLLMSettings] = None,
settings: Optional[Settings] = None,
inference_on_context_initialization: bool = True,
file_api_base_url: str = "https://generativelanguage.googleapis.com/v1beta/files",
http_options: Optional[HttpOptions] = None,
@@ -680,12 +680,12 @@ class GeminiLiveLLMService(LLMService):
model: Model identifier to use.
.. deprecated:: 0.0.105
Use ``settings=GeminiLiveLLMSettings(model=...)`` instead.
Use ``settings=GeminiLiveLLMService.Settings(model=...)`` instead.
voice_id: TTS voice identifier. Defaults to "Charon".
.. deprecated:: 0.0.105
Use ``settings=GeminiLiveLLMSettings(voice=...)`` instead.
Use ``settings=GeminiLiveLLMService.Settings(voice=...)`` instead.
start_audio_paused: Whether to start with audio input paused. Defaults to False.
start_video_paused: Whether to start with video input paused. Defaults to False.
system_instruction: System prompt for the model. Defaults to None.
@@ -693,7 +693,7 @@ class GeminiLiveLLMService(LLMService):
params: Configuration parameters for the model.
.. deprecated:: 0.0.105
Use ``settings=GeminiLiveLLMSettings(...)`` instead.
Use ``settings=GeminiLiveLLMService.Settings(...)`` instead.
settings: Gemini Live LLM settings. If provided together with deprecated
top-level parameters, the ``settings`` values take precedence.
@@ -716,7 +716,7 @@ class GeminiLiveLLMService(LLMService):
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = GeminiLiveLLMSettings(
default_settings = self.Settings(
model="models/gemini-2.5-flash-native-audio-preview-12-2025",
system_instruction=system_instruction,
voice="Charon",
@@ -742,15 +742,15 @@ class GeminiLiveLLMService(LLMService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GeminiLiveLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if voice_id != "Charon":
_warn_deprecated_param("voice_id", GeminiLiveLLMSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GeminiLiveLLMSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.frequency_penalty = params.frequency_penalty
default_settings.max_tokens = params.max_tokens

View File

@@ -4,277 +4,15 @@
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Service for accessing Gemini Live via Google Vertex AI.
"""Deprecated: use ``pipecat.services.google.gemini_live.vertex.llm`` instead."""
This module provides integration with Google's Gemini Live model via
Vertex AI, supporting both text and audio modalities with voice transcription,
streaming responses, and tool usage.
"""
import warnings
import json
from dataclasses import dataclass
from typing import List, Optional, Union
from loguru import logger
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.services.google.gemini_live.llm import (
GeminiLiveLLMService,
GeminiLiveLLMSettings,
GeminiMediaResolution,
GeminiModalities,
HttpOptions,
InputParams,
language_to_gemini_language,
warnings.warn(
"Module `pipecat.services.google.gemini_live.llm_vertex` is deprecated, "
"use `pipecat.services.google.gemini_live.vertex.llm` instead.",
DeprecationWarning,
stacklevel=2,
)
from pipecat.services.settings import _warn_deprecated_param
try:
from google.auth import default
from google.auth.exceptions import GoogleAuthError
from google.auth.transport.requests import Request
from google.genai import Client
from google.oauth2 import service_account
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error("In order to use Google Vertex AI, you need to `pip install pipecat-ai[google]`.")
raise Exception(f"Missing module: {e}")
@dataclass
class GeminiLiveVertexLLMSettings(GeminiLiveLLMSettings):
"""Settings for GeminiLiveVertexLLMService."""
pass
class GeminiLiveVertexLLMService(GeminiLiveLLMService):
"""Provides access to Google's Gemini Live model via Vertex AI.
This service enables real-time conversations with Gemini, supporting both
text and audio modalities. It handles voice transcription, streaming audio
responses, and tool usage.
"""
Settings = GeminiLiveVertexLLMSettings
_settings: GeminiLiveVertexLLMSettings
def __init__(
self,
*,
credentials: Optional[str] = None,
credentials_path: Optional[str] = None,
location: str,
project_id: str,
model: Optional[str] = None,
voice_id: str = "Charon",
start_audio_paused: bool = False,
start_video_paused: bool = False,
system_instruction: Optional[str] = None,
tools: Optional[Union[List[dict], ToolsSchema]] = None,
params: Optional[InputParams] = None,
settings: Optional[GeminiLiveVertexLLMSettings] = None,
inference_on_context_initialization: bool = True,
file_api_base_url: str = "https://generativelanguage.googleapis.com/v1beta/files",
http_options: Optional[HttpOptions] = None,
**kwargs,
):
"""Initialize the service for accessing Gemini Live via Google Vertex AI.
Args:
credentials: JSON string of service account credentials.
credentials_path: Path to the service account JSON file.
location: GCP region for Vertex AI endpoint (e.g., "us-east4").
project_id: Google Cloud project ID.
model: Model identifier to use.
.. deprecated:: 0.0.105
Use ``settings=GeminiLiveLLMSettings(model=...)`` instead.
voice_id: TTS voice identifier. Defaults to "Charon".
.. deprecated:: 0.0.105
Use ``settings=GeminiLiveVertexLLMSettings(voice=...)`` instead.
start_audio_paused: Whether to start with audio input paused. Defaults to False.
start_video_paused: Whether to start with video input paused. Defaults to False.
system_instruction: System prompt for the model. Defaults to None.
tools: Tools/functions available to the model. Defaults to None.
params: Configuration parameters for the model along with Vertex AI
location and project ID.
.. deprecated:: 0.0.105
Use ``settings=GeminiLiveLLMSettings(...)`` instead.
settings: Gemini Live LLM settings. If provided together with deprecated
top-level parameters, the ``settings`` values take precedence.
inference_on_context_initialization: Whether to generate a response when context
is first set. Defaults to True.
file_api_base_url: Base URL for the Gemini File API. Defaults to the official endpoint.
http_options: HTTP options for the client.
**kwargs: Additional arguments passed to parent GeminiLiveLLMService.
"""
# Check if user incorrectly passed api_key, which is used by parent
# class but not here.
if "api_key" in kwargs:
logger.error(
"GeminiLiveVertexLLMService does not accept 'api_key' parameter. "
"Use 'credentials' or 'credentials_path' instead for Vertex AI authentication."
)
raise ValueError(
"Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication."
)
# These need to be set before calling super().__init__() because
# super().__init__() invokes create_client(), which needs these.
self._credentials = self._get_credentials(credentials, credentials_path)
self._project_id = project_id
self._location = location
# Build default_settings from deprecated args, then apply settings delta.
# We pass settings= to super() instead of model=/params= to avoid
# double deprecation warnings from the parent.
# 1. Initialize default_settings with hardcoded defaults
default_settings = GeminiLiveVertexLLMSettings(
model="google/gemini-live-2.5-flash-native-audio",
voice="Charon",
frequency_penalty=None,
max_tokens=4096,
presence_penalty=None,
temperature=None,
top_k=None,
top_p=None,
seed=None,
filter_incomplete_user_turns=False,
user_turn_completion_config=None,
modalities=GeminiModalities.AUDIO,
language="en-US",
media_resolution=GeminiMediaResolution.UNSPECIFIED,
vad=None,
context_window_compression={},
thinking={},
enable_affective_dialog=False,
proactivity={},
extra={},
)
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GeminiLiveVertexLLMSettings, "model")
default_settings.model = model
if voice_id != "Charon":
_warn_deprecated_param("voice_id", GeminiLiveVertexLLMSettings, "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GeminiLiveVertexLLMSettings)
if not settings:
default_settings.frequency_penalty = params.frequency_penalty
default_settings.max_tokens = params.max_tokens
default_settings.presence_penalty = params.presence_penalty
default_settings.temperature = params.temperature
default_settings.top_k = params.top_k
default_settings.top_p = params.top_p
default_settings.modalities = params.modalities
default_settings.language = (
language_to_gemini_language(params.language) if params.language else "en-US"
)
default_settings.media_resolution = params.media_resolution
default_settings.vad = params.vad
default_settings.context_window_compression = (
params.context_window_compression.model_dump()
if params.context_window_compression
else {}
)
default_settings.thinking = params.thinking or {}
default_settings.enable_affective_dialog = params.enable_affective_dialog or False
default_settings.proactivity = params.proactivity or {}
if isinstance(params.extra, dict):
default_settings.extra = params.extra
# 4. Apply settings delta (canonical API, always wins)
if settings is not None:
default_settings.apply_update(settings)
# Call parent constructor with the obtained settings
super().__init__(
# api_key is required by parent class, but actually not used with
# Vertex
api_key="dummy",
start_audio_paused=start_audio_paused,
start_video_paused=start_video_paused,
system_instruction=system_instruction,
tools=tools,
settings=default_settings,
inference_on_context_initialization=inference_on_context_initialization,
file_api_base_url=file_api_base_url,
http_options=http_options,
**kwargs,
)
def create_client(self):
"""Create the Gemini client instance."""
self._client = Client(
vertexai=True,
credentials=self._credentials,
project=self._project_id,
location=self._location,
http_options=self._http_options,
)
@property
def file_api(self):
"""Gemini File API is not supported with Vertex AI."""
raise NotImplementedError(
"When using Vertex AI, the recommended approach is to use Google Cloud Storage for file handling. The Gemini File API is not directly supported in this context."
)
@staticmethod
def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]) -> str:
"""Retrieve Credentials using Google service account credentials JSON.
Supports multiple authentication methods:
1. Direct JSON credentials string
2. Path to service account JSON file
3. Default application credentials (ADC)
Args:
credentials: JSON string of service account credentials.
credentials_path: Path to the service account JSON file.
Returns:
OAuth token for API authentication.
Raises:
ValueError: If no valid credentials are provided or found.
"""
creds: Optional[service_account.Credentials] = None
if credentials:
# Parse and load credentials from JSON string
creds = service_account.Credentials.from_service_account_info(
json.loads(credentials),
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
elif credentials_path:
# Load credentials from JSON file
creds = service_account.Credentials.from_service_account_file(
credentials_path,
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
else:
try:
creds, project_id = default(
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
except GoogleAuthError:
pass
if not creds:
raise ValueError("No valid credentials provided.")
creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour.
return creds
from pipecat.services.google.gemini_live.vertex.llm import * # noqa: E402, F401, F403

View File

@@ -0,0 +1,5 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#

View File

@@ -0,0 +1,278 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Service for accessing Gemini Live via Google Vertex AI.
This module provides integration with Google's Gemini Live model via
Vertex AI, supporting both text and audio modalities with voice transcription,
streaming responses, and tool usage.
"""
import json
from dataclasses import dataclass
from typing import List, Optional, Union
from loguru import logger
from pipecat.adapters.schemas.tools_schema import ToolsSchema
from pipecat.services.google.gemini_live.llm import (
GeminiLiveLLMService,
GeminiMediaResolution,
GeminiModalities,
HttpOptions,
InputParams,
language_to_gemini_language,
)
try:
from google.auth import default
from google.auth.exceptions import GoogleAuthError
from google.auth.transport.requests import Request
from google.genai import Client
from google.oauth2 import service_account
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error("In order to use Google Vertex AI, you need to `pip install pipecat-ai[google]`.")
raise Exception(f"Missing module: {e}")
@dataclass
class GeminiLiveVertexLLMSettings(GeminiLiveLLMService.Settings):
"""Settings for GeminiLiveVertexLLMService."""
pass
class GeminiLiveVertexLLMService(GeminiLiveLLMService):
"""Provides access to Google's Gemini Live model via Vertex AI.
This service enables real-time conversations with Gemini, supporting both
text and audio modalities. It handles voice transcription, streaming audio
responses, and tool usage.
"""
Settings = GeminiLiveVertexLLMSettings
_settings: Settings
def __init__(
self,
*,
credentials: Optional[str] = None,
credentials_path: Optional[str] = None,
location: str,
project_id: str,
model: Optional[str] = None,
voice_id: str = "Charon",
start_audio_paused: bool = False,
start_video_paused: bool = False,
system_instruction: Optional[str] = None,
tools: Optional[Union[List[dict], ToolsSchema]] = None,
params: Optional[InputParams] = None,
settings: Optional[Settings] = None,
inference_on_context_initialization: bool = True,
file_api_base_url: str = "https://generativelanguage.googleapis.com/v1beta/files",
http_options: Optional[HttpOptions] = None,
**kwargs,
):
"""Initialize the service for accessing Gemini Live via Google Vertex AI.
Args:
credentials: JSON string of service account credentials.
credentials_path: Path to the service account JSON file.
location: GCP region for Vertex AI endpoint (e.g., "us-east4").
project_id: Google Cloud project ID.
model: Model identifier to use.
.. deprecated:: 0.0.105
Use ``settings=GeminiLiveVertexLLMService.Settings(model=...)`` instead.
voice_id: TTS voice identifier. Defaults to "Charon".
.. deprecated:: 0.0.105
Use ``settings=GeminiLiveVertexLLMService.Settings(voice=...)`` instead.
start_audio_paused: Whether to start with audio input paused. Defaults to False.
start_video_paused: Whether to start with video input paused. Defaults to False.
system_instruction: System prompt for the model. Defaults to None.
tools: Tools/functions available to the model. Defaults to None.
params: Configuration parameters for the model along with Vertex AI
location and project ID.
.. deprecated:: 0.0.105
Use ``settings=GeminiLiveVertexLLMService.Settings(...)`` instead.
settings: Gemini Live LLM settings. If provided together with deprecated
top-level parameters, the ``settings`` values take precedence.
inference_on_context_initialization: Whether to generate a response when context
is first set. Defaults to True.
file_api_base_url: Base URL for the Gemini File API. Defaults to the official endpoint.
http_options: HTTP options for the client.
**kwargs: Additional arguments passed to parent GeminiLiveLLMService.
"""
# Check if user incorrectly passed api_key, which is used by parent
# class but not here.
if "api_key" in kwargs:
logger.error(
"GeminiLiveVertexLLMService does not accept 'api_key' parameter. "
"Use 'credentials' or 'credentials_path' instead for Vertex AI authentication."
)
raise ValueError(
"Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication."
)
# These need to be set before calling super().__init__() because
# super().__init__() invokes create_client(), which needs these.
self._credentials = self._get_credentials(credentials, credentials_path)
self._project_id = project_id
self._location = location
# Build default_settings from deprecated args, then apply settings delta.
# We pass settings= to super() instead of model=/params= to avoid
# double deprecation warnings from the parent.
# 1. Initialize default_settings with hardcoded defaults
default_settings = self.Settings(
model="google/gemini-live-2.5-flash-native-audio",
voice="Charon",
frequency_penalty=None,
max_tokens=4096,
presence_penalty=None,
temperature=None,
top_k=None,
top_p=None,
seed=None,
filter_incomplete_user_turns=False,
user_turn_completion_config=None,
modalities=GeminiModalities.AUDIO,
language="en-US",
media_resolution=GeminiMediaResolution.UNSPECIFIED,
vad=None,
context_window_compression={},
thinking={},
enable_affective_dialog=False,
proactivity={},
extra={},
)
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if voice_id != "Charon":
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.frequency_penalty = params.frequency_penalty
default_settings.max_tokens = params.max_tokens
default_settings.presence_penalty = params.presence_penalty
default_settings.temperature = params.temperature
default_settings.top_k = params.top_k
default_settings.top_p = params.top_p
default_settings.modalities = params.modalities
default_settings.language = (
language_to_gemini_language(params.language) if params.language else "en-US"
)
default_settings.media_resolution = params.media_resolution
default_settings.vad = params.vad
default_settings.context_window_compression = (
params.context_window_compression.model_dump()
if params.context_window_compression
else {}
)
default_settings.thinking = params.thinking or {}
default_settings.enable_affective_dialog = params.enable_affective_dialog or False
default_settings.proactivity = params.proactivity or {}
if isinstance(params.extra, dict):
default_settings.extra = params.extra
# 4. Apply settings delta (canonical API, always wins)
if settings is not None:
default_settings.apply_update(settings)
# Call parent constructor with the obtained settings
super().__init__(
# api_key is required by parent class, but actually not used with
# Vertex
api_key="dummy",
start_audio_paused=start_audio_paused,
start_video_paused=start_video_paused,
system_instruction=system_instruction,
tools=tools,
settings=default_settings,
inference_on_context_initialization=inference_on_context_initialization,
file_api_base_url=file_api_base_url,
http_options=http_options,
**kwargs,
)
def create_client(self):
"""Create the Gemini client instance."""
self._client = Client(
vertexai=True,
credentials=self._credentials,
project=self._project_id,
location=self._location,
http_options=self._http_options,
)
@property
def file_api(self):
"""Gemini File API is not supported with Vertex AI."""
raise NotImplementedError(
"When using Vertex AI, the recommended approach is to use Google Cloud Storage for file handling. The Gemini File API is not directly supported in this context."
)
@staticmethod
def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]) -> str:
"""Retrieve Credentials using Google service account credentials JSON.
Supports multiple authentication methods:
1. Direct JSON credentials string
2. Path to service account JSON file
3. Default application credentials (ADC)
Args:
credentials: JSON string of service account credentials.
credentials_path: Path to the service account JSON file.
Returns:
OAuth token for API authentication.
Raises:
ValueError: If no valid credentials are provided or found.
"""
creds: Optional[service_account.Credentials] = None
if credentials:
# Parse and load credentials from JSON string
creds = service_account.Credentials.from_service_account_info(
json.loads(credentials),
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
elif credentials_path:
# Load credentials from JSON file
creds = service_account.Credentials.from_service_account_file(
credentials_path,
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
else:
try:
creds, project_id = default(
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
except GoogleAuthError:
pass
if not creds:
raise ValueError("No valid credentials provided.")
creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour.
return creds

View File

@@ -13,12 +13,12 @@ from pipecat.services import DeprecatedModuleProxy
from .frames import *
from .image import *
from .llm import *
from .llm_openai import *
from .llm_vertex import *
from .openai import *
from .rtvi import *
from .stt import *
from .tts import *
from .vertex import *
sys.modules[__name__] = DeprecatedModuleProxy(
globals(), "google", "google.[frames,image,llm,llm_openai,llm_vertex,rtvi,stt,tts]"
globals(), "google", "google.[frames,image,llm,openai,vertex,rtvi,stt,tts]"
)

View File

@@ -26,7 +26,7 @@ from pydantic import BaseModel, Field
from pipecat.frames.frames import ErrorFrame, Frame, URLImageRawFrame
from pipecat.services.google.utils import update_google_client_http_options
from pipecat.services.image_service import ImageGenService
from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven
try:
from google import genai
@@ -60,13 +60,13 @@ class GoogleImageGenService(ImageGenService):
"""
Settings = GoogleImageGenSettings
_settings: GoogleImageGenSettings
_settings: Settings
class InputParams(BaseModel):
"""Configuration parameters for Google image generation.
.. deprecated:: 0.0.105
Use ``settings=GoogleImageGenSettings(...)`` instead.
Use ``settings=GoogleImageGenService.Settings(...)`` instead.
Parameters:
number_of_images: Number of images to generate (1-8). Defaults to 1.
@@ -84,7 +84,7 @@ class GoogleImageGenService(ImageGenService):
api_key: str,
params: Optional[InputParams] = None,
http_options: Optional[Any] = None,
settings: Optional[GoogleImageGenSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the GoogleImageGenService with API key and parameters.
@@ -94,7 +94,7 @@ class GoogleImageGenService(ImageGenService):
params: Configuration parameters for image generation.
.. deprecated:: 0.0.105
Use ``settings=GoogleImageGenSettings(...)`` instead.
Use ``settings=GoogleImageGenService.Settings(...)`` instead.
http_options: HTTP options for the client.
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -102,7 +102,7 @@ class GoogleImageGenService(ImageGenService):
**kwargs: Additional arguments passed to the parent ImageGenService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = GoogleImageGenSettings(
default_settings = self.Settings(
model="imagen-3.0-generate-002",
number_of_images=1,
negative_prompt=None,
@@ -110,7 +110,7 @@ class GoogleImageGenService(ImageGenService):
# 2. Apply params overrides (deprecated)
if params is not None:
_warn_deprecated_param("params", GoogleImageGenSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.model = params.model
default_settings.number_of_images = params.number_of_images

View File

@@ -16,7 +16,7 @@ import json
import os
import uuid
from dataclasses import dataclass, field
from typing import Any, AsyncIterator, Dict, List, Literal, Optional
from typing import Any, AsyncIterator, Dict, List, Literal, Optional, Union
from loguru import logger
from PIL import Image
@@ -61,7 +61,6 @@ from pipecat.services.settings import (
NOT_GIVEN,
LLMSettings,
_NotGiven,
_warn_deprecated_param,
is_given,
)
from pipecat.utils.tracing.service_decorators import traced_llm
@@ -201,7 +200,9 @@ class GoogleAssistantContextAggregator(OpenAIAssistantContextAggregator):
if message.role == "user":
for part in message.parts:
if part.function_response and part.function_response.id == tool_call_id:
part.function_response.response = {"value": json.dumps(result)}
part.function_response.response = {
"value": json.dumps(result, ensure_ascii=False)
}
@dataclass
@@ -719,18 +720,20 @@ class GoogleLLMSettings(LLMSettings):
thinking: Thinking configuration.
"""
thinking: GoogleThinkingConfig | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
thinking: Union["GoogleLLMService.ThinkingConfig", _NotGiven] = field(
default_factory=lambda: NOT_GIVEN
)
@classmethod
def from_mapping(cls, settings):
"""Convert a plain dict to settings, coercing thinking dicts.
For backward compatibility, a ``thinking`` value that is a plain dict
is converted to a :class:`GoogleThinkingConfig`.
is converted to a :class:`GoogleLLMService.ThinkingConfig`.
"""
instance = super().from_mapping(settings)
if is_given(instance.thinking) and isinstance(instance.thinking, dict):
instance.thinking = GoogleThinkingConfig(**instance.thinking)
instance.thinking = GoogleLLMService.ThinkingConfig(**instance.thinking)
return instance
@@ -743,7 +746,7 @@ class GoogleLLMService(LLMService):
"""
Settings = GoogleLLMSettings
_settings: GoogleLLMSettings
_settings: Settings
# Overriding the default adapter to use the Gemini one.
adapter_class = GeminiLLMAdapter
@@ -755,7 +758,7 @@ class GoogleLLMService(LLMService):
"""Input parameters for Google AI models.
.. deprecated:: 0.0.105
Use ``settings=GoogleLLMSettings(...)`` instead.
Use ``settings=GoogleLLMService.Settings(...)`` instead.
Parameters:
max_tokens: Maximum number of tokens to generate.
@@ -775,7 +778,7 @@ class GoogleLLMService(LLMService):
temperature: Optional[float] = Field(default=None, ge=0.0, le=2.0)
top_k: Optional[int] = Field(default=None, ge=0)
top_p: Optional[float] = Field(default=None, ge=0.0, le=1.0)
thinking: Optional[GoogleThinkingConfig] = Field(default=None)
thinking: Optional["GoogleLLMService.ThinkingConfig"] = Field(default=None)
extra: Optional[Dict[str, Any]] = Field(default_factory=dict)
def __init__(
@@ -784,7 +787,7 @@ class GoogleLLMService(LLMService):
api_key: str,
model: Optional[str] = None,
params: Optional[InputParams] = None,
settings: Optional[GoogleLLMSettings] = None,
settings: Optional[Settings] = None,
system_instruction: Optional[str] = None,
tools: Optional[List[Dict[str, Any]]] = None,
tool_config: Optional[Dict[str, Any]] = None,
@@ -798,12 +801,12 @@ class GoogleLLMService(LLMService):
model: Model name to use.
.. deprecated:: 0.0.105
Use ``settings=GoogleLLMSettings(model=...)`` instead.
Use ``settings=GoogleLLMService.Settings(model=...)`` instead.
params: Optional model parameters for inference.
.. deprecated:: 0.0.105
Use ``settings=GoogleLLMSettings(...)`` instead.
Use ``settings=GoogleLLMService.Settings(...)`` instead.
settings: Runtime-updatable settings for this service. When both
deprecated parameters and *settings* are provided, *settings*
@@ -811,14 +814,14 @@ class GoogleLLMService(LLMService):
system_instruction: System instruction/prompt for the model.
.. deprecated:: 0.0.105
Use ``settings=GoogleLLMSettings(system_instruction=...)`` instead.
Use ``settings=GoogleLLMService.Settings(system_instruction=...)`` instead.
tools: List of available tools/functions.
tool_config: Configuration for tool usage.
http_options: HTTP options for the client.
**kwargs: Additional arguments passed to parent class.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = GoogleLLMSettings(
default_settings = self.Settings(
model="gemini-2.5-flash",
system_instruction=None,
max_tokens=4096,
@@ -836,15 +839,15 @@ class GoogleLLMService(LLMService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GoogleLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if system_instruction is not None:
_warn_deprecated_param("system_instruction", GoogleLLMSettings, "system_instruction")
self._warn_init_param_moved_to_settings("system_instruction", "system_instruction")
default_settings.system_instruction = system_instruction
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GoogleLLMSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.max_tokens = params.max_tokens
default_settings.temperature = params.temperature
@@ -881,7 +884,10 @@ class GoogleLLMService(LLMService):
self._client = genai.Client(api_key=self._api_key, http_options=self._http_options)
async def run_inference(
self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None
self,
context: LLMContext | OpenAILLMContext,
max_tokens: Optional[int] = None,
system_instruction: Optional[str] = None,
) -> Optional[str]:
"""Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context.
@@ -889,6 +895,8 @@ class GoogleLLMService(LLMService):
context: The LLM context containing conversation history.
max_tokens: Optional maximum number of tokens to generate. If provided,
overrides the service's default max_tokens setting.
system_instruction: Optional system instruction to use for this inference.
If provided, overrides any system instruction in the context.
Returns:
The LLM's response as a string, or None if no response is generated.
@@ -908,6 +916,15 @@ class GoogleLLMService(LLMService):
system = getattr(context, "system_message", None)
tools = context.tools or []
# Override system instruction if provided
if system_instruction is not None:
if system:
logger.warning(
f"{self}: Both system_instruction and a system message in context are set."
" Using system_instruction."
)
system = system_instruction
# Build generation config using the same method as streaming
generation_params = self._build_generation_params(
system_instruction=system, tools=tools if tools else None
@@ -997,12 +1014,16 @@ class GoogleLLMService(LLMService):
self, params_from_context: GeminiLLMInvocationParams
) -> AsyncIterator[GenerateContentResponse]:
messages = params_from_context["messages"]
if (
params_from_context["system_instruction"]
and self._settings.system_instruction != params_from_context["system_instruction"]
):
logger.debug(f"System instruction changed: {params_from_context['system_instruction']}")
self._settings.system_instruction = params_from_context["system_instruction"]
# Constructor/settings system instruction takes priority over context.
if self._settings.system_instruction and params_from_context["system_instruction"]:
logger.warning(
f"{self}: Both system_instruction and a system message in context are"
" set. Using system_instruction."
)
system_instruction = (
self._settings.system_instruction or params_from_context["system_instruction"]
)
tools = []
if params_from_context["tools"]:
@@ -1015,7 +1036,7 @@ class GoogleLLMService(LLMService):
# Build generation parameters
generation_params = self._build_generation_params(
system_instruction=self._settings.system_instruction,
system_instruction=system_instruction,
tools=tools,
tool_config=tool_config,
)

View File

@@ -4,211 +4,15 @@
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Google LLM service using OpenAI-compatible API format.
"""Deprecated: use ``pipecat.services.google.openai.llm`` instead."""
This module provides integration with Google's AI LLM models using the OpenAI
API format through Google's Gemini API OpenAI compatibility layer.
"""
import warnings
import json
import os
from dataclasses import dataclass
from typing import Optional
warnings.warn(
"Module `pipecat.services.google.llm_openai` is deprecated, "
"use `pipecat.services.google.openai.llm` instead.",
DeprecationWarning,
stacklevel=2,
)
from openai import AsyncStream
from openai.types.chat import ChatCompletionChunk
from pipecat.services.llm_service import FunctionCallFromLLM
# Suppress gRPC fork warnings
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
from loguru import logger
from pipecat.frames.frames import LLMTextFrame
from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class GoogleOpenAILLMSettings(OpenAILLMSettings):
"""Settings for GoogleLLMOpenAIBetaService."""
pass
class GoogleLLMOpenAIBetaService(OpenAILLMService):
"""Google LLM service using OpenAI-compatible API format.
This service provides access to Google's AI LLM models (like Gemini) through
the OpenAI API format. It handles streaming responses, function calls, and
tool usage while maintaining compatibility with OpenAI's interface.
Note: This service includes a workaround for a Google API bug where function
call indices may be incorrectly set to None, resulting in empty function names.
.. deprecated:: 0.0.82
GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version.
Use GoogleLLMService instead for better integration with Google's native API.
Reference:
https://ai.google.dev/gemini-api/docs/openai
"""
Settings = GoogleOpenAILLMSettings
_settings: GoogleOpenAILLMSettings
def __init__(
self,
*,
api_key: str,
base_url: str = "https://generativelanguage.googleapis.com/v1beta/openai/",
model: Optional[str] = None,
settings: Optional[GoogleOpenAILLMSettings] = None,
**kwargs,
):
"""Initialize the Google LLM service.
Args:
api_key: Google API key for authentication.
base_url: Base URL for Google's OpenAI-compatible API.
model: Google model name to use (e.g., "gemini-2.0-flash").
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to the parent OpenAILLMService.
"""
import warnings
with warnings.catch_warnings():
warnings.simplefilter("always")
warnings.warn(
"GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. "
"Use GoogleLLMService instead for better integration with Google's native API.",
DeprecationWarning,
stacklevel=2,
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = GoogleOpenAILLMSettings(model="gemini-2.0-flash")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GoogleOpenAILLMSettings, "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)
# 4. Apply settings delta (canonical API, always wins)
if settings is not None:
default_settings.apply_update(settings)
super().__init__(api_key=api_key, base_url=base_url, settings=default_settings, **kwargs)
async def _process_context(self, context: OpenAILLMContext):
functions_list = []
arguments_list = []
tool_id_list = []
func_idx = 0
function_name = ""
arguments = ""
tool_call_id = ""
await self.start_ttfb_metrics()
chunk_stream: AsyncStream[
ChatCompletionChunk
] = await self._stream_chat_completions_specific_context(context)
# Use context manager to ensure stream is closed on cancellation/exception.
# Without this, CancelledError during iteration leaves the underlying socket open.
async with chunk_stream:
async for chunk in chunk_stream:
if chunk.usage:
tokens = LLMTokenUsage(
prompt_tokens=chunk.usage.prompt_tokens or 0,
completion_tokens=chunk.usage.completion_tokens or 0,
total_tokens=chunk.usage.total_tokens or 0,
)
await self.start_llm_usage_metrics(tokens)
if chunk.choices is None or len(chunk.choices) == 0:
continue
await self.stop_ttfb_metrics()
if not chunk.choices[0].delta:
continue
if chunk.choices[0].delta.tool_calls:
# We're streaming the LLM response to enable the fastest response times.
# For text, we just yield each chunk as we receive it and count on consumers
# to do whatever coalescing they need (eg. to pass full sentences to TTS)
#
# If the LLM is a function call, we'll do some coalescing here.
# If the response contains a function name, we'll yield a frame to tell consumers
# that they can start preparing to call the function with that name.
# We accumulate all the arguments for the rest of the streamed response, then when
# the response is done, we package up all the arguments and the function name and
# yield a frame containing the function name and the arguments.
logger.debug(f"Tool call: {chunk.choices[0].delta.tool_calls}")
tool_call = chunk.choices[0].delta.tool_calls[0]
if tool_call.index != func_idx:
functions_list.append(function_name)
arguments_list.append(arguments)
tool_id_list.append(tool_call_id)
function_name = ""
arguments = ""
tool_call_id = ""
func_idx += 1
if tool_call.function and tool_call.function.name:
function_name += tool_call.function.name
tool_call_id = tool_call.id
if tool_call.function and tool_call.function.arguments:
# Keep iterating through the response to collect all the argument fragments
arguments += tool_call.function.arguments
elif chunk.choices[0].delta.content:
await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content))
# if we got a function name and arguments, check to see if it's a function with
# a registered handler. If so, run the registered callback, save the result to
# the context, and re-prompt to get a chat answer. If we don't have a registered
# handler, raise an exception.
if function_name and arguments:
# added to the list as last function name and arguments not added to the list
functions_list.append(function_name)
arguments_list.append(arguments)
tool_id_list.append(tool_call_id)
logger.debug(
f"Function list: {functions_list}, Arguments list: {arguments_list}, Tool ID list: {tool_id_list}"
)
function_calls = []
for function_name, arguments, tool_id in zip(
functions_list, arguments_list, tool_id_list
):
if function_name == "":
# TODO: Remove the _process_context method once Google resolves the bug
# where the index is incorrectly set to None instead of returning the actual index,
# which currently results in an empty function name('').
continue
arguments = json.loads(arguments)
function_calls.append(
FunctionCallFromLLM(
context=context,
tool_call_id=tool_id,
function_name=function_name,
arguments=arguments,
)
)
await self.run_function_calls(function_calls)
from pipecat.services.google.openai.llm import * # noqa: E402, F401, F403

View File

@@ -4,306 +4,15 @@
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Google Vertex AI LLM service implementation.
"""Deprecated: use ``pipecat.services.google.vertex.llm`` instead."""
This module provides integration with Google's AI models via Vertex AI,
extending the GoogleLLMService with Vertex AI authentication.
"""
import warnings
import json
import os
from dataclasses import dataclass
warnings.warn(
"Module `pipecat.services.google.llm_vertex` is deprecated, "
"use `pipecat.services.google.vertex.llm` instead.",
DeprecationWarning,
stacklevel=2,
)
# Suppress gRPC fork warnings
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
from typing import Optional
from loguru import logger
from pipecat.services.google.llm import GoogleLLMService, GoogleLLMSettings
from pipecat.services.settings import _warn_deprecated_param
try:
from google.auth import default
from google.auth.exceptions import GoogleAuthError
from google.auth.transport.requests import Request
from google.genai import Client
from google.genai.types import HttpOptions
from google.oauth2 import service_account
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error(
"In order to use Google AI, you need to `pip install pipecat-ai[google]`. Also, set `GOOGLE_APPLICATION_CREDENTIALS` environment variable."
)
raise Exception(f"Missing module: {e}")
@dataclass
class GoogleVertexLLMSettings(GoogleLLMSettings):
"""Settings for GoogleVertexLLMService."""
pass
class GoogleVertexLLMService(GoogleLLMService):
"""Google Vertex AI LLM service extending GoogleLLMService.
Provides access to Google's AI models via Vertex AI while using the same
Google AI client and message format as GoogleLLMService. Handles authentication
using Google service account credentials and configures the client for
Vertex AI endpoints.
Reference:
https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference
"""
Settings = GoogleVertexLLMSettings
_settings: GoogleVertexLLMSettings
class InputParams(GoogleLLMService.InputParams):
"""Input parameters specific to Vertex AI.
Parameters:
location: GCP region for Vertex AI endpoint (e.g., "us-east4").
.. deprecated:: 0.0.90
Use `location` as a direct argument to
`GoogleVertexLLMService.__init__()` instead.
project_id: Google Cloud project ID.
.. deprecated:: 0.0.90
Use `project_id` as a direct argument to
`GoogleVertexLLMService.__init__()` instead.
"""
# https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations
location: Optional[str] = None
project_id: Optional[str] = None
def __init__(self, **kwargs):
"""Initializes the InputParams."""
import warnings
with warnings.catch_warnings():
warnings.simplefilter("always")
if "location" in kwargs and kwargs["location"] is not None:
warnings.warn(
"GoogleVertexLLMService.InputParams.location is deprecated. "
"Please provide 'location' as a direct argument to GoogleVertexLLMService.__init__() instead.",
DeprecationWarning,
stacklevel=2,
)
if "project_id" in kwargs and kwargs["project_id"] is not None:
warnings.warn(
"GoogleVertexLLMService.InputParams.project_id is deprecated. "
"Please provide 'project_id' as a direct argument to GoogleVertexLLMService.__init__() instead.",
DeprecationWarning,
stacklevel=2,
)
super().__init__(**kwargs)
def __init__(
self,
*,
credentials: Optional[str] = None,
credentials_path: Optional[str] = None,
model: Optional[str] = None,
location: Optional[str] = None,
project_id: Optional[str] = None,
params: Optional[GoogleLLMService.InputParams] = None,
settings: Optional[GoogleVertexLLMSettings] = None,
system_instruction: Optional[str] = None,
tools: Optional[list] = None,
tool_config: Optional[dict] = None,
http_options: Optional[HttpOptions] = None,
**kwargs,
):
"""Initializes the VertexLLMService.
Args:
credentials: JSON string of service account credentials.
credentials_path: Path to the service account JSON file.
model: Model identifier (e.g., "gemini-2.5-flash").
.. deprecated:: 0.0.105
Use ``settings=GoogleLLMSettings(model=...)`` instead.
location: GCP region for Vertex AI endpoint (e.g., "us-east4").
project_id: Google Cloud project ID.
params: Input parameters for the model.
.. deprecated:: 0.0.105
Use ``settings=GoogleLLMSettings(...)`` instead.
settings: Runtime-updatable settings for this service. When both
deprecated parameters and *settings* are provided, *settings*
values take precedence.
system_instruction: System instruction/prompt for the model.
.. deprecated:: 0.0.105
Use ``settings=GoogleVertexLLMSettings(system_instruction=...)`` instead.
tools: List of available tools/functions.
tool_config: Configuration for tool usage.
http_options: HTTP options for the client.
**kwargs: Additional arguments passed to GoogleLLMService.
"""
# Check if user incorrectly passed api_key, which is used by parent
# class but not here.
if "api_key" in kwargs:
logger.error(
"GoogleVertexLLMService does not accept 'api_key' parameter. "
"Use 'credentials' or 'credentials_path' instead for Vertex AI authentication."
)
raise ValueError(
"Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication."
)
# Handle deprecated InputParams fields (location/project_id extraction
# must happen before validation, regardless of settings)
if params and isinstance(params, GoogleVertexLLMService.InputParams):
if project_id is None:
project_id = params.project_id
if location is None:
location = params.location
# Convert to base InputParams
params = GoogleLLMService.InputParams(
**params.model_dump(exclude={"location", "project_id"}, exclude_unset=True)
)
# Validate project_id and location parameters
# NOTE: once we remove Vertex-specific InputParams class, we can update
# __init__() signature as follows:
# - location: str = "us-east4",
# - project_id: str,
# But for now, we need them as-is to maintain proper backward
# compatibility.
if project_id is None:
raise ValueError("project_id is required")
if location is None:
# If location is not provided, default to "us-east4".
# Note: this is legacy behavior; ideally location would be
# required.
logger.warning("location is not provided. Defaulting to 'us-east4'.")
location = "us-east4" # Default location if not provided
# These need to be set before calling super().__init__() because
# super().__init__() invokes _create_client(), which needs these.
self._credentials = self._get_credentials(credentials, credentials_path)
self._project_id = project_id
self._location = location
# 1. Initialize default_settings with hardcoded defaults
default_settings = GoogleVertexLLMSettings(
model="gemini-2.5-flash",
system_instruction=None,
max_tokens=4096,
temperature=None,
top_k=None,
top_p=None,
frequency_penalty=None,
presence_penalty=None,
seed=None,
filter_incomplete_user_turns=False,
user_turn_completion_config=None,
thinking=None,
extra={},
)
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GoogleVertexLLMSettings, "model")
default_settings.model = model
if system_instruction is not None:
_warn_deprecated_param(
"system_instruction", GoogleVertexLLMSettings, "system_instruction"
)
default_settings.system_instruction = system_instruction
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GoogleVertexLLMSettings)
if not settings:
default_settings.max_tokens = params.max_tokens
default_settings.temperature = params.temperature
default_settings.top_k = params.top_k
default_settings.top_p = params.top_p
default_settings.thinking = params.thinking
if isinstance(params.extra, dict):
default_settings.extra = params.extra
# 4. Apply settings delta (canonical API, always wins)
if settings is not None:
default_settings.apply_update(settings)
# Call parent constructor with dummy api_key
# (api_key is required by parent class, but not actually used with Vertex)
super().__init__(
api_key="dummy",
settings=default_settings,
tools=tools,
tool_config=tool_config,
http_options=http_options,
**kwargs,
)
def create_client(self):
"""Create the Gemini client instance configured for Vertex AI."""
self._client = Client(
vertexai=True,
credentials=self._credentials,
project=self._project_id,
location=self._location,
http_options=self._http_options,
)
@staticmethod
def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]):
"""Retrieve Credentials using Google service account credentials.
Supports multiple authentication methods:
1. Direct JSON credentials string
2. Path to service account JSON file
3. Default application credentials (ADC)
Args:
credentials: JSON string of service account credentials.
credentials_path: Path to the service account JSON file.
Returns:
Google credentials object for API authentication.
Raises:
ValueError: If no valid credentials are provided or found.
"""
creds: Optional[service_account.Credentials] = None
if credentials:
# Parse and load credentials from JSON string
creds = service_account.Credentials.from_service_account_info(
json.loads(credentials),
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
elif credentials_path:
# Load credentials from JSON file
creds = service_account.Credentials.from_service_account_file(
credentials_path,
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
else:
try:
creds, project_id = default(
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
except GoogleAuthError:
pass
if not creds:
raise ValueError("No valid credentials provided.")
creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour.
return creds
from pipecat.services.google.vertex.llm import * # noqa: E402, F401, F403

View File

@@ -0,0 +1,5 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#

View File

@@ -0,0 +1,213 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Google LLM service using OpenAI-compatible API format.
This module provides integration with Google's AI LLM models using the OpenAI
API format through Google's Gemini API OpenAI compatibility layer.
"""
import json
import os
from dataclasses import dataclass
from typing import Optional
from openai import AsyncStream
from openai.types.chat import ChatCompletionChunk
from pipecat.services.llm_service import FunctionCallFromLLM
# Suppress gRPC fork warnings
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
from loguru import logger
from pipecat.frames.frames import LLMTextFrame
from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
@dataclass
class GoogleOpenAILLMSettings(BaseOpenAILLMService.Settings):
"""Settings for GoogleLLMOpenAIBetaService."""
pass
class GoogleLLMOpenAIBetaService(OpenAILLMService):
"""Google LLM service using OpenAI-compatible API format.
This service provides access to Google's AI LLM models (like Gemini) through
the OpenAI API format. It handles streaming responses, function calls, and
tool usage while maintaining compatibility with OpenAI's interface.
Note: This service includes a workaround for a Google API bug where function
call indices may be incorrectly set to None, resulting in empty function names.
.. deprecated:: 0.0.82
GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version.
Use GoogleLLMService instead for better integration with Google's native API.
Reference:
https://ai.google.dev/gemini-api/docs/openai
"""
Settings = GoogleOpenAILLMSettings
_settings: Settings
def __init__(
self,
*,
api_key: str,
base_url: str = "https://generativelanguage.googleapis.com/v1beta/openai/",
model: Optional[str] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Google LLM service.
Args:
api_key: Google API key for authentication.
base_url: Base URL for Google's OpenAI-compatible API.
model: Google model name to use (e.g., "gemini-2.0-flash").
.. deprecated:: 0.0.105
Use ``settings=GoogleLLMOpenAIBetaService.Settings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to the parent OpenAILLMService.
"""
import warnings
with warnings.catch_warnings():
warnings.simplefilter("always")
warnings.warn(
"GoogleLLMOpenAIBetaService is deprecated and will be removed in a future version. "
"Use GoogleLLMService instead for better integration with Google's native API.",
DeprecationWarning,
stacklevel=2,
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = self.Settings(model="gemini-2.0-flash")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)
# 4. Apply settings delta (canonical API, always wins)
if settings is not None:
default_settings.apply_update(settings)
super().__init__(api_key=api_key, base_url=base_url, settings=default_settings, **kwargs)
async def _process_context(self, context: OpenAILLMContext):
functions_list = []
arguments_list = []
tool_id_list = []
func_idx = 0
function_name = ""
arguments = ""
tool_call_id = ""
await self.start_ttfb_metrics()
chunk_stream: AsyncStream[
ChatCompletionChunk
] = await self._stream_chat_completions_specific_context(context)
# Use context manager to ensure stream is closed on cancellation/exception.
# Without this, CancelledError during iteration leaves the underlying socket open.
async with chunk_stream:
async for chunk in chunk_stream:
if chunk.usage:
tokens = LLMTokenUsage(
prompt_tokens=chunk.usage.prompt_tokens or 0,
completion_tokens=chunk.usage.completion_tokens or 0,
total_tokens=chunk.usage.total_tokens or 0,
)
await self.start_llm_usage_metrics(tokens)
if chunk.choices is None or len(chunk.choices) == 0:
continue
await self.stop_ttfb_metrics()
if not chunk.choices[0].delta:
continue
if chunk.choices[0].delta.tool_calls:
# We're streaming the LLM response to enable the fastest response times.
# For text, we just yield each chunk as we receive it and count on consumers
# to do whatever coalescing they need (eg. to pass full sentences to TTS)
#
# If the LLM is a function call, we'll do some coalescing here.
# If the response contains a function name, we'll yield a frame to tell consumers
# that they can start preparing to call the function with that name.
# We accumulate all the arguments for the rest of the streamed response, then when
# the response is done, we package up all the arguments and the function name and
# yield a frame containing the function name and the arguments.
logger.debug(f"Tool call: {chunk.choices[0].delta.tool_calls}")
tool_call = chunk.choices[0].delta.tool_calls[0]
if tool_call.index != func_idx:
functions_list.append(function_name)
arguments_list.append(arguments)
tool_id_list.append(tool_call_id)
function_name = ""
arguments = ""
tool_call_id = ""
func_idx += 1
if tool_call.function and tool_call.function.name:
function_name += tool_call.function.name
tool_call_id = tool_call.id
if tool_call.function and tool_call.function.arguments:
# Keep iterating through the response to collect all the argument fragments
arguments += tool_call.function.arguments
elif chunk.choices[0].delta.content:
await self.push_frame(LLMTextFrame(chunk.choices[0].delta.content))
# if we got a function name and arguments, check to see if it's a function with
# a registered handler. If so, run the registered callback, save the result to
# the context, and re-prompt to get a chat answer. If we don't have a registered
# handler, raise an exception.
if function_name and arguments:
# added to the list as last function name and arguments not added to the list
functions_list.append(function_name)
arguments_list.append(arguments)
tool_id_list.append(tool_call_id)
logger.debug(
f"Function list: {functions_list}, Arguments list: {arguments_list}, Tool ID list: {tool_id_list}"
)
function_calls = []
for function_name, arguments, tool_id in zip(
functions_list, arguments_list, tool_id_list
):
if function_name == "":
# TODO: Remove the _process_context method once Google resolves the bug
# where the index is incorrectly set to None instead of returning the actual index,
# which currently results in an empty function name('').
continue
arguments = json.loads(arguments)
function_calls.append(
FunctionCallFromLLM(
context=context,
tool_call_id=tool_id,
function_name=function_name,
arguments=arguments,
)
)
await self.run_function_calls(function_calls)

View File

@@ -36,7 +36,7 @@ from pipecat.frames.frames import (
StartFrame,
TranscriptionFrame,
)
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven
from pipecat.services.stt_latency import GOOGLE_TTFS_P99
from pipecat.services.stt_service import STTService
from pipecat.transcriptions.language import Language, resolve_language
@@ -415,7 +415,7 @@ class GoogleSTTService(STTService):
"""
Settings = GoogleSTTSettings
_settings: GoogleSTTSettings
_settings: Settings
# Google Cloud's STT service has a connection time limit of 5 minutes per stream.
# They've shared an "endless streaming" example that guided this implementation:
@@ -427,7 +427,7 @@ class GoogleSTTService(STTService):
"""Configuration parameters for Google Speech-to-Text.
.. deprecated:: 0.0.105
Use ``settings=GoogleSTTSettings(...)`` instead.
Use ``settings=GoogleSTTService.Settings(...)`` instead.
Parameters:
languages: Single language or list of recognition languages. First language is primary.
@@ -488,7 +488,7 @@ class GoogleSTTService(STTService):
location: str = "global",
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
settings: Optional[GoogleSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = GOOGLE_TTFS_P99,
**kwargs,
):
@@ -502,7 +502,7 @@ class GoogleSTTService(STTService):
params: Configuration parameters for the service.
.. deprecated:: 0.0.105
Use ``settings=GoogleSTTSettings(...)`` instead.
Use ``settings=GoogleSTTService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
``params``, ``settings`` values take precedence.
@@ -511,7 +511,7 @@ class GoogleSTTService(STTService):
**kwargs: Additional arguments passed to STTService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = GoogleSTTSettings(
default_settings = self.Settings(
language=None,
languages=[Language.EN_US],
language_codes=None,
@@ -531,7 +531,7 @@ class GoogleSTTService(STTService):
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GoogleSTTSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.languages = list(params.language_list)
default_settings.model = params.model
@@ -655,7 +655,7 @@ class GoogleSTTService(STTService):
"""Update the service's recognition languages.
.. deprecated:: 0.0.104
Use ``STTUpdateSettingsFrame`` with ``GoogleSTTSettings(languages=...)``
Use ``STTUpdateSettingsFrame`` with ``GoogleSTTService.Settings(languages=...)``
instead.
Args:
@@ -665,13 +665,13 @@ class GoogleSTTService(STTService):
warnings.simplefilter("always")
warnings.warn(
"set_languages() is deprecated. Use STTUpdateSettingsFrame with "
"GoogleSTTSettings(languages=...) instead.",
"self.Settings(languages=...) instead.",
DeprecationWarning,
)
logger.debug(f"Switching STT languages to: {languages}")
await self._update_settings(GoogleSTTSettings(languages=list(languages)))
await self._update_settings(self.Settings(languages=list(languages)))
async def _update_settings(self, delta: GoogleSTTSettings) -> dict[str, Any]:
async def _update_settings(self, delta: Settings) -> dict[str, Any]:
"""Apply settings delta and reconnect if anything changed.
Handles ``language`` from base ``set_language`` by converting it to
@@ -698,8 +698,8 @@ class GoogleSTTService(STTService):
with warnings.catch_warnings():
warnings.simplefilter("always")
warnings.warn(
"GoogleSTTSettings.language_codes is deprecated. "
"Use GoogleSTTSettings.languages (List[Language]) instead.",
"self.Settings.language_codes is deprecated. "
"Use self.Settings.languages (List[Language]) instead.",
DeprecationWarning,
stacklevel=2,
)
@@ -756,7 +756,7 @@ class GoogleSTTService(STTService):
"""Update service options dynamically.
.. deprecated::
Use ``STTUpdateSettingsFrame`` with ``GoogleSTTSettings(...)``
Use ``STTUpdateSettingsFrame`` with ``GoogleSTTService.Settings(...)``
instead.
Args:
@@ -780,11 +780,11 @@ class GoogleSTTService(STTService):
warnings.simplefilter("always")
warnings.warn(
"update_options() is deprecated. Use STTUpdateSettingsFrame with "
"GoogleSTTSettings(...) instead.",
"self.Settings(...) instead.",
DeprecationWarning,
)
# Build a settings delta from the provided options
delta = GoogleSTTSettings()
delta = self.Settings()
if languages is not None:
delta.languages = list(languages)

View File

@@ -39,7 +39,6 @@ from pipecat.services.settings import (
NOT_GIVEN,
TTSSettings,
_NotGiven,
_warn_deprecated_param,
is_given,
)
from pipecat.services.tts_service import TTSService
@@ -523,7 +522,7 @@ class GoogleTTSSettings(TTSSettings):
#: .. deprecated:: 0.0.105
#: Use ``GoogleTTSSettings`` instead.
#: Use ``GoogleTTSService.Settings`` instead.
GoogleStreamTTSSettings = GoogleTTSSettings
@@ -559,13 +558,13 @@ class GoogleHttpTTSService(TTSService):
"""
Settings = GoogleHttpTTSSettings
_settings: GoogleHttpTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Google HTTP TTS voice customization.
.. deprecated:: 0.0.105
Use ``GoogleHttpTTSSettings`` directly via the ``settings`` parameter instead.
Use ``GoogleHttpTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
pitch: Voice pitch adjustment (e.g., "+2st", "-50%").
@@ -596,7 +595,7 @@ class GoogleHttpTTSService(TTSService):
voice_id: Optional[str] = None,
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
settings: Optional[GoogleHttpTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initializes the Google HTTP TTS service.
@@ -608,20 +607,20 @@ class GoogleHttpTTSService(TTSService):
voice_id: Google TTS voice identifier (e.g., "en-US-Standard-A").
.. deprecated:: 0.0.105
Use ``settings=GoogleHttpTTSSettings(voice=...)`` instead.
Use ``settings=GoogleHttpTTSService.Settings(voice=...)`` instead.
sample_rate: Audio sample rate in Hz. If None, uses default.
params: Voice customization parameters including pitch, rate, volume, etc.
.. deprecated:: 0.0.105
Use ``settings=GoogleHttpTTSSettings(...)`` instead.
Use ``settings=GoogleHttpTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to parent TTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = GoogleHttpTTSSettings(
default_settings = self.Settings(
model=None,
voice="en-US-Chirp3-HD-Charon",
language="en-US",
@@ -636,12 +635,12 @@ class GoogleHttpTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", GoogleHttpTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GoogleHttpTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.pitch is not None:
default_settings.pitch = params.pitch
@@ -654,7 +653,7 @@ class GoogleHttpTTSService(TTSService):
if params.emphasis is not None:
default_settings.emphasis = params.emphasis
if params.language is not None:
default_settings.language = self.language_to_service_language(params.language)
default_settings.language = params.language
if params.gender is not None:
default_settings.gender = params.gender
if params.google_style is not None:
@@ -747,7 +746,7 @@ class GoogleHttpTTSService(TTSService):
Args:
delta: Settings delta. Can include 'speaking_rate' (float).
"""
if isinstance(delta, GoogleHttpTTSSettings) and is_given(delta.speaking_rate):
if isinstance(delta, self.Settings) and is_given(delta.speaking_rate):
rate_value = float(delta.speaking_rate)
if not (0.25 <= rate_value <= 2.0):
logger.warning(
@@ -1014,21 +1013,21 @@ class GoogleTTSService(GoogleBaseTTSService):
tts = GoogleTTSService(
credentials_path="/path/to/service-account.json",
voice_id="en-US-Chirp3-HD-Charon",
params=GoogleTTSService.InputParams(
settings=GoogleTTSService.Settings(
voice="en-US-Chirp3-HD-Charon",
language=Language.EN_US,
)
)
"""
Settings = GoogleTTSSettings
_settings: GoogleTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Google streaming TTS configuration.
.. deprecated:: 0.0.105
Use ``GoogleTTSSettings`` directly via the ``settings`` parameter instead.
Use ``GoogleTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
language: Language for synthesis. Defaults to English.
@@ -1048,7 +1047,7 @@ class GoogleTTSService(GoogleBaseTTSService):
voice_cloning_key: Optional[str] = None,
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
settings: Optional[GoogleTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initializes the Google streaming TTS service.
@@ -1060,21 +1059,21 @@ class GoogleTTSService(GoogleBaseTTSService):
voice_id: Google TTS voice identifier (e.g., "en-US-Chirp3-HD-Charon").
.. deprecated:: 0.0.105
Use ``settings=GoogleTTSSettings(voice=...)`` instead.
Use ``settings=GoogleTTSService.Settings(voice=...)`` instead.
voice_cloning_key: The voice cloning key for Chirp 3 custom voices.
sample_rate: Audio sample rate in Hz. If None, uses default.
params: Language configuration parameters.
.. deprecated:: 0.0.105
Use ``settings=GoogleTTSSettings(...)`` instead.
Use ``settings=GoogleTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to parent TTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = GoogleTTSSettings(
default_settings = self.Settings(
model=None,
voice="en-US-Chirp3-HD-Charon",
language="en-US",
@@ -1083,15 +1082,15 @@ class GoogleTTSService(GoogleBaseTTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", GoogleTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GoogleTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = self.language_to_service_language(params.language)
default_settings.language = params.language
if params.speaking_rate is not None:
default_settings.speaking_rate = params.speaking_rate
@@ -1119,7 +1118,7 @@ class GoogleTTSService(GoogleBaseTTSService):
Args:
delta: Settings delta. Can include 'speaking_rate' (float).
"""
if isinstance(delta, GoogleTTSSettings) and is_given(delta.speaking_rate):
if isinstance(delta, self.Settings) and is_given(delta.speaking_rate):
rate_value = float(delta.speaking_rate)
if not (0.25 <= rate_value <= 2.0):
logger.warning(
@@ -1191,9 +1190,9 @@ class GeminiTTSService(GoogleBaseTTSService):
tts = GeminiTTSService(
credentials_path="/path/to/service-account.json",
model="gemini-2.5-flash-tts",
voice_id="Kore",
params=GeminiTTSService.InputParams(
settings=GeminiTTSService.Settings(
model="gemini-2.5-flash-tts",
voice="Kore",
language=Language.EN_US,
prompt="Say this in a friendly and helpful tone"
)
@@ -1201,7 +1200,7 @@ class GeminiTTSService(GoogleBaseTTSService):
"""
Settings = GeminiTTSSettings
_settings: GeminiTTSSettings
_settings: Settings
GOOGLE_SAMPLE_RATE = 24000 # Google TTS always outputs at 24kHz
@@ -1243,7 +1242,7 @@ class GeminiTTSService(GoogleBaseTTSService):
"""Input parameters for Gemini TTS configuration.
.. deprecated:: 0.0.105
Use ``GeminiTTSSettings`` directly via the ``settings`` parameter instead.
Use ``GeminiTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
language: Language for synthesis. Defaults to English.
@@ -1268,7 +1267,7 @@ class GeminiTTSService(GoogleBaseTTSService):
voice_id: Optional[str] = None,
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
settings: Optional[GeminiTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initializes the Gemini TTS service.
@@ -1284,7 +1283,7 @@ class GeminiTTSService(GoogleBaseTTSService):
"gemini-2.5-flash-tts" or "gemini-2.5-pro-tts".
.. deprecated:: 0.0.105
Use ``settings=GeminiTTSSettings(model=...)`` instead.
Use ``settings=GeminiTTSService.Settings(model=...)`` instead.
credentials: JSON string containing Google Cloud service account credentials.
credentials_path: Path to Google Cloud service account JSON file.
@@ -1292,13 +1291,13 @@ class GeminiTTSService(GoogleBaseTTSService):
voice_id: Voice name from the available Gemini voices.
.. deprecated:: 0.0.105
Use ``settings=GeminiTTSSettings(voice=...)`` instead.
Use ``settings=GeminiTTSService.Settings(voice=...)`` instead.
sample_rate: Audio sample rate in Hz. If None, uses Google's default 24kHz.
params: TTS configuration parameters.
.. deprecated:: 0.0.105
Use ``settings=GeminiTTSSettings(...)`` instead.
Use ``settings=GeminiTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -1320,7 +1319,7 @@ class GeminiTTSService(GoogleBaseTTSService):
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = GeminiTTSSettings(
default_settings = self.Settings(
model="gemini-2.5-flash-tts",
voice="Kore",
language="en-US",
@@ -1331,10 +1330,10 @@ class GeminiTTSService(GoogleBaseTTSService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GeminiTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if voice_id is not None:
_warn_deprecated_param("voice_id", GeminiTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if default_settings.voice not in self.AVAILABLE_VOICES:
@@ -1344,10 +1343,10 @@ class GeminiTTSService(GoogleBaseTTSService):
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GeminiTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = self.language_to_service_language(params.language)
default_settings.language = params.language
if params.prompt is not None:
default_settings.prompt = params.prompt
if params.multi_speaker is not None:

View File

@@ -0,0 +1,5 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#

View File

@@ -0,0 +1,306 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Google Vertex AI LLM service implementation.
This module provides integration with Google's AI models via Vertex AI,
extending the GoogleLLMService with Vertex AI authentication.
"""
import json
import os
from dataclasses import dataclass
# Suppress gRPC fork warnings
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
from typing import Optional
from loguru import logger
from pipecat.services.google.llm import GoogleLLMService
try:
from google.auth import default
from google.auth.exceptions import GoogleAuthError
from google.auth.transport.requests import Request
from google.genai import Client
from google.genai.types import HttpOptions
from google.oauth2 import service_account
except ModuleNotFoundError as e:
logger.error(f"Exception: {e}")
logger.error(
"In order to use Google AI, you need to `pip install pipecat-ai[google]`. Also, set `GOOGLE_APPLICATION_CREDENTIALS` environment variable."
)
raise Exception(f"Missing module: {e}")
@dataclass
class GoogleVertexLLMSettings(GoogleLLMService.Settings):
"""Settings for GoogleVertexLLMService."""
pass
class GoogleVertexLLMService(GoogleLLMService):
"""Google Vertex AI LLM service extending GoogleLLMService.
Provides access to Google's AI models via Vertex AI while using the same
Google AI client and message format as GoogleLLMService. Handles authentication
using Google service account credentials and configures the client for
Vertex AI endpoints.
Reference:
https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference
"""
Settings = GoogleVertexLLMSettings
_settings: Settings
class InputParams(GoogleLLMService.InputParams):
"""Input parameters specific to Vertex AI.
Parameters:
location: GCP region for Vertex AI endpoint (e.g., "us-east4").
.. deprecated:: 0.0.90
Use `location` as a direct argument to
`GoogleVertexLLMService.__init__()` instead.
project_id: Google Cloud project ID.
.. deprecated:: 0.0.90
Use `project_id` as a direct argument to
`GoogleVertexLLMService.__init__()` instead.
"""
# https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations
location: Optional[str] = None
project_id: Optional[str] = None
def __init__(self, **kwargs):
"""Initializes the InputParams."""
import warnings
with warnings.catch_warnings():
warnings.simplefilter("always")
if "location" in kwargs and kwargs["location"] is not None:
warnings.warn(
"GoogleVertexLLMService.InputParams.location is deprecated. "
"Please provide 'location' as a direct argument to GoogleVertexLLMService.__init__() instead.",
DeprecationWarning,
stacklevel=2,
)
if "project_id" in kwargs and kwargs["project_id"] is not None:
warnings.warn(
"GoogleVertexLLMService.InputParams.project_id is deprecated. "
"Please provide 'project_id' as a direct argument to GoogleVertexLLMService.__init__() instead.",
DeprecationWarning,
stacklevel=2,
)
super().__init__(**kwargs)
def __init__(
self,
*,
credentials: Optional[str] = None,
credentials_path: Optional[str] = None,
model: Optional[str] = None,
location: Optional[str] = None,
project_id: Optional[str] = None,
params: Optional[GoogleLLMService.InputParams] = None,
settings: Optional[Settings] = None,
system_instruction: Optional[str] = None,
tools: Optional[list] = None,
tool_config: Optional[dict] = None,
http_options: Optional[HttpOptions] = None,
**kwargs,
):
"""Initializes the VertexLLMService.
Args:
credentials: JSON string of service account credentials.
credentials_path: Path to the service account JSON file.
model: Model identifier (e.g., "gemini-2.5-flash").
.. deprecated:: 0.0.105
Use ``settings=GoogleVertexLLMService.Settings(model=...)`` instead.
location: GCP region for Vertex AI endpoint (e.g., "us-east4").
project_id: Google Cloud project ID.
params: Input parameters for the model.
.. deprecated:: 0.0.105
Use ``settings=GoogleVertexLLMService.Settings(...)`` instead.
settings: Runtime-updatable settings for this service. When both
deprecated parameters and *settings* are provided, *settings*
values take precedence.
system_instruction: System instruction/prompt for the model.
.. deprecated:: 0.0.105
Use ``settings=GoogleVertexLLMService.Settings(system_instruction=...)`` instead.
tools: List of available tools/functions.
tool_config: Configuration for tool usage.
http_options: HTTP options for the client.
**kwargs: Additional arguments passed to GoogleLLMService.
"""
# Check if user incorrectly passed api_key, which is used by parent
# class but not here.
if "api_key" in kwargs:
logger.error(
"GoogleVertexLLMService does not accept 'api_key' parameter. "
"Use 'credentials' or 'credentials_path' instead for Vertex AI authentication."
)
raise ValueError(
"Invalid parameter 'api_key'. Use 'credentials' or 'credentials_path' for Vertex AI authentication."
)
# Handle deprecated InputParams fields (location/project_id extraction
# must happen before validation, regardless of settings)
if params and isinstance(params, GoogleVertexLLMService.InputParams):
if project_id is None:
project_id = params.project_id
if location is None:
location = params.location
# Convert to base InputParams
params = GoogleLLMService.InputParams(
**params.model_dump(exclude={"location", "project_id"}, exclude_unset=True)
)
# Validate project_id and location parameters
# NOTE: once we remove Vertex-specific InputParams class, we can update
# __init__() signature as follows:
# - location: str = "us-east4",
# - project_id: str,
# But for now, we need them as-is to maintain proper backward
# compatibility.
if project_id is None:
raise ValueError("project_id is required")
if location is None:
# If location is not provided, default to "us-east4".
# Note: this is legacy behavior; ideally location would be
# required.
logger.warning("location is not provided. Defaulting to 'us-east4'.")
location = "us-east4" # Default location if not provided
# These need to be set before calling super().__init__() because
# super().__init__() invokes _create_client(), which needs these.
self._credentials = self._get_credentials(credentials, credentials_path)
self._project_id = project_id
self._location = location
# 1. Initialize default_settings with hardcoded defaults
default_settings = self.Settings(
model="gemini-2.5-flash",
system_instruction=None,
max_tokens=4096,
temperature=None,
top_k=None,
top_p=None,
frequency_penalty=None,
presence_penalty=None,
seed=None,
filter_incomplete_user_turns=False,
user_turn_completion_config=None,
thinking=None,
extra={},
)
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if system_instruction is not None:
self._warn_init_param_moved_to_settings("system_instruction", "system_instruction")
default_settings.system_instruction = system_instruction
# 3. Apply params overrides — only if settings not provided
if params is not None:
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.max_tokens = params.max_tokens
default_settings.temperature = params.temperature
default_settings.top_k = params.top_k
default_settings.top_p = params.top_p
default_settings.thinking = params.thinking
if isinstance(params.extra, dict):
default_settings.extra = params.extra
# 4. Apply settings delta (canonical API, always wins)
if settings is not None:
default_settings.apply_update(settings)
# Call parent constructor with dummy api_key
# (api_key is required by parent class, but not actually used with Vertex)
super().__init__(
api_key="dummy",
settings=default_settings,
tools=tools,
tool_config=tool_config,
http_options=http_options,
**kwargs,
)
def create_client(self):
"""Create the Gemini client instance configured for Vertex AI."""
self._client = Client(
vertexai=True,
credentials=self._credentials,
project=self._project_id,
location=self._location,
http_options=self._http_options,
)
@staticmethod
def _get_credentials(credentials: Optional[str], credentials_path: Optional[str]):
"""Retrieve Credentials using Google service account credentials.
Supports multiple authentication methods:
1. Direct JSON credentials string
2. Path to service account JSON file
3. Default application credentials (ADC)
Args:
credentials: JSON string of service account credentials.
credentials_path: Path to the service account JSON file.
Returns:
Google credentials object for API authentication.
Raises:
ValueError: If no valid credentials are provided or found.
"""
creds: Optional[service_account.Credentials] = None
if credentials:
# Parse and load credentials from JSON string
creds = service_account.Credentials.from_service_account_info(
json.loads(credentials),
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
elif credentials_path:
# Load credentials from JSON file
creds = service_account.Credentials.from_service_account_file(
credentials_path,
scopes=["https://www.googleapis.com/auth/cloud-platform"],
)
else:
try:
creds, project_id = default(
scopes=["https://www.googleapis.com/auth/cloud-platform"]
)
except GoogleAuthError:
pass
if not creds:
raise ValueError("No valid credentials provided.")
creds.refresh(Request()) # Ensure token is up-to-date, lifetime is 1 hour.
return creds

View File

@@ -10,6 +10,7 @@ This module provides integration with Gradium's real-time speech-to-text
WebSocket API for streaming audio transcription.
"""
import asyncio
import base64
import json
from dataclasses import dataclass, field
@@ -22,13 +23,14 @@ from pipecat.frames.frames import (
CancelFrame,
EndFrame,
Frame,
InterimTranscriptionFrame,
StartFrame,
TranscriptionFrame,
VADUserStartedSpeakingFrame,
VADUserStoppedSpeakingFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven
from pipecat.services.stt_latency import GRADIUM_TTFS_P99
from pipecat.services.stt_service import WebsocketSTTService
from pipecat.transcriptions.language import Language, resolve_language
@@ -43,7 +45,37 @@ except ModuleNotFoundError as e:
logger.error('In order to use Gradium, you need to `pip install "pipecat-ai[gradium]"`.')
raise Exception(f"Missing module: {e}")
SAMPLE_RATE = 24000
# Seconds to wait after a "flushed" message for trailing text tokens to arrive
# before finalizing the transcription.
TRANSCRIPT_AGGREGATION_DELAY = 0.1
def _input_format_from_encoding(encoding: str, sample_rate: int) -> str:
"""Build Gradium input_format from encoding type and sample rate.
For PCM encoding, appends the sample rate (e.g., "pcm_16000").
For other encodings (wav, opus), returns the encoding as-is.
Args:
encoding: Base encoding type ("pcm", "wav", or "opus").
sample_rate: Audio sample rate in Hz.
Returns:
The full input_format string for the Gradium API.
"""
if encoding == "pcm":
match sample_rate:
case 8000:
return "pcm_8000"
case 16000:
return "pcm_16000"
case 24000:
return "pcm_24000"
logger.warning(
f"GradiumSTTService: unsupported sample rate {sample_rate} for PCM encoding, using pcm_16000"
)
return "pcm_16000"
return encoding
def language_to_gradium_language(language: Language) -> Optional[str]:
@@ -89,13 +121,13 @@ class GradiumSTTService(WebsocketSTTService):
"""
Settings = GradiumSTTSettings
_settings: GradiumSTTSettings
_settings: Settings
class InputParams(BaseModel):
"""Configuration parameters for Gradium STT API.
.. deprecated:: 0.0.105
Use ``settings=GradiumSTTSettings(...)`` instead.
Use ``settings=GradiumSTTService.Settings(...)`` instead.
Parameters:
language: Expected language of the audio (e.g., "en", "es", "fr").
@@ -115,9 +147,11 @@ class GradiumSTTService(WebsocketSTTService):
*,
api_key: str,
api_endpoint_base_url: str = "wss://eu.api.gradium.ai/api/speech/asr",
encoding: str = "pcm",
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
json_config: Optional[str] = None,
settings: Optional[GradiumSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = GRADIUM_TTFS_P99,
**kwargs,
):
@@ -126,10 +160,16 @@ class GradiumSTTService(WebsocketSTTService):
Args:
api_key: Gradium API key for authentication.
api_endpoint_base_url: WebSocket endpoint URL. Defaults to Gradium's streaming endpoint.
encoding: Base audio encoding type. One of "pcm", "wav", or "opus".
For PCM, the sample rate is appended automatically from the
pipeline's audio_in_sample_rate (e.g., "pcm" becomes "pcm_16000").
Defaults to "pcm".
sample_rate: Audio sample rate in Hz. If None, uses the pipeline
sample rate.
params: Configuration parameters for language and delay settings.
.. deprecated:: 0.0.105
Use ``settings=GradiumSTTSettings(...)`` instead.
Use ``settings=GradiumSTTService.Settings(...)`` instead.
json_config: Optional JSON configuration string for additional model settings.
@@ -152,8 +192,8 @@ class GradiumSTTService(WebsocketSTTService):
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = GradiumSTTSettings(
model=None,
default_settings = self.Settings(
model="default",
language=None,
delay_in_frames=None,
)
@@ -162,7 +202,7 @@ class GradiumSTTService(WebsocketSTTService):
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GradiumSTTSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.language = params.language
if params.delay_in_frames is not None:
@@ -173,7 +213,7 @@ class GradiumSTTService(WebsocketSTTService):
default_settings.apply_update(settings)
super().__init__(
sample_rate=SAMPLE_RATE,
sample_rate=sample_rate,
ttfs_p99_latency=ttfs_p99_latency,
settings=default_settings,
**kwargs,
@@ -181,19 +221,25 @@ class GradiumSTTService(WebsocketSTTService):
self._api_key = api_key
self._api_endpoint_base_url = api_endpoint_base_url
self._encoding = encoding
self._websocket = None
self._json_config = json_config
self._receive_task = None
self._input_format = ""
self._audio_buffer = bytearray()
self._chunk_size_ms = 80
self._chunk_size_bytes = 0
# Set from the ready message when connecting to the service.
# These values are used for flushing transcription.
self._delay_in_frames = 0
self._frame_size = 0
# Accumulates text fragments within a turn. Each "text" message
# appends to this list. On "flushed" a short aggregation delay
# allows trailing tokens to arrive before the full text is joined
# and pushed as a TranscriptionFrame.
self._accumulated_text: list[str] = []
self._flush_counter = 0
self._transcript_aggregation_task: Optional[asyncio.Task] = None
def can_generate_metrics(self) -> bool:
"""Check if the service can generate metrics.
@@ -207,7 +253,7 @@ class GradiumSTTService(WebsocketSTTService):
"""Apply a settings delta, sync params, and reconnect.
Args:
delta: A :class:`STTSettings` (or ``GradiumSTTSettings``) delta.
delta: A :class:`STTSettings` (or ``GradiumSTTService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.
@@ -228,6 +274,7 @@ class GradiumSTTService(WebsocketSTTService):
frame: Start frame to begin processing.
"""
await super().start(frame)
self._input_format = _input_format_from_encoding(self._encoding, self.sample_rate)
self._chunk_size_bytes = int(self._chunk_size_ms * self.sample_rate * 2 / 1000)
await self._connect()
@@ -249,56 +296,41 @@ class GradiumSTTService(WebsocketSTTService):
await super().cancel(frame)
await self._disconnect()
async def process_frame(self, frame: Frame, direction: FrameDirection):
"""Process frames with VAD-specific handling.
async def _start_metrics(self):
"""Start performance metrics collection for transcription processing."""
await self.start_processing_metrics()
When VAD detects the user has stopped speaking, we flush the transcription
by sending silence frames. This makes the system more reactive by getting
the final transcription faster without closing the connection.
async def process_frame(self, frame: Frame, direction: FrameDirection):
"""Process incoming frames and handle speech events.
Args:
frame: The frame to process.
direction: The direction of frame processing.
direction: Direction of frame flow in the pipeline.
"""
await super().process_frame(frame, direction)
if isinstance(frame, VADUserStartedSpeakingFrame):
await self.start_processing_metrics()
await self._start_metrics()
elif isinstance(frame, VADUserStoppedSpeakingFrame):
await self._flush_transcription()
await self._send_flush()
async def _flush_transcription(self):
"""Flush the transcription by sending silence frames.
async def _send_flush(self):
"""Send a flush request to process any buffered audio immediately.
When VAD detects the user stopped speaking, we send delay_in_frames
chunks of silence (zeros) to flush the remaining audio from the model's
buffer. This allows for faster turn-around without closing the connection.
From Gradium docs: "feed in delay_in_frames chunks of silence (vectors
of zeros). If those are fed in faster than realtime, the API also has
a possibility to process them faster."
Sends a flush message to tell the server to process buffered audio.
The server responds with text fragments followed by a "flushed"
acknowledgment, which triggers finalization.
"""
if not self._websocket or self._websocket.state is not State.OPEN:
return
if self._delay_in_frames <= 0:
logger.debug("No delay_in_frames set, skipping flush")
return
# Create a silence chunk (zeros) of frame_size samples
# Each sample is 2 bytes (16-bit PCM)
silence_bytes = bytes(self._frame_size * 2)
silence_b64 = base64.b64encode(silence_bytes).decode("utf-8")
logger.debug(f"Flushing Gradium STT with {self._delay_in_frames} silence frames")
for _ in range(self._delay_in_frames):
msg = {"type": "audio", "audio": silence_b64}
try:
await self._websocket.send(json.dumps(msg))
except Exception as e:
logger.warning(f"Failed to send silence frame: {e}")
break
self._flush_counter += 1
flush_id = str(self._flush_counter)
msg = {"type": "flush", "flush_id": flush_id}
try:
await self._websocket.send(json.dumps(msg))
except Exception as e:
logger.warning(f"Failed to send flush: {e}")
async def run_stt(self, audio: bytes) -> AsyncGenerator[Frame, None]:
"""Process audio data for speech-to-text conversion.
@@ -353,7 +385,8 @@ class GradiumSTTService(WebsocketSTTService):
await self._call_event_handler("on_connected")
setup_msg = {
"type": "setup",
"input_format": "pcm",
"model_name": self._settings.model,
"input_format": self._input_format,
}
# Build json_config: start with deprecated json_config, then override with params
json_config = {}
@@ -375,13 +408,7 @@ class GradiumSTTService(WebsocketSTTService):
if ready_msg["type"] != "ready":
raise Exception(f"unexpected first message type {ready_msg['type']}")
# Store delay_in_frames and frame_size for silence flushing
self._delay_in_frames = ready_msg.get("delay_in_frames", 0)
self._frame_size = ready_msg.get("frame_size", 1920)
logger.debug(
f"Connected to Gradium STT (delay_in_frames={self._delay_in_frames}, "
f"frame_size={self._frame_size})"
)
logger.debug("Connected to Gradium STT")
except Exception as e:
await self.push_error(error_msg=f"Unknown error occurred: {e}", exception=e)
@@ -390,6 +417,13 @@ class GradiumSTTService(WebsocketSTTService):
async def _disconnect(self):
await super()._disconnect()
if self._transcript_aggregation_task:
await self.cancel_task(self._transcript_aggregation_task)
self._transcript_aggregation_task = None
self._accumulated_text.clear()
self._flush_counter = 0
if self._receive_task:
await self.cancel_task(self._receive_task)
self._receive_task = None
@@ -412,41 +446,75 @@ class GradiumSTTService(WebsocketSTTService):
return self._websocket
raise Exception("Websocket not connected")
async def _process_messages(self):
async def _receive_messages(self):
async for message in self._get_websocket():
try:
data = json.loads(message)
await self._process_response(data)
msg = json.loads(message)
except json.JSONDecodeError:
logger.warning(f"Received non-JSON message: {message}")
continue
async def _receive_messages(self):
while True:
await self._process_messages()
logger.debug(f"{self} Gradium connection was disconnected (timeout?), reconnecting")
await self._connect_websocket()
async def _process_response(self, msg):
type_ = msg.get("type", "")
if type_ == "text":
await self._handle_text(msg["text"])
elif type_ == "end_of_stream":
await self._handle_end_of_stream()
elif type_ == "error":
await self.push_error(error_msg=f"Error: {msg}")
async def _handle_end_of_stream(self):
"""Handle termination message."""
logger.debug("Received end_of_stream message from server")
type_ = msg.get("type", "")
if type_ == "text":
await self._handle_text(msg["text"])
elif type_ == "flushed":
await self._handle_flushed()
elif type_ == "end_of_stream":
logger.debug("Received end_of_stream message from server")
elif type_ == "error":
await self.push_error(error_msg=f"Error: {msg}")
async def _handle_text(self, text: str):
"""Handle transcription results."""
"""Handle streaming transcription fragment.
Accumulates text and pushes an InterimTranscriptionFrame with the
full accumulated text so far.
"""
self._accumulated_text.append(text)
accumulated = " ".join(self._accumulated_text)
await self.push_frame(
InterimTranscriptionFrame(
text=accumulated,
user_id=self._user_id,
timestamp=time_now_iso8601(),
language=self._settings.language,
)
)
await self.stop_processing_metrics()
async def _handle_flushed(self):
"""Handle flush completion by starting a transcript aggregation timer.
The "flushed" message confirms that buffered audio has been processed,
but text tokens may still arrive after this point. A short timer allows
trailing tokens to accumulate before finalizing the transcription.
"""
if self._transcript_aggregation_task:
await self.cancel_task(self._transcript_aggregation_task)
self._transcript_aggregation_task = self.create_task(
self._transcript_aggregation_handler(), "transcript_aggregation"
)
async def _transcript_aggregation_handler(self):
"""Wait for trailing tokens then finalize the accumulated transcription."""
await asyncio.sleep(TRANSCRIPT_AGGREGATION_DELAY)
await self._finalize_accumulated_text()
async def _finalize_accumulated_text(self):
"""Join accumulated text, push TranscriptionFrame, and clear state."""
if not self._accumulated_text:
return
self._transcript_aggregation_task = None
text = " ".join(self._accumulated_text)
self._accumulated_text.clear()
logger.debug(f"Final transcription: [{text}]")
await self.push_frame(
TranscriptionFrame(
text,
self._user_id,
time_now_iso8601(),
self._settings.language,
)
)
await self._trace_transcription(text, is_final=True, language=None)
await self.stop_processing_metrics()
await self._trace_transcription(text, is_final=True, language=self._settings.language)

View File

@@ -21,7 +21,7 @@ from pipecat.frames.frames import (
TTSAudioRawFrame,
TTSStoppedFrame,
)
from pipecat.services.settings import TTSSettings, _warn_deprecated_param
from pipecat.services.settings import TTSSettings
from pipecat.services.tts_service import WebsocketTTSService
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -48,13 +48,13 @@ class GradiumTTSService(WebsocketTTSService):
"""Text-to-Speech service using Gradium's websocket API."""
Settings = GradiumTTSSettings
_settings: GradiumTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Configuration parameters for Gradium TTS service.
.. deprecated:: 0.0.105
Use ``GradiumTTSSettings`` directly via the ``settings`` parameter instead.
Use ``GradiumTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
temp: Temperature to be used for generation, defaults to 0.6.
@@ -71,7 +71,7 @@ class GradiumTTSService(WebsocketTTSService):
model: Optional[str] = None,
json_config: Optional[str] = None,
params: Optional[InputParams] = None,
settings: Optional[GradiumTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Gradium TTS service.
@@ -81,26 +81,26 @@ class GradiumTTSService(WebsocketTTSService):
voice_id: the voice identifier.
.. deprecated:: 0.0.105
Use ``settings=GradiumTTSSettings(voice=...)`` instead.
Use ``settings=GradiumTTSService.Settings(voice=...)`` instead.
url: Gradium websocket API endpoint.
model: Model ID to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=GradiumTTSSettings(model=...)`` instead.
Use ``settings=GradiumTTSService.Settings(model=...)`` instead.
json_config: Optional JSON configuration string for additional model settings.
params: Additional configuration parameters.
.. deprecated:: 0.0.105
Use ``settings=GradiumTTSSettings(...)`` instead.
Use ``settings=GradiumTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to parent class.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = GradiumTTSSettings(
default_settings = self.Settings(
model="default",
voice="YTpq7expH9539ERJ",
language=None,
@@ -108,15 +108,15 @@ class GradiumTTSService(WebsocketTTSService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GradiumTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if voice_id is not None:
_warn_deprecated_param("voice_id", GradiumTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GradiumTTSSettings)
self._warn_init_param_moved_to_settings("params")
# Note: params.temp has no corresponding settings field
# 4. Apply settings delta (canonical API, always wins)
@@ -153,7 +153,7 @@ class GradiumTTSService(WebsocketTTSService):
"""Apply a settings delta and reconnect if voice changed.
Args:
delta: A :class:`TTSSettings` (or ``GradiumTTSSettings``) delta.
delta: A :class:`TTSSettings` (or ``GradiumTTSService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.

View File

@@ -23,13 +23,12 @@ from pipecat.processors.aggregators.llm_response import (
LLMUserAggregatorParams,
)
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import (
OpenAIAssistantContextAggregator,
OpenAILLMService,
OpenAIUserContextAggregator,
)
from pipecat.services.settings import _warn_deprecated_param
@dataclass
@@ -71,7 +70,7 @@ class GrokContextAggregatorPair:
@dataclass
class GrokLLMSettings(OpenAILLMSettings):
class GrokLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for GrokLLMService."""
pass
@@ -87,7 +86,7 @@ class GrokLLMService(OpenAILLMService):
"""
Settings = GrokLLMSettings
_settings: GrokLLMSettings
_settings: Settings
def __init__(
self,
@@ -95,7 +94,7 @@ class GrokLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.x.ai/v1",
model: Optional[str] = None,
settings: Optional[GrokLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the GrokLLMService with API key and model.
@@ -106,18 +105,18 @@ class GrokLLMService(OpenAILLMService):
model: The model identifier to use. Defaults to "grok-3-beta".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=GrokLLMService.Settings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = GrokLLMSettings(model="grok-3-beta")
default_settings = self.Settings(model="grok-3-beta")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GrokLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)

View File

@@ -60,7 +60,6 @@ from pipecat.services.settings import (
NOT_GIVEN,
LLMSettings,
_NotGiven,
_warn_deprecated_param,
is_given,
)
from pipecat.utils.time import time_now_iso8601
@@ -109,7 +108,7 @@ class GrokRealtimeLLMSettings(LLMSettings):
# -- Bidirectional sync helpers ------------------------------------------
@staticmethod
def _sync_top_level_to_sp(settings: "GrokRealtimeLLMSettings"):
def _sync_top_level_to_sp(settings: "GrokRealtimeLLMService.Settings"):
"""Push top-level ``system_instruction`` into ``session_properties``."""
if not is_given(settings.session_properties):
return
@@ -119,7 +118,7 @@ class GrokRealtimeLLMSettings(LLMSettings):
# -- apply_update override -----------------------------------------------
def apply_update(self, delta: "GrokRealtimeLLMSettings") -> Dict[str, Any]:
def apply_update(self, delta: "GrokRealtimeLLMService.Settings") -> Dict[str, Any]:
"""Merge a delta, keeping ``system_instruction`` in sync with SP.
When the delta contains ``session_properties``, it **replaces** the
@@ -151,8 +150,8 @@ class GrokRealtimeLLMSettings(LLMSettings):
@classmethod
def from_mapping(
cls: Type["GrokRealtimeLLMSettings"], settings: Mapping[str, Any]
) -> "GrokRealtimeLLMSettings":
cls: Type["GrokRealtimeLLMService.Settings"], settings: Mapping[str, Any]
) -> "GrokRealtimeLLMService.Settings":
"""Build a delta from a plain dict, routing SP keys into ``session_properties``.
Keys that correspond to ``SessionProperties`` fields are collected into
@@ -203,7 +202,7 @@ class GrokRealtimeLLMService(LLMService):
"""
Settings = GrokRealtimeLLMSettings
_settings: GrokRealtimeLLMSettings
_settings: Settings
# Use the Grok-specific adapter
adapter_class = GrokRealtimeLLMAdapter
@@ -214,7 +213,7 @@ class GrokRealtimeLLMService(LLMService):
api_key: str,
base_url: str = "wss://api.x.ai/v1/realtime",
session_properties: Optional[events.SessionProperties] = None,
settings: Optional[GrokRealtimeLLMSettings] = None,
settings: Optional[Settings] = None,
start_audio_paused: bool = False,
**kwargs,
):
@@ -228,7 +227,7 @@ class GrokRealtimeLLMService(LLMService):
If None, uses default SessionProperties with voice "Ara".
.. deprecated:: 0.0.105
Use ``settings=GrokRealtimeLLMSettings(session_properties=...)``
Use ``settings=GrokRealtimeLLMService.Settings(session_properties=...)``
instead.
To set a different voice, configure it in session_properties:
@@ -241,7 +240,7 @@ class GrokRealtimeLLMService(LLMService):
**kwargs: Additional arguments passed to parent LLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = GrokRealtimeLLMSettings(
default_settings = self.Settings(
model=None,
system_instruction=None,
temperature=None,
@@ -260,7 +259,7 @@ class GrokRealtimeLLMService(LLMService):
if session_properties is not None:
_warn_deprecated_param(
"session_properties",
GrokRealtimeLLMSettings,
self.Settings,
"session_properties",
)
default_settings.session_properties = session_properties
@@ -269,7 +268,7 @@ class GrokRealtimeLLMService(LLMService):
default_settings.system_instruction = session_properties.instructions
# Sync top-level system_instruction back into session_properties
GrokRealtimeLLMSettings._sync_top_level_to_sp(default_settings)
self.Settings._sync_top_level_to_sp(default_settings)
# 3. Apply settings delta (canonical API, always wins)
if settings is not None:
@@ -677,7 +676,10 @@ class GrokRealtimeLLMService(LLMService):
elif evt.type == "response.function_call_arguments.done":
await self._handle_evt_function_call_arguments_done(evt)
elif evt.type == "error":
if evt.error.code == "response_cancel_not_active":
if evt.error.code in (
"response_cancel_not_active",
"conversation_already_has_active_response",
):
logger.debug(f"{self} {evt.error.message}")
else:
await self._handle_evt_error(evt)
@@ -937,7 +939,7 @@ class GrokRealtimeLLMService(LLMService):
item = events.ConversationItem(
type="function_call_output",
call_id=tool_call_id,
output=json.dumps(result),
output=json.dumps(result, ensure_ascii=False),
)
await self.send_client_event(events.ConversationItemCreateEvent(item=item))

View File

@@ -11,13 +11,12 @@ from typing import Optional
from loguru import logger
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class GroqLLMSettings(OpenAILLMSettings):
class GroqLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for GroqLLMService."""
pass
@@ -31,7 +30,7 @@ class GroqLLMService(OpenAILLMService):
"""
Settings = GroqLLMSettings
_settings: GroqLLMSettings
_settings: Settings
def __init__(
self,
@@ -39,7 +38,7 @@ class GroqLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.groq.com/openai/v1",
model: Optional[str] = None,
settings: Optional[GroqLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize Groq LLM service.
@@ -50,18 +49,18 @@ class GroqLLMService(OpenAILLMService):
model: The model identifier to use. Defaults to "llama-3.3-70b-versatile".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=GroqLLMService.Settings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = GroqLLMSettings(model="llama-3.3-70b-versatile")
default_settings = self.Settings(model="llama-3.3-70b-versatile")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GroqLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)

View File

@@ -9,18 +9,16 @@
from dataclasses import dataclass
from typing import Optional
from pipecat.services.settings import _warn_deprecated_param
from pipecat.services.stt_latency import GROQ_TTFS_P99
from pipecat.services.whisper.base_stt import (
BaseWhisperSTTService,
BaseWhisperSTTSettings,
Transcription,
)
from pipecat.transcriptions.language import Language
@dataclass
class GroqSTTSettings(BaseWhisperSTTSettings):
class GroqSTTSettings(BaseWhisperSTTService.Settings):
"""Settings for the Groq STT service.
Parameters:
@@ -38,7 +36,7 @@ class GroqSTTService(BaseWhisperSTTService):
"""
Settings = GroqSTTSettings
_settings: GroqSTTSettings
_settings: Settings
def __init__(
self,
@@ -49,7 +47,7 @@ class GroqSTTService(BaseWhisperSTTService):
language: Optional[Language] = None,
prompt: Optional[str] = None,
temperature: Optional[float] = None,
settings: Optional[GroqSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = GROQ_TTFS_P99,
**kwargs,
):
@@ -59,24 +57,24 @@ class GroqSTTService(BaseWhisperSTTService):
model: Whisper model to use.
.. deprecated:: 0.0.105
Use ``settings=GroqSTTSettings(model=...)`` instead.
Use ``settings=GroqSTTService.Settings(model=...)`` instead.
api_key: Groq API key. Defaults to None.
base_url: API base URL. Defaults to "https://api.groq.com/openai/v1".
language: Language of the audio input.
.. deprecated:: 0.0.105
Use ``settings=GroqSTTSettings(language=...)`` instead.
Use ``settings=GroqSTTService.Settings(language=...)`` instead.
prompt: Optional text to guide the model's style or continue a previous segment.
.. deprecated:: 0.0.105
Use ``settings=GroqSTTSettings(prompt=...)`` instead.
Use ``settings=GroqSTTService.Settings(prompt=...)`` instead.
temperature: Optional sampling temperature between 0 and 1.
.. deprecated:: 0.0.105
Use ``settings=GroqSTTSettings(temperature=...)`` instead.
Use ``settings=GroqSTTService.Settings(temperature=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -85,25 +83,25 @@ class GroqSTTService(BaseWhisperSTTService):
**kwargs: Additional arguments passed to BaseWhisperSTTService.
"""
# --- 1. Hardcoded defaults ---
default_settings = GroqSTTSettings(
default_settings = self.Settings(
model="whisper-large-v3-turbo",
language=self.language_to_service_language(Language.EN),
language=Language.EN,
prompt=None,
temperature=None,
)
# --- 2. Deprecated direct-arg overrides ---
if model is not None:
_warn_deprecated_param("model", GroqSTTSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if language is not None:
_warn_deprecated_param("language", GroqSTTSettings, "language")
default_settings.language = self.language_to_service_language(language)
self._warn_init_param_moved_to_settings("language", "language")
default_settings.language = language
if prompt is not None:
_warn_deprecated_param("prompt", GroqSTTSettings, "prompt")
self._warn_init_param_moved_to_settings("prompt", "prompt")
default_settings.prompt = prompt
if temperature is not None:
_warn_deprecated_param("temperature", GroqSTTSettings, "temperature")
self._warn_init_param_moved_to_settings("temperature", "temperature")
default_settings.temperature = temperature
# --- 3. (no params object for this service) ---

View File

@@ -19,7 +19,7 @@ from pipecat.frames.frames import (
Frame,
TTSAudioRawFrame,
)
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import TTSService
from pipecat.transcriptions.language import Language
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -52,13 +52,13 @@ class GroqTTSService(TTSService):
"""
Settings = GroqTTSSettings
_settings: GroqTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Groq TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=GroqTTSSettings(...)`` instead.
Use ``settings=GroqTTSService.Settings(...)`` instead.
Parameters:
language: Language for speech synthesis. Defaults to English.
@@ -79,7 +79,7 @@ class GroqTTSService(TTSService):
model_name: Optional[str] = None,
voice_id: Optional[str] = None,
sample_rate: Optional[int] = GROQ_SAMPLE_RATE,
settings: Optional[GroqTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize Groq TTS service.
@@ -90,17 +90,17 @@ class GroqTTSService(TTSService):
params: Additional input parameters for voice customization.
.. deprecated:: 0.0.105
Use ``settings=GroqTTSSettings(...)`` instead.
Use ``settings=GroqTTSService.Settings(...)`` instead.
model_name: TTS model to use.
.. deprecated:: 0.0.105
Use ``settings=GroqTTSSettings(model=...)`` instead.
Use ``settings=GroqTTSService.Settings(model=...)`` instead.
voice_id: Voice identifier to use.
.. deprecated:: 0.0.105
Use ``settings=GroqTTSSettings(voice=...)`` instead.
Use ``settings=GroqTTSService.Settings(voice=...)`` instead.
sample_rate: Audio sample rate. Must be 48000 Hz for Groq TTS.
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -111,7 +111,7 @@ class GroqTTSService(TTSService):
logger.warning(f"Groq TTS only supports {self.GROQ_SAMPLE_RATE}Hz sample rate. ")
# 1. Initialize default_settings with hardcoded defaults
default_settings = GroqTTSSettings(
default_settings = self.Settings(
model="canopylabs/orpheus-v1-english",
voice="autumn",
language="en",
@@ -120,15 +120,15 @@ class GroqTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if model_name is not None:
_warn_deprecated_param("model_name", GroqTTSSettings, "model")
self._warn_init_param_moved_to_settings("model_name", "model")
default_settings.model = model_name
if voice_id is not None:
_warn_deprecated_param("voice_id", GroqTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GroqTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.language = str(params.language) if params.language else "en"
default_settings.speed = params.speed

View File

@@ -83,7 +83,7 @@ class HeyGenVideoService(AIService):
"""
Settings = HeyGenVideoSettings
_settings: HeyGenVideoSettings
_settings: Settings
def __init__(
self,
@@ -92,7 +92,7 @@ class HeyGenVideoService(AIService):
session: aiohttp.ClientSession,
session_request: Optional[Union[LiveAvatarNewSessionRequest, NewSessionRequest]] = None,
service_type: Optional[ServiceType] = None,
settings: Optional[HeyGenVideoSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
) -> None:
"""Initialize the HeyGen video service.

View File

@@ -25,7 +25,7 @@ from pipecat.frames.frames import (
TTSStoppedFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import TTSService
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -79,13 +79,13 @@ class HumeTTSService(TTSService):
"""
Settings = HumeTTSSettings
_settings: HumeTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Optional synthesis parameters for Hume TTS.
.. deprecated:: 0.0.105
Use ``settings=HumeTTSSettings(...)`` instead.
Use ``settings=HumeTTSService.Settings(...)`` instead.
Parameters:
description: Natural-language acting directions (up to 100 characters).
@@ -104,7 +104,7 @@ class HumeTTSService(TTSService):
voice_id: Optional[str] = None,
params: Optional[InputParams] = None,
sample_rate: Optional[int] = HUME_SAMPLE_RATE,
settings: Optional[HumeTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
) -> None:
"""Initialize the HumeTTSService.
@@ -114,12 +114,12 @@ class HumeTTSService(TTSService):
voice_id: ID of the voice to use. Only voice IDs are supported; voice names are not.
.. deprecated:: 0.0.105
Use ``settings=HumeTTSSettings(voice=...)`` instead.
Use ``settings=HumeTTSService.Settings(voice=...)`` instead.
params: Optional synthesis controls (acting instructions, speed, trailing silence).
.. deprecated:: 0.0.105
Use ``settings=HumeTTSSettings(...)`` instead.
Use ``settings=HumeTTSService.Settings(...)`` instead.
sample_rate: Output sample rate for emitted PCM frames. Defaults to 48_000 (Hume).
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -136,7 +136,7 @@ class HumeTTSService(TTSService):
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = HumeTTSSettings(
default_settings = self.Settings(
model=None,
voice=None,
language=None, # Not applicable here
@@ -147,12 +147,12 @@ class HumeTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", HumeTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", HumeTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.description = params.description
default_settings.speed = params.speed
@@ -242,7 +242,7 @@ class HumeTTSService(TTSService):
"""Runtime updates via key/value pair.
.. deprecated:: 0.0.104
Use ``TTSUpdateSettingsFrame(delta=HumeTTSSettings(...))`` instead.
Use ``TTSUpdateSettingsFrame(delta=HumeTTSService.Settings(...))`` instead.
Args:
key: The name of the setting to update. Recognized keys are:
@@ -256,7 +256,7 @@ class HumeTTSService(TTSService):
warnings.simplefilter("always")
warnings.warn(
"'update_setting' is deprecated, use "
"'TTSUpdateSettingsFrame(delta=HumeTTSSettings(...))' instead.",
"'TTSUpdateSettingsFrame(delta=self.Settings(...))' instead.",
DeprecationWarning,
stacklevel=2,
)
@@ -274,7 +274,7 @@ class HumeTTSService(TTSService):
kwargs["speed"] = None if value is None else float(value)
elif key_l == "trailing_silence":
kwargs["trailing_silence"] = None if value is None else float(value)
await self._update_settings(HumeTTSSettings(**kwargs))
await self._update_settings(self.Settings(**kwargs))
@traced_tts
async def run_tts(self, text: str, context_id: str) -> AsyncGenerator[Frame, None]:

View File

@@ -40,7 +40,7 @@ from pipecat import version as pipecat_version
USER_AGENT = f"pipecat/{pipecat_version()}"
from pydantic import BaseModel
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
try:
from websockets.asyncio.client import connect as websocket_connect
@@ -101,13 +101,13 @@ class InworldHttpTTSService(TTSService):
"""
Settings = InworldTTSSettings
_settings: InworldTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Inworld TTS configuration.
.. deprecated:: 0.0.105
Use ``InworldTTSSettings`` directly via the ``settings`` parameter instead.
Use ``InworldHttpTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
temperature: Temperature for speech synthesis.
@@ -131,7 +131,7 @@ class InworldHttpTTSService(TTSService):
encoding: str = "LINEAR16",
timestamp_transport_strategy: Optional[Literal["ASYNC", "SYNC"]] = "ASYNC",
params: Optional[InputParams] = None,
settings: Optional[InworldTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Inworld TTS service.
@@ -142,12 +142,12 @@ class InworldHttpTTSService(TTSService):
voice_id: ID of the voice to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=InworldTTSSettings(voice=...)`` instead.
Use ``settings=InworldHttpTTSService.Settings(voice=...)`` instead.
model: ID of the model to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=InworldTTSSettings(model=...)`` instead.
Use ``settings=InworldHttpTTSService.Settings(model=...)`` instead.
streaming: Whether to use streaming mode.
sample_rate: Audio sample rate in Hz.
@@ -157,14 +157,14 @@ class InworldHttpTTSService(TTSService):
params: Input parameters for Inworld TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=InworldTTSSettings(...)`` instead.
Use ``settings=InworldHttpTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to the parent class.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = InworldTTSSettings(
default_settings = self.Settings(
model="inworld-tts-1.5-max",
voice="Ashley",
language=None,
@@ -174,15 +174,15 @@ class InworldHttpTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", InworldTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if model is not None:
_warn_deprecated_param("model", InworldTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", InworldTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.speaking_rate is not None:
default_settings.speaking_rate = params.speaking_rate
@@ -489,13 +489,13 @@ class InworldTTSService(WebsocketTTSService):
"""
Settings = InworldTTSSettings
_settings: InworldTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Inworld WebSocket TTS configuration.
.. deprecated:: 0.0.105
Use ``InworldTTSSettings`` directly via the ``settings`` parameter instead.
Use ``InworldTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
temperature: Temperature for speech synthesis.
@@ -532,7 +532,7 @@ class InworldTTSService(WebsocketTTSService):
apply_text_normalization: Optional[str] = None,
timestamp_transport_strategy: Optional[Literal["ASYNC", "SYNC"]] = "ASYNC",
params: Optional[InputParams] = None,
settings: Optional[InworldTTSSettings] = None,
settings: Optional[Settings] = None,
aggregate_sentences: Optional[bool] = None,
text_aggregation_mode: Optional[TextAggregationMode] = None,
append_trailing_space: bool = True,
@@ -545,12 +545,12 @@ class InworldTTSService(WebsocketTTSService):
voice_id: ID of the voice to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=InworldTTSSettings(voice=...)`` instead.
Use ``settings=InworldTTSService.Settings(voice=...)`` instead.
model: ID of the model to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=InworldTTSSettings(model=...)`` instead.
Use ``settings=InworldTTSService.Settings(model=...)`` instead.
url: URL of the Inworld WebSocket API.
sample_rate: Audio sample rate in Hz.
@@ -564,7 +564,7 @@ class InworldTTSService(WebsocketTTSService):
params: Input parameters for Inworld WebSocket TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=InworldTTSSettings(...)`` instead.
Use ``settings=InworldTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -582,7 +582,7 @@ class InworldTTSService(WebsocketTTSService):
auto_mode = True if aggregate_sentences is None else aggregate_sentences
# 1. Initialize default_settings with hardcoded defaults
default_settings = InworldTTSSettings(
default_settings = self.Settings(
model="inworld-tts-1.5-max",
voice="Ashley",
language=None,
@@ -592,17 +592,17 @@ class InworldTTSService(WebsocketTTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", InworldTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if model is not None:
_warn_deprecated_param("model", InworldTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
_buffer_max_delay_ms = None
_buffer_char_threshold = None
if params is not None:
_warn_deprecated_param("params", InworldTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.speaking_rate is not None:
default_settings.speaking_rate = params.speaking_rate

View File

@@ -21,7 +21,7 @@ from pipecat.frames.frames import (
Frame,
TTSAudioRawFrame,
)
from pipecat.services.settings import TTSSettings, _warn_deprecated_param
from pipecat.services.settings import TTSSettings
from pipecat.services.tts_service import TTSService
from pipecat.transcriptions.language import Language, resolve_language
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -102,13 +102,13 @@ class KokoroTTSService(TTSService):
"""
Settings = KokoroTTSSettings
_settings: KokoroTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Kokoro TTS configuration.
.. deprecated:: 0.0.105
Use ``KokoroTTSSettings`` directly via the ``settings`` parameter instead.
Use ``KokoroTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
language: Language to use for synthesis.
@@ -123,7 +123,7 @@ class KokoroTTSService(TTSService):
model_path: Optional[str] = None,
voices_path: Optional[str] = None,
params: Optional[InputParams] = None,
settings: Optional[KokoroTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Kokoro TTS service.
@@ -132,14 +132,14 @@ class KokoroTTSService(TTSService):
voice_id: Voice identifier to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=KokoroTTSSettings(voice=...)`` instead.
Use ``settings=KokoroTTSService.Settings(voice=...)`` instead.
model_path: Path to the kokoro ONNX model file. Defaults to auto-downloaded file.
voices_path: Path to the voices binary file. Defaults to auto-downloaded file.
params: Configuration parameters for synthesis.
.. deprecated:: 0.0.105
Use ``settings=KokoroTTSSettings(...)`` instead.
Use ``settings=KokoroTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -147,22 +147,22 @@ class KokoroTTSService(TTSService):
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = KokoroTTSSettings(
default_settings = self.Settings(
model=None,
voice=None,
language=language_to_kokoro_language(Language.EN),
language=Language.EN,
)
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", KokoroTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", KokoroTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.language = language_to_kokoro_language(params.language)
default_settings.language = params.language
# 4. Apply settings delta (canonical API, always wins)
if settings is not None:

View File

@@ -244,7 +244,10 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
return self.get_llm_adapter().create_llm_specific_message(message)
async def run_inference(
self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None
self,
context: LLMContext | OpenAILLMContext,
max_tokens: Optional[int] = None,
system_instruction: Optional[str] = None,
) -> Optional[str]:
"""Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context.
@@ -254,6 +257,8 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
context: The LLM context containing conversation history.
max_tokens: Optional maximum number of tokens to generate. If provided,
overrides the service's default max_tokens/max_completion_tokens setting.
system_instruction: Optional system instruction to use for this inference.
If provided, overrides any system instruction in the context.
Returns:
The LLM's response as a string, or None if no response is generated.
@@ -398,7 +403,9 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
elif isinstance(frame, LLMConfigureOutputFrame):
self._skip_tts = frame.skip_tts
elif isinstance(frame, LLMUpdateSettingsFrame):
if frame.delta is not None:
if frame.service is not None and frame.service is not self:
await self.push_frame(frame, direction)
elif frame.delta is not None:
await self._update_settings(frame.delta)
elif frame.settings:
# Backward-compatible path: convert legacy dict to settings object.
@@ -535,23 +542,17 @@ class LLMService(UserTurnCompletionLLMServiceMixin, AIService):
# Create summary context
transcript = LLMContextSummarizationUtil.format_messages_for_summary(result.messages)
prompt_messages = [
{
"role": "system",
"content": frame.summarization_prompt,
},
{
"role": "user",
"content": f"Conversation history:\n{transcript}",
},
]
summary_context = LLMContext(messages=prompt_messages)
summary_context = LLMContext(
messages=[{"role": "user", "content": f"Conversation history:\n{transcript}"}]
)
# Generate summary using run_inference
# This will be overridden by each LLM service implementation
try:
summary_text = await self.run_inference(
summary_context, max_tokens=frame.target_context_tokens
summary_context,
max_tokens=frame.target_context_tokens,
system_instruction=frame.summarization_prompt,
)
except NotImplementedError:
raise RuntimeError(

View File

@@ -22,7 +22,7 @@ from pipecat.frames.frames import (
TTSStoppedFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import TTSSettings, _warn_deprecated_param
from pipecat.services.settings import TTSSettings
from pipecat.services.tts_service import InterruptibleTTSService
from pipecat.transcriptions.language import Language, resolve_language
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -89,7 +89,7 @@ class LmntTTSService(InterruptibleTTSService):
"""
Settings = LmntTTSSettings
_settings: LmntTTSSettings
_settings: Settings
def __init__(
self,
@@ -100,7 +100,7 @@ class LmntTTSService(InterruptibleTTSService):
language: Language = Language.EN,
output_format: str = "pcm_s16le",
model: Optional[str] = None,
settings: Optional[LmntTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the LMNT TTS service.
@@ -110,34 +110,41 @@ class LmntTTSService(InterruptibleTTSService):
voice_id: ID of the voice to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=LmntTTSSettings(voice=...)`` instead.
Use ``settings=LmntTTSService.Settings(voice=...)`` instead.
sample_rate: Audio sample rate. If None, uses default.
language: Language for synthesis. Defaults to English.
.. deprecated:: 0.0.106
Use ``settings=LmntTTSService.Settings(language=...)`` instead.
output_format: Audio output format. One of "pcm_s16le", "pcm_f32le",
"mp3", "ulaw", "webm". Defaults to "pcm_s16le".
model: TTS model to use.
.. deprecated:: 0.0.105
Use ``settings=LmntTTSSettings(model=...)`` instead.
Use ``settings=LmntTTSService.Settings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to parent InterruptibleTTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = LmntTTSSettings(
default_settings = self.Settings(
model="aurora",
voice=None,
language=self.language_to_service_language(language),
language=Language.EN,
)
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", LmntTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
if language is not None:
self._warn_init_param_moved_to_settings("language", "language")
default_settings.language = language
if model is not None:
_warn_deprecated_param("model", LmntTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)
@@ -237,7 +244,7 @@ class LmntTTSService(InterruptibleTTSService):
"""Apply a settings delta.
Args:
delta: A :class:`TTSSettings` (or ``LmntTTSSettings``) delta.
delta: A :class:`TTSSettings` (or ``LmntTTSService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.

View File

@@ -24,7 +24,7 @@ from pipecat.frames.frames import (
StartFrame,
TTSAudioRawFrame,
)
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import TTSService
from pipecat.transcriptions.language import Language, resolve_language
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -141,13 +141,13 @@ class MiniMaxHttpTTSService(TTSService):
"""
Settings = MiniMaxTTSSettings
_settings: MiniMaxTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Configuration parameters for MiniMax TTS.
.. deprecated:: 0.0.105
Use ``MiniMaxTTSSettings`` directly via the ``settings`` parameter instead.
Use ``MiniMaxHttpTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
language: Language for TTS generation. Supports 40 languages.
@@ -190,7 +190,7 @@ class MiniMaxHttpTTSService(TTSService):
sample_rate: Optional[int] = None,
stream: bool = True,
params: Optional[InputParams] = None,
settings: Optional[MiniMaxTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the MiniMax TTS service.
@@ -208,12 +208,12 @@ class MiniMaxHttpTTSService(TTSService):
"speech-01-hd", "speech-01-turbo".
.. deprecated:: 0.0.105
Use ``settings=MiniMaxTTSSettings(model=...)`` instead.
Use ``settings=MiniMaxHttpTTSService.Settings(model=...)`` instead.
voice_id: Voice identifier. Defaults to "Calm_Woman".
.. deprecated:: 0.0.105
Use ``settings=MiniMaxTTSSettings(voice=...)`` instead.
Use ``settings=MiniMaxHttpTTSService.Settings(voice=...)`` instead.
aiohttp_session: aiohttp.ClientSession for API communication.
sample_rate: Output audio sample rate in Hz. If None, uses pipeline default.
@@ -221,14 +221,14 @@ class MiniMaxHttpTTSService(TTSService):
params: Additional configuration parameters.
.. deprecated:: 0.0.105
Use ``settings=MiniMaxTTSSettings(...)`` instead.
Use ``settings=MiniMaxHttpTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to parent TTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = MiniMaxTTSSettings(
default_settings = self.Settings(
model="speech-02-turbo",
voice="Calm_Woman",
language=None,
@@ -243,15 +243,15 @@ class MiniMaxHttpTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", MiniMaxTTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if voice_id is not None:
_warn_deprecated_param("voice_id", MiniMaxTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", MiniMaxTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.speed = params.speed
default_settings.volume = params.volume

View File

@@ -14,13 +14,12 @@ from openai.types.chat import ChatCompletionMessageParam
from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams
from pipecat.frames.frames import FunctionCallFromLLM
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class MistralLLMSettings(OpenAILLMSettings):
class MistralLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for MistralLLMService."""
pass
@@ -34,7 +33,7 @@ class MistralLLMService(OpenAILLMService):
"""
Settings = MistralLLMSettings
_settings: MistralLLMSettings
_settings: Settings
def __init__(
self,
@@ -42,7 +41,7 @@ class MistralLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.mistral.ai/v1",
model: Optional[str] = None,
settings: Optional[MistralLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Mistral LLM service.
@@ -53,18 +52,18 @@ class MistralLLMService(OpenAILLMService):
model: The model identifier to use. Defaults to "mistral-small-latest".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=MistralLLMService.Settings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = MistralLLMSettings(model="mistral-small-latest")
default_settings = self.Settings(model="mistral-small-latest")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", MistralLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)
@@ -237,6 +236,10 @@ class MistralLLMService(OpenAILLMService):
# Prepend system instruction if set
if self._settings.system_instruction:
messages = params.get("messages", [])
if messages and messages[0].get("role") == "system":
logger.warning(
f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended."
)
params["messages"] = [
{"role": "system", "content": self._settings.system_instruction}
] + messages

View File

@@ -25,7 +25,7 @@ from pipecat.frames.frames import (
VisionFullResponseStartFrame,
VisionTextFrame,
)
from pipecat.services.settings import VisionSettings, _warn_deprecated_param
from pipecat.services.settings import VisionSettings
from pipecat.services.vision_service import VisionService
try:
@@ -80,7 +80,7 @@ class MoondreamService(VisionService):
"""
Settings = MoondreamSettings
_settings: MoondreamSettings
_settings: Settings
def __init__(
self,
@@ -88,7 +88,7 @@ class MoondreamService(VisionService):
model: Optional[str] = None,
revision="2025-01-09",
use_cpu=False,
settings: Optional[MoondreamSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Moondream service.
@@ -97,7 +97,7 @@ class MoondreamService(VisionService):
model: Hugging Face model identifier for the Moondream model.
.. deprecated:: 0.0.105
Use ``settings=MoondreamSettings(model=...)`` instead.
Use ``settings=MoondreamService.Settings(model=...)`` instead.
revision: Specific model revision to use.
use_cpu: Whether to force CPU usage instead of hardware acceleration.
@@ -106,11 +106,11 @@ class MoondreamService(VisionService):
**kwargs: Additional arguments passed to the parent VisionService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = MoondreamSettings(model="vikhyatk/moondream2")
default_settings = self.Settings(model="vikhyatk/moondream2")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", MoondreamSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -33,7 +33,7 @@ from pipecat.frames.frames import (
TTSStoppedFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import InterruptibleTTSService, TextAggregationMode, TTSService
from pipecat.transcriptions.language import Language, resolve_language
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -92,13 +92,13 @@ class NeuphonicTTSService(InterruptibleTTSService):
"""
Settings = NeuphonicTTSSettings
_settings: NeuphonicTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Neuphonic TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=NeuphonicTTSSettings(...)`` instead.
Use ``settings=NeuphonicTTSService.Settings(...)`` instead.
Parameters:
language: Language for synthesis. Defaults to English.
@@ -117,7 +117,7 @@ class NeuphonicTTSService(InterruptibleTTSService):
sample_rate: Optional[int] = 22050,
encoding: str = "pcm_linear",
params: Optional[InputParams] = None,
settings: Optional[NeuphonicTTSSettings] = None,
settings: Optional[Settings] = None,
aggregate_sentences: Optional[bool] = None,
text_aggregation_mode: Optional[TextAggregationMode] = None,
**kwargs,
@@ -129,7 +129,7 @@ class NeuphonicTTSService(InterruptibleTTSService):
voice_id: ID of the voice to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=NeuphonicTTSSettings(voice=...)`` instead.
Use ``settings=NeuphonicTTSService.Settings(voice=...)`` instead.
url: WebSocket URL for the Neuphonic API.
sample_rate: Audio sample rate in Hz. Defaults to 22050.
@@ -137,7 +137,7 @@ class NeuphonicTTSService(InterruptibleTTSService):
params: Additional input parameters for TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=NeuphonicTTSSettings(...)`` instead.
Use ``settings=NeuphonicTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -150,24 +150,24 @@ class NeuphonicTTSService(InterruptibleTTSService):
**kwargs: Additional arguments passed to parent InterruptibleTTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = NeuphonicTTSSettings(
default_settings = self.Settings(
model=None,
voice=None,
language=self.language_to_service_language(Language.EN),
language=Language.EN,
speed=1.0,
)
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", NeuphonicTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", NeuphonicTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = self.language_to_service_language(params.language)
default_settings.language = params.language
if params.speed is not None:
default_settings.speed = params.speed
@@ -432,13 +432,13 @@ class NeuphonicHttpTTSService(TTSService):
"""
Settings = NeuphonicTTSSettings
_settings: NeuphonicTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Neuphonic HTTP TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=NeuphonicTTSSettings(...)`` instead.
Use ``settings=NeuphonicHttpTTSService.Settings(...)`` instead.
Parameters:
language: Language for synthesis. Defaults to English.
@@ -458,7 +458,7 @@ class NeuphonicHttpTTSService(TTSService):
sample_rate: Optional[int] = 22050,
encoding: Optional[str] = "pcm_linear",
params: Optional[InputParams] = None,
settings: Optional[NeuphonicTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Neuphonic HTTP TTS service.
@@ -468,7 +468,7 @@ class NeuphonicHttpTTSService(TTSService):
voice_id: ID of the voice to use for synthesis.
.. deprecated:: 0.0.105
Use ``settings=NeuphonicTTSSettings(voice=...)`` instead.
Use ``settings=NeuphonicHttpTTSService.Settings(voice=...)`` instead.
aiohttp_session: Shared aiohttp session for HTTP requests.
url: Base URL for the Neuphonic HTTP API.
@@ -477,31 +477,31 @@ class NeuphonicHttpTTSService(TTSService):
params: Additional input parameters for TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=NeuphonicTTSSettings(...)`` instead.
Use ``settings=NeuphonicHttpTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to parent TTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = NeuphonicTTSSettings(
default_settings = self.Settings(
model=None,
voice=None,
language=self.language_to_service_language(Language.EN),
language=Language.EN,
speed=1.0,
)
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", NeuphonicTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", NeuphonicTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = self.language_to_service_language(params.language)
default_settings.language = params.language
if params.speed is not None:
default_settings.speed = params.speed

View File

@@ -16,13 +16,12 @@ from typing import Optional
from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class NvidiaLLMSettings(OpenAILLMSettings):
class NvidiaLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for NvidiaLLMService."""
pass
@@ -37,7 +36,7 @@ class NvidiaLLMService(OpenAILLMService):
"""
Settings = NvidiaLLMSettings
_settings: NvidiaLLMSettings
_settings: Settings
def __init__(
self,
@@ -45,7 +44,7 @@ class NvidiaLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://integrate.api.nvidia.com/v1",
model: Optional[str] = None,
settings: Optional[NvidiaLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the NvidiaLLMService.
@@ -57,18 +56,18 @@ class NvidiaLLMService(OpenAILLMService):
"nvidia/llama-3.1-nemotron-70b-instruct".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=NvidiaLLMService.Settings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = NvidiaLLMSettings(model="nvidia/llama-3.1-nemotron-70b-instruct")
default_settings = self.Settings(model="nvidia/llama-3.1-nemotron-70b-instruct")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", NvidiaLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)

View File

@@ -23,7 +23,7 @@ from pipecat.frames.frames import (
StartFrame,
TranscriptionFrame,
)
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven
from pipecat.services.stt_latency import NVIDIA_TTFS_P99
from pipecat.services.stt_service import SegmentedSTTService, STTService
from pipecat.transcriptions.language import Language, resolve_language
@@ -126,13 +126,13 @@ class NvidiaSTTService(STTService):
"""
Settings = NvidiaSTTSettings
_settings: NvidiaSTTSettings
_settings: Settings
class InputParams(BaseModel):
"""Configuration parameters for NVIDIA Riva STT service.
.. deprecated:: 0.0.105
Use ``settings=NvidiaSTTSettings(...)`` instead.
Use ``settings=NvidiaSTTService.Settings(...)`` instead.
Parameters:
language: Target language for transcription. Defaults to EN_US.
@@ -152,7 +152,7 @@ class NvidiaSTTService(STTService):
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
use_ssl: bool = True,
settings: Optional[NvidiaSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = NVIDIA_TTFS_P99,
**kwargs,
):
@@ -166,7 +166,7 @@ class NvidiaSTTService(STTService):
params: Additional configuration parameters for NVIDIA Riva.
.. deprecated:: 0.0.105
Use ``settings=NvidiaSTTSettings(...)`` instead.
Use ``settings=NvidiaSTTService.Settings(...)`` instead.
use_ssl: Whether to use SSL for the NVIDIA Riva server. Defaults to True.
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -176,7 +176,7 @@ class NvidiaSTTService(STTService):
**kwargs: Additional arguments passed to STTService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = NvidiaSTTSettings(
default_settings = self.Settings(
model=model_function_map.get("model_name"),
language=Language.EN_US,
)
@@ -185,7 +185,7 @@ class NvidiaSTTService(STTService):
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", NvidiaSTTSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.language = params.language
@@ -441,13 +441,13 @@ class NvidiaSegmentedSTTService(SegmentedSTTService):
"""
Settings = NvidiaSegmentedSTTSettings
_settings: NvidiaSegmentedSTTSettings
_settings: Settings
class InputParams(BaseModel):
"""Configuration parameters for NVIDIA Riva segmented STT service.
.. deprecated:: 0.0.105
Use ``settings=NvidiaSegmentedSTTSettings(...)`` instead.
Use ``settings=NvidiaSegmentedSTTService.Settings(...)`` instead.
Parameters:
language: Target language for transcription. Defaults to EN_US.
@@ -477,7 +477,7 @@ class NvidiaSegmentedSTTService(SegmentedSTTService):
sample_rate: Optional[int] = None,
params: Optional[InputParams] = None,
use_ssl: bool = True,
settings: Optional[NvidiaSegmentedSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = NVIDIA_TTFS_P99,
**kwargs,
):
@@ -491,7 +491,7 @@ class NvidiaSegmentedSTTService(SegmentedSTTService):
params: Additional configuration parameters for NVIDIA Riva
.. deprecated:: 0.0.105
Use ``settings=NvidiaSegmentedSTTSettings(...)`` instead.
Use ``settings=NvidiaSegmentedSTTService.Settings(...)`` instead.
use_ssl: Whether to use SSL for the NVIDIA Riva server. Defaults to True.
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -501,9 +501,9 @@ class NvidiaSegmentedSTTService(SegmentedSTTService):
**kwargs: Additional arguments passed to SegmentedSTTService
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = NvidiaSegmentedSTTSettings(
default_settings = self.Settings(
model=model_function_map.get("model_name"),
language=language_to_nvidia_riva_language(Language.EN_US) or "en-US",
language=Language.EN_US,
profanity_filter=False,
automatic_punctuation=True,
verbatim_transcripts=False,
@@ -515,11 +515,9 @@ class NvidiaSegmentedSTTService(SegmentedSTTService):
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", NvidiaSegmentedSTTSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.language = (
language_to_nvidia_riva_language(params.language or Language.EN_US) or "en-US"
)
default_settings.language = params.language or Language.EN_US
default_settings.profanity_filter = params.profanity_filter
default_settings.automatic_punctuation = params.automatic_punctuation
default_settings.verbatim_transcripts = params.verbatim_transcripts
@@ -641,7 +639,7 @@ class NvidiaSegmentedSTTService(SegmentedSTTService):
"""Apply a settings delta and sync internal state.
Args:
delta: A :class:`STTSettings` (or ``NvidiaSegmentedSTTSettings``) delta.
delta: A :class:`STTSettings` (or ``NvidiaSegmentedSTTService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.

View File

@@ -29,7 +29,7 @@ from pipecat.frames.frames import (
StartFrame,
TTSAudioRawFrame,
)
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import TTSService
from pipecat.transcriptions.language import Language
@@ -62,13 +62,13 @@ class NvidiaTTSService(TTSService):
"""
Settings = NvidiaTTSSettings
_settings: NvidiaTTSSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for Riva TTS configuration.
.. deprecated:: 0.0.105
Use ``NvidiaTTSSettings`` directly via the ``settings`` parameter instead.
Use ``NvidiaTTSService.Settings`` directly via the ``settings`` parameter instead.
Parameters:
language: Language code for synthesis. Defaults to US English.
@@ -90,7 +90,7 @@ class NvidiaTTSService(TTSService):
"model_name": "magpie-tts-multilingual",
},
params: Optional[InputParams] = None,
settings: Optional[NvidiaTTSSettings] = None,
settings: Optional[Settings] = None,
use_ssl: bool = True,
**kwargs,
):
@@ -102,14 +102,14 @@ class NvidiaTTSService(TTSService):
voice_id: Voice model identifier. Defaults to multilingual Aria voice.
.. deprecated:: 0.0.105
Use ``settings=NvidiaTTSSettings(voice=...)`` instead.
Use ``settings=NvidiaTTSService.Settings(voice=...)`` instead.
sample_rate: Audio sample rate. If None, uses service default.
model_function_map: Dictionary containing function_id and model_name for the TTS model.
params: Additional configuration parameters for TTS synthesis.
.. deprecated:: 0.0.105
Use ``settings=NvidiaTTSSettings(...)`` instead.
Use ``settings=NvidiaTTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -117,7 +117,7 @@ class NvidiaTTSService(TTSService):
**kwargs: Additional arguments passed to parent TTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = NvidiaTTSSettings(
default_settings = self.Settings(
model=model_function_map.get("model_name"),
voice="Magpie-Multilingual.EN-US.Aria",
language=Language.EN_US,
@@ -126,12 +126,12 @@ class NvidiaTTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", NvidiaTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", NvidiaTTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.language is not None:
default_settings.language = params.language
@@ -186,7 +186,7 @@ class NvidiaTTSService(TTSService):
stacklevel=2,
)
async def _update_settings(self, delta: NvidiaTTSSettings) -> dict[str, Any]:
async def _update_settings(self, delta: Settings) -> dict[str, Any]:
"""Apply a settings delta.
Settings are stored but not applied to the active connection.

View File

@@ -11,13 +11,12 @@ from typing import Optional
from loguru import logger
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class OllamaLLMSettings(OpenAILLMSettings):
class OllamaLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for OLLamaLLMService."""
pass
@@ -31,14 +30,14 @@ class OLLamaLLMService(OpenAILLMService):
"""
Settings = OllamaLLMSettings
_settings: OllamaLLMSettings
_settings: Settings
def __init__(
self,
*,
model: Optional[str] = None,
base_url: str = "http://localhost:11434/v1",
settings: Optional[OllamaLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize OLLama LLM service.
@@ -47,7 +46,7 @@ class OLLamaLLMService(OpenAILLMService):
model: The OLLama model to use. Defaults to "llama2".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=OLLamaLLMService.Settings(model=...)`` instead.
base_url: The base URL for the OLLama API endpoint.
Defaults to "http://localhost:11434/v1".
@@ -56,11 +55,11 @@ class OLLamaLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OllamaLLMSettings(model="llama2")
default_settings = self.Settings(model="llama2")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OllamaLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)

View File

@@ -11,6 +11,7 @@ from pipecat.services import DeprecatedModuleProxy
from .image import *
from .llm import *
from .realtime import *
from .responses.llm import *
from .stt import *
from .tts import *

View File

@@ -69,13 +69,13 @@ class BaseOpenAILLMService(LLMService):
"""
Settings = OpenAILLMSettings
_settings: OpenAILLMSettings
_settings: Settings
class InputParams(BaseModel):
"""Input parameters for OpenAI model configuration.
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(...)`` instead of
Use ``settings=BaseOpenAILLMService.Settings(...)`` instead of
``params=InputParams(...)``.
Parameters:
@@ -119,7 +119,7 @@ class BaseOpenAILLMService(LLMService):
default_headers: Optional[Mapping[str, str]] = None,
service_tier: Optional[str] = None,
params: Optional[InputParams] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[Settings] = None,
retry_timeout_secs: Optional[float] = 5.0,
retry_on_timeout: Optional[bool] = False,
**kwargs,
@@ -130,7 +130,7 @@ class BaseOpenAILLMService(LLMService):
model: The OpenAI model name to use (e.g., "gpt-4.1", "gpt-4o").
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=BaseOpenAILLMService.Settings(model=...)`` instead.
api_key: OpenAI API key. If None, uses environment variable.
base_url: Custom base URL for OpenAI API. If None, uses default.
@@ -141,7 +141,7 @@ class BaseOpenAILLMService(LLMService):
params: Input parameters for model configuration and behavior.
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(...)`` instead.
Use ``settings=BaseOpenAILLMService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -150,8 +150,8 @@ class BaseOpenAILLMService(LLMService):
**kwargs: Additional arguments passed to the parent LLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(
model="gpt-4o",
default_settings = self.Settings(
model="gpt-4.1",
system_instruction=None,
frequency_penalty=NOT_GIVEN,
presence_penalty=NOT_GIVEN,
@@ -327,13 +327,12 @@ class BaseOpenAILLMService(LLMService):
params.update(self._settings.extra)
# Prepend system instruction from constructor, replacing any context system message
# Prepend system instruction from constructor
if self._settings.system_instruction:
messages = params.get("messages", [])
if messages and messages[0].get("role") == "system":
logger.warning(
f"{self}: Both system_instruction and a system message in context are set."
" Using system_instruction."
f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended."
)
params["messages"] = [
{"role": "system", "content": self._settings.system_instruction}
@@ -342,7 +341,10 @@ class BaseOpenAILLMService(LLMService):
return params
async def run_inference(
self, context: LLMContext | OpenAILLMContext, max_tokens: Optional[int] = None
self,
context: LLMContext | OpenAILLMContext,
max_tokens: Optional[int] = None,
system_instruction: Optional[str] = None,
) -> Optional[str]:
"""Run a one-shot, out-of-band (i.e. out-of-pipeline) inference with the given LLM context.
@@ -350,6 +352,8 @@ class BaseOpenAILLMService(LLMService):
context: The LLM context containing conversation history.
max_tokens: Optional maximum number of tokens to generate. If provided,
overrides the service's default max_tokens/max_completion_tokens setting.
system_instruction: Optional system instruction to use for this inference.
If provided, overrides any system instruction in the context.
Returns:
The LLM's response as a string, or None if no response is generated.
@@ -371,6 +375,15 @@ class BaseOpenAILLMService(LLMService):
params["stream"] = False
params.pop("stream_options", None)
# Prepend system instruction if provided
if system_instruction is not None:
messages = params.get("messages", [])
if messages and messages[0].get("role") == "system":
logger.warning(
f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended."
)
params["messages"] = [{"role": "system", "content": system_instruction}] + messages
# Override max_tokens if provided
if max_tokens is not None:
# Use max_completion_tokens for newer models, fallback to max_tokens

View File

@@ -25,7 +25,7 @@ from pipecat.frames.frames import (
URLImageRawFrame,
)
from pipecat.services.image_service import ImageGenService
from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, ImageGenSettings, _NotGiven
@dataclass
@@ -49,7 +49,7 @@ class OpenAIImageGenService(ImageGenService):
"""
Settings = OpenAIImageGenSettings
_settings: OpenAIImageGenSettings
_settings: Settings
def __init__(
self,
@@ -61,7 +61,7 @@ class OpenAIImageGenService(ImageGenService):
Literal["256x256", "512x512", "1024x1024", "1792x1024", "1024x1792"]
] = None,
model: Optional[str] = None,
settings: Optional[OpenAIImageGenSettings] = None,
settings: Optional[Settings] = None,
):
"""Initialize the OpenAI image generation service.
@@ -72,29 +72,29 @@ class OpenAIImageGenService(ImageGenService):
image_size: Target size for generated images. Defaults to "1024x1024".
.. deprecated:: 0.0.105
Use ``settings=OpenAIImageGenSettings(image_size=...)`` instead.
Use ``settings=OpenAIImageGenService.Settings(image_size=...)`` instead.
model: DALL-E model to use for generation. Defaults to "dall-e-3".
.. deprecated:: 0.0.105
Use ``settings=OpenAIImageGenSettings(model=...)`` instead.
Use ``settings=OpenAIImageGenService.Settings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAIImageGenSettings(
default_settings = self.Settings(
model="dall-e-3",
image_size=None,
)
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAIImageGenSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if image_size is not None:
_warn_deprecated_param("image_size", OpenAIImageGenSettings, "image_size")
self._warn_init_param_moved_to_settings("image_size", "image_size")
default_settings.image_size = image_size
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -25,8 +25,7 @@ from pipecat.processors.aggregators.llm_response import (
LLMUserContextAggregator,
)
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.openai.base_llm import BaseOpenAILLMService, OpenAILLMSettings
from pipecat.services.settings import _warn_deprecated_param
from pipecat.services.openai.base_llm import BaseOpenAILLMService
@dataclass
@@ -72,13 +71,15 @@ class OpenAILLMService(BaseOpenAILLMService):
context aggregator creation.
"""
Settings = BaseOpenAILLMService.Settings
def __init__(
self,
*,
model: Optional[str] = None,
service_tier: Optional[str] = None,
params: Optional[BaseOpenAILLMService.InputParams] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize OpenAI LLM service.
@@ -87,20 +88,20 @@ class OpenAILLMService(BaseOpenAILLMService):
model: The OpenAI model name to use. Defaults to "gpt-4.1".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=OpenAILLMService.Settings(model=...)`` instead.
service_tier: Service tier to use (e.g., "auto", "flex", "priority").
params: Input parameters for model configuration.
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(...)`` instead.
Use ``settings=OpenAILLMService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to the parent BaseOpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(
default_settings = self.Settings(
model="gpt-4.1",
system_instruction=None,
frequency_penalty=NOT_GIVEN,
@@ -118,7 +119,7 @@ class OpenAILLMService(BaseOpenAILLMService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# Handle service_tier from deprecated params
@@ -127,7 +128,7 @@ class OpenAILLMService(BaseOpenAILLMService):
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", OpenAILLMSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
default_settings.frequency_penalty = params.frequency_penalty
default_settings.presence_penalty = params.presence_penalty
@@ -254,7 +255,7 @@ class OpenAIAssistantContextAggregator(LLMAssistantContextAggregator):
frame: Frame containing the function call result.
"""
if frame.result:
result = json.dumps(frame.result)
result = json.dumps(frame.result, ensure_ascii=False)
await self._update_function_call_result(frame.function_name, frame.tool_call_id, result)
else:
await self._update_function_call_result(

View File

@@ -63,7 +63,6 @@ from pipecat.services.settings import (
NOT_GIVEN,
LLMSettings,
_NotGiven,
_warn_deprecated_param,
is_given,
)
from pipecat.transcriptions.language import Language
@@ -115,7 +114,7 @@ class OpenAIRealtimeLLMSettings(LLMSettings):
# -- Bidirectional sync helpers ------------------------------------------
@staticmethod
def _sync_top_level_to_sp(settings: "OpenAIRealtimeLLMSettings"):
def _sync_top_level_to_sp(settings: "OpenAIRealtimeLLMService.Settings"):
"""Push top-level ``model``/``system_instruction`` into ``session_properties``."""
if not is_given(settings.session_properties):
return
@@ -127,7 +126,7 @@ class OpenAIRealtimeLLMSettings(LLMSettings):
# -- apply_update override -----------------------------------------------
def apply_update(self, delta: "OpenAIRealtimeLLMSettings") -> Dict[str, Any]:
def apply_update(self, delta: "OpenAIRealtimeLLMService.Settings") -> Dict[str, Any]:
"""Merge a delta, keeping ``model``/``system_instruction`` in sync with SP.
When the delta contains ``session_properties``, it **replaces** the
@@ -165,8 +164,8 @@ class OpenAIRealtimeLLMSettings(LLMSettings):
@classmethod
def from_mapping(
cls: Type["OpenAIRealtimeLLMSettings"], settings: Mapping[str, Any]
) -> "OpenAIRealtimeLLMSettings":
cls: Type["OpenAIRealtimeLLMService.Settings"], settings: Mapping[str, Any]
) -> "OpenAIRealtimeLLMService.Settings":
"""Build a delta from a plain dict, routing SP keys into ``session_properties``.
Keys that correspond to ``SessionProperties`` fields (except ``model``)
@@ -211,7 +210,7 @@ class OpenAIRealtimeLLMService(LLMService):
"""
Settings = OpenAIRealtimeLLMSettings
_settings: OpenAIRealtimeLLMSettings
_settings: Settings
# Overriding the default adapter to use the OpenAIRealtimeLLMAdapter one.
adapter_class = OpenAIRealtimeLLMAdapter
@@ -223,7 +222,7 @@ class OpenAIRealtimeLLMService(LLMService):
model: Optional[str] = None,
base_url: str = "wss://api.openai.com/v1/realtime",
session_properties: Optional[events.SessionProperties] = None,
settings: Optional[OpenAIRealtimeLLMSettings] = None,
settings: Optional[Settings] = None,
start_audio_paused: bool = False,
start_video_paused: bool = False,
video_frame_detail: str = "auto",
@@ -237,7 +236,7 @@ class OpenAIRealtimeLLMService(LLMService):
model: OpenAI model name.
.. deprecated:: 0.0.105
Use ``settings=OpenAIRealtimeLLMSettings(model=...)`` instead.
Use ``settings=OpenAIRealtimeLLMService.Settings(model=...)`` instead.
This is a connection-level parameter set via the WebSocket URL query
parameter and cannot be changed during the session.
@@ -247,7 +246,7 @@ class OpenAIRealtimeLLMService(LLMService):
If None, uses default SessionProperties.
.. deprecated:: 0.0.105
Use ``settings=OpenAIRealtimeLLMSettings(session_properties=...)``
Use ``settings=OpenAIRealtimeLLMService.Settings(session_properties=...)``
instead.
settings: Runtime-updatable settings for this service.
start_audio_paused: Whether to start with audio input paused. Defaults to False.
@@ -277,7 +276,7 @@ class OpenAIRealtimeLLMService(LLMService):
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAIRealtimeLLMSettings(
default_settings = self.Settings(
model="gpt-realtime-1.5",
system_instruction=None,
temperature=None,
@@ -294,13 +293,13 @@ class OpenAIRealtimeLLMService(LLMService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAIRealtimeLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if session_properties is not None:
_warn_deprecated_param(
"session_properties",
OpenAIRealtimeLLMSettings,
self.Settings,
"session_properties",
)
default_settings.session_properties = session_properties
@@ -312,7 +311,7 @@ class OpenAIRealtimeLLMService(LLMService):
default_settings.system_instruction = session_properties.instructions
# Sync top-level model back into session_properties
OpenAIRealtimeLLMSettings._sync_top_level_to_sp(default_settings)
self.Settings._sync_top_level_to_sp(default_settings)
# 3. Apply settings delta (canonical API, always wins)
if settings is not None:
@@ -747,7 +746,10 @@ class OpenAIRealtimeLLMService(LLMService):
await self._handle_evt_function_call_arguments_done(evt)
elif evt.type == "error":
if not await self._maybe_handle_evt_retrieve_conversation_item_error(evt):
if evt.error.code == "response_cancel_not_active":
if evt.error.code in (
"response_cancel_not_active",
"conversation_already_has_active_response",
):
logger.debug(f"{self} {evt.error.message}")
else:
await self._handle_evt_error(evt)
@@ -1126,7 +1128,7 @@ class OpenAIRealtimeLLMService(LLMService):
item = events.ConversationItem(
type="function_call_output",
call_id=tool_call_id,
output=json.dumps(result),
output=json.dumps(result, ensure_ascii=False),
)
await self.send_client_event(events.ConversationItemCreateEvent(item=item))

View File

@@ -0,0 +1,5 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#

View File

@@ -0,0 +1,400 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""OpenAI Responses API LLM service implementation."""
import json
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from typing import Any, Dict, List, Mapping, Optional
import httpx
from loguru import logger
from openai import NOT_GIVEN, AsyncOpenAI, AsyncStream, DefaultAsyncHttpxClient
from openai.types.responses import (
ResponseCompletedEvent,
ResponseFunctionCallArgumentsDeltaEvent,
ResponseFunctionCallArgumentsDoneEvent,
ResponseFunctionToolCall,
ResponseOutputItemAddedEvent,
ResponseOutputItemDoneEvent,
ResponseStreamEvent,
ResponseTextDeltaEvent,
)
from pipecat.adapters.services.open_ai_responses_adapter import (
OpenAIResponsesLLMAdapter,
OpenAIResponsesLLMInvocationParams,
)
from pipecat.frames.frames import (
Frame,
LLMContextFrame,
LLMFullResponseEndFrame,
LLMFullResponseStartFrame,
)
from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.llm_service import FunctionCallFromLLM, LLMService
from pipecat.services.settings import NOT_GIVEN as _NOT_GIVEN
from pipecat.services.settings import LLMSettings, _NotGiven
from pipecat.utils.tracing.service_decorators import traced_llm
@dataclass
class OpenAIResponsesLLMSettings(LLMSettings):
"""Settings for OpenAIResponsesLLMService.
Parameters:
max_completion_tokens: Maximum completion tokens to generate.
"""
max_completion_tokens: int | _NotGiven = field(default_factory=lambda: _NOT_GIVEN)
class OpenAIResponsesLLMService(LLMService):
"""OpenAI Responses API LLM service.
This service works with the universal LLMContext and LLMContextAggregatorPair.
Example::
llm = OpenAIResponsesLLMService(
api_key=os.getenv("OPENAI_API_KEY"),
settings=OpenAIResponsesLLMService.Settings(
model="gpt-4.1",
system_instruction="You are a helpful assistant.",
),
)
"""
Settings = OpenAIResponsesLLMSettings
_settings: Settings
adapter_class = OpenAIResponsesLLMAdapter
def __init__(
self,
*,
api_key=None,
base_url=None,
organization=None,
project=None,
default_headers: Optional[Mapping[str, str]] = None,
service_tier: Optional[str] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the OpenAI Responses API LLM service.
Args:
api_key: OpenAI API key. If None, uses environment variable.
base_url: Custom base URL for OpenAI API. If None, uses default.
organization: OpenAI organization ID.
project: OpenAI project ID.
default_headers: Additional HTTP headers to include in requests.
service_tier: Service tier to use (e.g., "auto", "flex", "priority").
settings: Runtime-updatable settings.
**kwargs: Additional arguments passed to the parent LLMService.
"""
default_settings = self.Settings(
model="gpt-4.1",
system_instruction=None,
frequency_penalty=None,
presence_penalty=None,
seed=None,
temperature=NOT_GIVEN,
top_p=NOT_GIVEN,
top_k=None,
max_tokens=None,
max_completion_tokens=NOT_GIVEN,
filter_incomplete_user_turns=False,
user_turn_completion_config=None,
extra={},
)
if settings is not None:
default_settings.apply_update(settings)
super().__init__(
settings=default_settings,
**kwargs,
)
self._service_tier = service_tier
self._client = self._create_client(
api_key=api_key,
base_url=base_url,
organization=organization,
project=project,
default_headers=default_headers,
)
if self._settings.system_instruction:
logger.debug(f"{self}: Using system instruction: {self._settings.system_instruction}")
def _create_client(
self,
api_key=None,
base_url=None,
organization=None,
project=None,
default_headers=None,
) -> AsyncOpenAI:
"""Create an AsyncOpenAI client instance.
Args:
api_key: OpenAI API key.
base_url: Custom base URL for the API.
organization: OpenAI organization ID.
project: OpenAI project ID.
default_headers: Additional HTTP headers.
Returns:
Configured AsyncOpenAI client instance.
"""
return AsyncOpenAI(
api_key=api_key,
base_url=base_url,
organization=organization,
project=project,
http_client=DefaultAsyncHttpxClient(
limits=httpx.Limits(
max_keepalive_connections=100, max_connections=1000, keepalive_expiry=None
)
),
default_headers=default_headers,
)
def can_generate_metrics(self) -> bool:
"""Check if this service can generate processing metrics."""
return True
async def process_frame(self, frame: Frame, direction: FrameDirection):
"""Process frames for LLM completion requests.
Args:
frame: The frame to process.
direction: The direction of frame processing.
"""
await super().process_frame(frame, direction)
context = None
if isinstance(frame, LLMContextFrame):
context = frame.context
else:
await self.push_frame(frame, direction)
if context:
try:
await self.push_frame(LLMFullResponseStartFrame())
await self.start_processing_metrics()
await self._process_context(context)
except httpx.TimeoutException as e:
await self._call_event_handler("on_completion_timeout")
await self.push_error(error_msg="LLM completion timeout", exception=e)
except Exception as e:
await self.push_error(error_msg=f"Error during completion: {e}", exception=e)
finally:
await self.stop_processing_metrics()
await self.push_frame(LLMFullResponseEndFrame())
@traced_llm
async def _process_context(self, context: LLMContext):
adapter: OpenAIResponsesLLMAdapter = self.get_llm_adapter()
logger.debug(
f"{self}: Generating response from universal context "
f"{adapter.get_messages_for_logging(context)}"
)
invocation_params = adapter.get_llm_invocation_params(
context, system_instruction=self._settings.system_instruction
)
params = self._build_response_params(invocation_params)
await self.start_ttfb_metrics()
stream: AsyncStream[ResponseStreamEvent] = await self._client.responses.create(**params)
# Track function calls across stream events
function_calls: Dict[str, Dict[str, str]] = {} # item_id -> {name, call_id, arguments}
current_arguments: Dict[str, str] = {} # item_id -> accumulated arguments
# Ensure stream and its async iterator are closed on cancellation/exception
# to prevent socket leaks and uvloop crashes. Closing the iterator first
# cascades cleanup through nested async generators (httpx/httpcore internals),
# preventing uvloop's broken asyncgen finalizer from firing on Python 3.12+
# (MagicStack/uvloop#699).
@asynccontextmanager
async def _closing(stream):
chunk_iter = stream.__aiter__()
try:
yield chunk_iter
finally:
# Close the iterator first to cascade cleanup through
# nested async generators (httpx/httpcore internals).
if hasattr(chunk_iter, "aclose"):
await chunk_iter.aclose()
# Then close the stream to release HTTP resources.
if hasattr(stream, "close"):
await stream.close()
elif hasattr(stream, "aclose"):
await stream.aclose()
async with _closing(stream) as event_iter:
async for event in event_iter:
if isinstance(event, ResponseTextDeltaEvent):
await self.stop_ttfb_metrics()
await self._push_llm_text(event.delta)
elif isinstance(event, ResponseOutputItemAddedEvent):
await self.stop_ttfb_metrics()
item = event.item
if isinstance(item, ResponseFunctionToolCall):
item_id = item.id or ""
function_calls[item_id] = {
"name": item.name,
"call_id": item.call_id,
"arguments": "",
}
current_arguments[item_id] = ""
elif isinstance(event, ResponseFunctionCallArgumentsDeltaEvent):
item_id = event.item_id
if item_id in current_arguments:
current_arguments[item_id] += event.delta
elif isinstance(event, ResponseFunctionCallArgumentsDoneEvent):
item_id = event.item_id
if item_id in function_calls:
function_calls[item_id]["arguments"] = event.arguments
elif isinstance(event, ResponseOutputItemDoneEvent):
item = event.item
if isinstance(item, ResponseFunctionToolCall):
item_id = item.id or ""
if item_id in function_calls:
function_calls[item_id]["name"] = item.name
function_calls[item_id]["call_id"] = item.call_id
function_calls[item_id]["arguments"] = item.arguments
elif isinstance(event, ResponseCompletedEvent):
response = event.response
if response.usage:
tokens = LLMTokenUsage(
prompt_tokens=response.usage.input_tokens,
completion_tokens=response.usage.output_tokens,
total_tokens=response.usage.total_tokens,
cache_read_input_tokens=response.usage.input_tokens_details.cached_tokens,
reasoning_tokens=response.usage.output_tokens_details.reasoning_tokens,
)
await self.start_llm_usage_metrics(tokens)
# This field is used by @traced_llm for more detailed
# model name in tracing spans
self._full_model_name = response.model
# Process any function calls
if function_calls:
fc_list: List[FunctionCallFromLLM] = []
for item_id, fc in function_calls.items():
try:
arguments = json.loads(fc["arguments"]) if fc["arguments"] else {}
except json.JSONDecodeError:
logger.warning(
f"{self}: Failed to parse function call arguments: {fc['arguments']}"
)
arguments = {}
fc_list.append(
FunctionCallFromLLM(
context=context,
tool_call_id=fc["call_id"],
function_name=fc["name"],
arguments=arguments,
)
)
await self.run_function_calls(fc_list)
def _build_response_params(self, invocation_params: OpenAIResponsesLLMInvocationParams) -> dict:
"""Build parameters for the responses.create() call.
Args:
invocation_params: Parameters derived from the LLM context.
Returns:
Dictionary of parameters for the Responses API call.
"""
params: Dict[str, Any] = {
"model": self._settings.model,
"stream": True,
"store": False,
"input": invocation_params["input"],
}
# instructions (set by the adapter when input is non-empty)
if "instructions" in invocation_params:
params["instructions"] = invocation_params["instructions"]
# Optional parameters - only include if given
if isinstance(self._settings.temperature, (int, float)):
params["temperature"] = self._settings.temperature
if isinstance(self._settings.top_p, (int, float)):
params["top_p"] = self._settings.top_p
if isinstance(self._settings.max_completion_tokens, int):
params["max_output_tokens"] = self._settings.max_completion_tokens
if self._service_tier is not None:
params["service_tier"] = self._service_tier
# Tools
tools = invocation_params.get("tools")
if tools is not None and not isinstance(tools, type(NOT_GIVEN)):
params["tools"] = tools
# Extra settings
params.update(self._settings.extra)
return params
async def run_inference(
self,
context: LLMContext,
max_tokens: Optional[int] = None,
system_instruction: Optional[str] = None,
) -> Optional[str]:
"""Run a one-shot, out-of-band inference with the given LLM context.
Args:
context: The LLM context containing conversation history.
max_tokens: Optional maximum number of tokens to generate.
system_instruction: Optional system instruction for this inference.
Returns:
The LLM's response as a string, or None if no response is generated.
"""
adapter: OpenAIResponsesLLMAdapter = self.get_llm_adapter()
effective_instruction = system_instruction or self._settings.system_instruction
invocation_params = adapter.get_llm_invocation_params(
context, system_instruction=effective_instruction
)
params = self._build_response_params(invocation_params)
# Override for non-streaming
params["stream"] = False
if max_tokens is not None:
params["max_output_tokens"] = max_tokens
response = await self._client.responses.create(**params)
return response.output_text
__all__ = ["OpenAIResponsesLLMService", "OpenAIResponsesLLMSettings"]

View File

@@ -35,12 +35,11 @@ from pipecat.frames.frames import (
VADUserStoppedSpeakingFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven
from pipecat.services.stt_latency import OPENAI_REALTIME_TTFS_P99, OPENAI_TTFS_P99
from pipecat.services.stt_service import WebsocketSTTService
from pipecat.services.whisper.base_stt import (
BaseWhisperSTTService,
BaseWhisperSTTSettings,
Transcription,
)
from pipecat.transcriptions.language import Language
@@ -56,7 +55,7 @@ except ModuleNotFoundError:
@dataclass
class OpenAISTTSettings(BaseWhisperSTTSettings):
class OpenAISTTSettings(BaseWhisperSTTService.Settings):
"""Settings for the OpenAI STT service."""
pass
@@ -70,7 +69,7 @@ class OpenAISTTService(BaseWhisperSTTService):
"""
Settings = OpenAISTTSettings
_settings: OpenAISTTSettings
_settings: Settings
def __init__(
self,
@@ -81,7 +80,7 @@ class OpenAISTTService(BaseWhisperSTTService):
language: Optional[Language] = Language.EN,
prompt: Optional[str] = None,
temperature: Optional[float] = None,
settings: Optional[OpenAISTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = OPENAI_TTFS_P99,
**kwargs,
):
@@ -91,24 +90,24 @@ class OpenAISTTService(BaseWhisperSTTService):
model: Model to use — either gpt-4o or Whisper.
.. deprecated:: 0.0.105
Use ``settings=OpenAISTTSettings(model=...)`` instead.
Use ``settings=OpenAISTTService.Settings(model=...)`` instead.
api_key: OpenAI API key. Defaults to None.
base_url: API base URL. Defaults to None.
language: Language of the audio input. Defaults to English.
.. deprecated:: 0.0.105
Use ``settings=OpenAISTTSettings(language=...)`` instead.
Use ``settings=OpenAISTTService.Settings(language=...)`` instead.
prompt: Optional text to guide the model's style or continue a previous segment.
.. deprecated:: 0.0.105
Use ``settings=OpenAISTTSettings(prompt=...)`` instead.
Use ``settings=OpenAISTTService.Settings(prompt=...)`` instead.
temperature: Optional sampling temperature between 0 and 1. Defaults to 0.0.
.. deprecated:: 0.0.105
Use ``settings=OpenAISTTSettings(temperature=...)`` instead.
Use ``settings=OpenAISTTService.Settings(temperature=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -118,22 +117,22 @@ class OpenAISTTService(BaseWhisperSTTService):
"""
# --- 1. Hardcoded defaults ---
_language = language or Language.EN
default_settings = OpenAISTTSettings(
default_settings = self.Settings(
model="gpt-4o-transcribe",
language=self.language_to_service_language(_language),
language=_language,
prompt=None,
temperature=None,
)
# --- 2. Deprecated direct-arg overrides ---
if model is not None:
_warn_deprecated_param("model", OpenAISTTSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if prompt is not None:
_warn_deprecated_param("prompt", OpenAISTTSettings, "prompt")
self._warn_init_param_moved_to_settings("prompt", "prompt")
default_settings.prompt = prompt
if temperature is not None:
_warn_deprecated_param("temperature", OpenAISTTSettings, "temperature")
self._warn_init_param_moved_to_settings("temperature", "temperature")
default_settings.temperature = temperature
# --- 3. (no params object for this service) ---
@@ -187,9 +186,15 @@ class OpenAIRealtimeSTTSettings(STTSettings):
Parameters:
prompt: Optional prompt text to guide transcription style.
noise_reduction: Noise reduction mode. ``"near_field"`` for close
microphones, ``"far_field"`` for distant microphones, or ``None``
to disable.
"""
prompt: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
noise_reduction: Literal["near_field", "far_field"] | None | _NotGiven = field(
default_factory=lambda: NOT_GIVEN
)
class OpenAIRealtimeSTTService(WebsocketSTTService):
@@ -220,13 +225,15 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
stt = OpenAIRealtimeSTTService(
api_key="sk-...",
model="gpt-4o-transcribe",
noise_reduction="near_field",
settings=OpenAIRealtimeSTTService.Settings(
model="gpt-4o-transcribe",
noise_reduction="near_field",
),
)
"""
Settings = OpenAIRealtimeSTTSettings
_settings: OpenAIRealtimeSTTSettings
_settings: Settings
def __init__(
self,
@@ -239,7 +246,7 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
turn_detection: Optional[Union[dict, Literal[False]]] = False,
noise_reduction: Optional[Literal["near_field", "far_field"]] = None,
should_interrupt: bool = True,
settings: Optional[OpenAIRealtimeSTTSettings] = None,
settings: Optional[Settings] = None,
ttfs_p99_latency: Optional[float] = OPENAI_REALTIME_TTFS_P99,
**kwargs,
):
@@ -251,20 +258,20 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
``"gpt-4o-transcribe"`` and ``"gpt-4o-mini-transcribe"``.
.. deprecated:: 0.0.105
Use ``settings=OpenAIRealtimeSTTSettings(model=...)`` instead.
Use ``settings=OpenAIRealtimeSTTService.Settings(model=...)`` instead.
base_url: WebSocket base URL for the Realtime API.
Defaults to ``"wss://api.openai.com/v1/realtime"``.
language: Language of the audio input. Defaults to English.
.. deprecated:: 0.0.105
Use ``settings=OpenAIRealtimeSTTSettings(language=...)`` instead.
Use ``settings=OpenAIRealtimeSTTService.Settings(language=...)`` instead.
prompt: Optional prompt text to guide transcription style
or provide keyword hints.
.. deprecated:: 0.0.105
Use ``settings=OpenAIRealtimeSTTSettings(prompt=...)`` instead.
Use ``settings=OpenAIRealtimeSTTService.Settings(prompt=...)`` instead.
turn_detection: Server-side VAD configuration. Defaults to
``False`` (disabled), which relies on a local VAD
@@ -274,6 +281,9 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
noise_reduction: Noise reduction mode. ``"near_field"`` for
close microphones, ``"far_field"`` for distant
microphones, or ``None`` to disable.
.. deprecated:: 0.0.106
Use ``settings=OpenAIRealtimeSTTService.Settings(noise_reduction=...)`` instead.
should_interrupt: Whether to interrupt bot output when
speech is detected by server-side VAD. Only applies when
turn detection is enabled. Defaults to True.
@@ -291,22 +301,26 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
)
# --- 1. Hardcoded defaults ---
default_settings = OpenAIRealtimeSTTSettings(
default_settings = self.Settings(
model="gpt-4o-transcribe",
language=Language.EN,
prompt=None,
noise_reduction=None,
)
# --- 2. Deprecated direct-arg overrides ---
if model is not None:
_warn_deprecated_param("model", OpenAIRealtimeSTTSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if language is not None and language != Language.EN:
_warn_deprecated_param("language", OpenAIRealtimeSTTSettings, "language")
self._warn_init_param_moved_to_settings("language", "language")
default_settings.language = language
if prompt is not None:
_warn_deprecated_param("prompt", OpenAIRealtimeSTTSettings, "prompt")
self._warn_init_param_moved_to_settings("prompt", "prompt")
default_settings.prompt = prompt
if noise_reduction is not None:
self._warn_init_param_moved_to_settings("noise_reduction", "noise_reduction")
default_settings.noise_reduction = noise_reduction
# --- 3. (no params object for this service) ---
@@ -324,7 +338,6 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
self._base_url = base_url
self._turn_detection = turn_detection
self._noise_reduction = noise_reduction
self._should_interrupt = should_interrupt
self._receive_task = None
@@ -345,8 +358,8 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
Returns:
Two-letter ISO-639-1 language code.
"""
# Language.value is e.g. "en", "en-US", "fr", "zh".
return language.value.split("-")[0].lower()
# Language value is e.g. "en", "en-US", "fr", "zh".
return str(language).split("-")[0].lower()
def can_generate_metrics(self) -> bool:
"""Check if the service can generate processing metrics.
@@ -362,7 +375,7 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
Sends a ``session.update`` to the server when the session is active.
Args:
delta: A :class:`STTSettings` (or ``OpenAIRealtimeSTTSettings``) delta.
delta: A :class:`STTSettings` (or ``OpenAIRealtimeSTTService.Settings``) delta.
Returns:
Dict mapping changed field names to their previous values.
@@ -544,9 +557,9 @@ class OpenAIRealtimeSTTService(WebsocketSTTService):
input_audio["turn_detection"] = self._turn_detection
# Noise reduction
if self._noise_reduction:
if self._settings.noise_reduction:
input_audio["noise_reduction"] = {
"type": self._noise_reduction,
"type": self._settings.noise_reduction,
}
await self._ws_send(

View File

@@ -23,7 +23,7 @@ from pipecat.frames.frames import (
StartFrame,
TTSAudioRawFrame,
)
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, TTSSettings, _NotGiven
from pipecat.services.tts_service import TTSService
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -82,7 +82,7 @@ class OpenAITTSService(TTSService):
"""
Settings = OpenAITTSSettings
_settings: OpenAITTSSettings
_settings: Settings
OPENAI_SAMPLE_RATE = 24000 # OpenAI TTS always outputs at 24kHz
@@ -90,7 +90,7 @@ class OpenAITTSService(TTSService):
"""Input parameters for OpenAI TTS configuration.
.. deprecated:: 0.0.105
Use ``settings=OpenAITTSSettings(...)`` instead.
Use ``settings=OpenAITTSService.Settings(...)`` instead.
Parameters:
instructions: Instructions to guide voice synthesis behavior.
@@ -111,7 +111,7 @@ class OpenAITTSService(TTSService):
instructions: Optional[str] = None,
speed: Optional[float] = None,
params: Optional[InputParams] = None,
settings: Optional[OpenAITTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize OpenAI TTS service.
@@ -122,28 +122,28 @@ class OpenAITTSService(TTSService):
voice: Voice ID to use for synthesis. Defaults to "alloy".
.. deprecated:: 0.0.105
Use ``settings=OpenAITTSSettings(voice=...)`` instead.
Use ``settings=OpenAITTSService.Settings(voice=...)`` instead.
model: TTS model to use. Defaults to "gpt-4o-mini-tts".
.. deprecated:: 0.0.105
Use ``settings=OpenAITTSSettings(model=...)`` instead.
Use ``settings=OpenAITTSService.Settings(model=...)`` instead.
sample_rate: Output audio sample rate in Hz. If None, uses OpenAI's default 24kHz.
instructions: Optional instructions to guide voice synthesis behavior.
.. deprecated:: 0.0.105
Use ``settings=OpenAITTSSettings(instructions=...)`` instead.
Use ``settings=OpenAITTSService.Settings(instructions=...)`` instead.
speed: Voice speed control (0.25 to 4.0, default 1.0).
.. deprecated:: 0.0.105
Use ``settings=OpenAITTSSettings(speed=...)`` instead.
Use ``settings=OpenAITTSService.Settings(speed=...)`` instead.
params: Optional synthesis controls (acting instructions, speed, ...).
.. deprecated:: 0.0.105
Use ``settings=OpenAITTSSettings(...)`` instead.
Use ``settings=OpenAITTSService.Settings(...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
@@ -156,7 +156,7 @@ class OpenAITTSService(TTSService):
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAITTSSettings(
default_settings = self.Settings(
model="gpt-4o-mini-tts",
voice="alloy",
language=None,
@@ -166,21 +166,21 @@ class OpenAITTSService(TTSService):
# 2. Apply direct init arg overrides (deprecated)
if voice is not None:
_warn_deprecated_param("voice", OpenAITTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice", "voice")
default_settings.voice = voice
if model is not None:
_warn_deprecated_param("model", OpenAITTSSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
if instructions is not None:
_warn_deprecated_param("instructions", OpenAITTSSettings, "instructions")
self._warn_init_param_moved_to_settings("instructions", "instructions")
default_settings.instructions = instructions
if speed is not None:
_warn_deprecated_param("speed", OpenAITTSSettings, "speed")
self._warn_init_param_moved_to_settings("speed", "speed")
default_settings.speed = speed
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", OpenAITTSSettings)
self._warn_init_param_moved_to_settings("params")
if not settings:
if params.instructions is not None:
default_settings.instructions = params.instructions

View File

@@ -11,7 +11,7 @@ from dataclasses import dataclass
from loguru import logger
from .openai import OpenAIRealtimeBetaLLMService, OpenAIRealtimeBetaLLMSettings
from .openai import OpenAIRealtimeBetaLLMService
try:
from websockets.asyncio.client import connect as websocket_connect
@@ -24,7 +24,7 @@ except ModuleNotFoundError as e:
@dataclass
class AzureRealtimeBetaLLMSettings(OpenAIRealtimeBetaLLMSettings):
class AzureRealtimeBetaLLMSettings(OpenAIRealtimeBetaLLMService.Settings):
"""Settings for AzureRealtimeBetaLLMService."""
pass
@@ -43,7 +43,7 @@ class AzureRealtimeBetaLLMService(OpenAIRealtimeBetaLLMService):
"""
Settings = AzureRealtimeBetaLLMSettings
_settings: AzureRealtimeBetaLLMSettings
_settings: Settings
def __init__(
self,

View File

@@ -54,7 +54,7 @@ from pipecat.processors.aggregators.openai_llm_context import (
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.llm_service import FunctionCallFromLLM, LLMService
from pipecat.services.openai.llm import OpenAIContextAggregatorPair
from pipecat.services.settings import LLMSettings, _warn_deprecated_param
from pipecat.services.settings import LLMSettings
from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601
from pipecat.utils.tracing.service_decorators import traced_openai_realtime, traced_stt
@@ -112,7 +112,7 @@ class OpenAIRealtimeBetaLLMService(LLMService):
"""
Settings = OpenAIRealtimeBetaLLMSettings
_settings: OpenAIRealtimeBetaLLMSettings
_settings: Settings
# Overriding the default adapter to use the OpenAIRealtimeLLMAdapter one.
adapter_class = OpenAIRealtimeLLMAdapter
@@ -124,7 +124,7 @@ class OpenAIRealtimeBetaLLMService(LLMService):
model: Optional[str] = None,
base_url: str = "wss://api.openai.com/v1/realtime",
session_properties: Optional[events.SessionProperties] = None,
settings: Optional[OpenAIRealtimeBetaLLMSettings] = None,
settings: Optional[Settings] = None,
start_audio_paused: bool = False,
send_transcription_frames: bool = True,
**kwargs,
@@ -136,7 +136,7 @@ class OpenAIRealtimeBetaLLMService(LLMService):
model: OpenAI model name.
.. deprecated:: 0.0.105
Use ``settings=OpenAIRealtimeBetaLLMSettings(model=...)`` instead.
Use ``settings=OpenAIRealtimeBetaLLMService.Settings(model=...)`` instead.
base_url: WebSocket base URL for the realtime API.
Defaults to "wss://api.openai.com/v1/realtime".
@@ -157,7 +157,7 @@ class OpenAIRealtimeBetaLLMService(LLMService):
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAIRealtimeBetaLLMSettings(
default_settings = self.Settings(
model="gpt-4o-realtime-preview-2025-06-03",
system_instruction=None,
temperature=None,
@@ -173,7 +173,7 @@ class OpenAIRealtimeBetaLLMService(LLMService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAIRealtimeBetaLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. Apply settings delta (canonical API, always wins)
if settings is not None:
@@ -441,7 +441,7 @@ class OpenAIRealtimeBetaLLMService(LLMService):
item = events.ConversationItem(
type="function_call_output",
call_id=frame.tool_call_id,
output=json.dumps(frame.result),
output=json.dumps(frame.result, ensure_ascii=False),
)
await self.send_client_event(events.ConversationItemCreateEvent(item=item))
@@ -556,7 +556,10 @@ class OpenAIRealtimeBetaLLMService(LLMService):
await self._handle_evt_audio_transcript_delta(evt)
elif evt.type == "error":
if not await self._maybe_handle_evt_retrieve_conversation_item_error(evt):
if evt.error.code == "response_cancel_not_active":
if evt.error.code in (
"response_cancel_not_active",
"conversation_already_has_active_response",
):
logger.debug(f"{self} {evt.error.message}")
else:
await self._handle_evt_error(evt)

View File

@@ -16,9 +16,8 @@ from typing import Dict, Optional
from loguru import logger
from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
try:
from openpipe import AsyncOpenAI as OpenPipeAI
@@ -29,7 +28,7 @@ except ModuleNotFoundError as e:
@dataclass
class OpenPipeLLMSettings(OpenAILLMSettings):
class OpenPipeLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for OpenPipeLLMService."""
pass
@@ -44,7 +43,7 @@ class OpenPipeLLMService(OpenAILLMService):
"""
Settings = OpenPipeLLMSettings
_settings: OpenPipeLLMSettings
_settings: Settings
def __init__(
self,
@@ -55,7 +54,7 @@ class OpenPipeLLMService(OpenAILLMService):
openpipe_api_key: Optional[str] = None,
openpipe_base_url: str = "https://app.openpipe.ai/api/v1",
tags: Optional[Dict[str, str]] = None,
settings: Optional[OpenPipeLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize OpenPipe LLM service.
@@ -64,7 +63,7 @@ class OpenPipeLLMService(OpenAILLMService):
model: The model name to use. Defaults to "gpt-4.1".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=OpenPipeLLMService.Settings(model=...)`` instead.
api_key: OpenAI API key for authentication. If None, reads from environment.
base_url: Custom OpenAI API endpoint URL. Uses default if None.
@@ -76,11 +75,11 @@ class OpenPipeLLMService(OpenAILLMService):
**kwargs: Additional arguments passed to parent OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenPipeLLMSettings(model="gpt-4.1")
default_settings = self.Settings(model="gpt-4.1")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenPipeLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)

View File

@@ -15,13 +15,12 @@ from typing import Any, Dict, Optional
from loguru import logger
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class OpenRouterLLMSettings(OpenAILLMSettings):
class OpenRouterLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for OpenRouterLLMService."""
pass
@@ -35,7 +34,7 @@ class OpenRouterLLMService(OpenAILLMService):
"""
Settings = OpenRouterLLMSettings
_settings: OpenRouterLLMSettings
_settings: Settings
def __init__(
self,
@@ -43,7 +42,7 @@ class OpenRouterLLMService(OpenAILLMService):
api_key: Optional[str] = None,
model: Optional[str] = None,
base_url: str = "https://openrouter.ai/api/v1",
settings: Optional[OpenRouterLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the OpenRouter LLM service.
@@ -54,7 +53,7 @@ class OpenRouterLLMService(OpenAILLMService):
model: The model identifier to use. Defaults to "openai/gpt-4o-2024-11-20".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=OpenRouterLLMService.Settings(model=...)`` instead.
base_url: The base URL for OpenRouter API. Defaults to "https://openrouter.ai/api/v1".
settings: Runtime-updatable settings. When provided alongside deprecated
@@ -62,11 +61,11 @@ class OpenRouterLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenRouterLLMSettings(model="openai/gpt-4o-2024-11-20")
default_settings = self.Settings(model="openai/gpt-4o-2024-11-20")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenRouterLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)

View File

@@ -14,17 +14,19 @@ reporting patterns while maintaining compatibility with the Pipecat framework.
from dataclasses import dataclass
from typing import Optional
from loguru import logger
from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams
from pipecat.adapters.services.perplexity_adapter import PerplexityLLMAdapter
from pipecat.metrics.metrics import LLMTokenUsage
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
from pipecat.services.openai.base_llm import OpenAILLMSettings
from pipecat.services.openai.base_llm import BaseOpenAILLMService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class PerplexityLLMSettings(OpenAILLMSettings):
class PerplexityLLMSettings(BaseOpenAILLMService.Settings):
"""Settings for PerplexityLLMService."""
pass
@@ -38,8 +40,10 @@ class PerplexityLLMService(OpenAILLMService):
in token usage reporting between Perplexity (incremental) and OpenAI (final summary).
"""
adapter_class = PerplexityLLMAdapter
Settings = PerplexityLLMSettings
_settings: PerplexityLLMSettings
_settings: Settings
def __init__(
self,
@@ -47,7 +51,7 @@ class PerplexityLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.perplexity.ai",
model: Optional[str] = None,
settings: Optional[PerplexityLLMSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Perplexity LLM service.
@@ -58,18 +62,18 @@ class PerplexityLLMService(OpenAILLMService):
model: The model identifier to use. Defaults to "sonar".
.. deprecated:: 0.0.105
Use ``settings=OpenAILLMSettings(model=...)`` instead.
Use ``settings=PerplexityLLMService.Settings(model=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = PerplexityLLMSettings(model="sonar")
default_settings = self.Settings(model="sonar")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", PerplexityLLMSettings, "model")
self._warn_init_param_moved_to_settings("model", "model")
default_settings.model = model
# 3. (No step 3, as there's no params object to apply)
@@ -120,6 +124,10 @@ class PerplexityLLMService(OpenAILLMService):
# Prepend system instruction if set
if self._settings.system_instruction:
messages = params.get("messages", [])
if messages and messages[0].get("role") == "system":
logger.warning(
f"{self}: Both system_instruction and an initial system message in context are set. This may be unintended."
)
params["messages"] = [
{"role": "system", "content": self._settings.system_instruction}
] + messages

View File

@@ -19,7 +19,7 @@ from pipecat.frames.frames import (
Frame,
TTSStoppedFrame,
)
from pipecat.services.settings import TTSSettings, _warn_deprecated_param
from pipecat.services.settings import TTSSettings
from pipecat.services.tts_service import TTSService
from pipecat.utils.tracing.service_decorators import traced_tts
@@ -48,7 +48,7 @@ class PiperTTSService(TTSService):
"""
Settings = PiperTTSSettings
_settings: PiperTTSSettings
_settings: Settings
def __init__(
self,
@@ -57,7 +57,7 @@ class PiperTTSService(TTSService):
download_dir: Optional[Path] = None,
force_redownload: bool = False,
use_cuda: bool = False,
settings: Optional[PiperTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Piper TTS service.
@@ -66,7 +66,7 @@ class PiperTTSService(TTSService):
voice_id: Piper voice model identifier (e.g. `en_US-ryan-high`).
.. deprecated:: 0.0.105
Use ``settings=PiperTTSSettings(voice=...)`` instead.
Use ``settings=PiperTTSService.Settings(voice=...)`` instead.
download_dir: Directory for storing voice model files. Defaults to
the current working directory.
@@ -77,11 +77,11 @@ class PiperTTSService(TTSService):
**kwargs: Additional arguments passed to the parent `TTSService`.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = PiperTTSSettings(model=None, voice=None, language=None)
default_settings = self.Settings(model=None, voice=None, language=None)
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", PiperTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. (No step 3, as there's no params object to apply)
@@ -121,7 +121,7 @@ class PiperTTSService(TTSService):
"""
return True
async def _update_settings(self, delta: PiperTTSSettings) -> dict[str, Any]:
async def _update_settings(self, delta: Settings) -> dict[str, Any]:
"""Apply a settings delta.
Settings are stored but not applied to the active connection.
@@ -202,7 +202,7 @@ class PiperHttpTTSService(TTSService):
"""
Settings = PiperHttpTTSSettings
_settings: PiperHttpTTSSettings
_settings: Settings
def __init__(
self,
@@ -210,7 +210,7 @@ class PiperHttpTTSService(TTSService):
base_url: str,
aiohttp_session: aiohttp.ClientSession,
voice_id: Optional[str] = None,
settings: Optional[PiperHttpTTSSettings] = None,
settings: Optional[Settings] = None,
**kwargs,
):
"""Initialize the Piper TTS service.
@@ -221,18 +221,18 @@ class PiperHttpTTSService(TTSService):
voice_id: Piper voice model identifier (e.g. `en_US-ryan-high`).
.. deprecated:: 0.0.105
Use ``settings=PiperHttpTTSSettings(voice=...)`` instead.
Use ``settings=PiperHttpTTSService.Settings(voice=...)`` instead.
settings: Runtime-updatable settings. When provided alongside deprecated
parameters, ``settings`` values take precedence.
**kwargs: Additional arguments passed to the parent TTSService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = PiperHttpTTSSettings(model=None, voice=None, language=None)
default_settings = self.Settings(model=None, voice=None, language=None)
# 2. Apply direct init arg overrides (deprecated)
if voice_id is not None:
_warn_deprecated_param("voice_id", PiperHttpTTSSettings, "voice")
self._warn_init_param_moved_to_settings("voice_id", "voice")
default_settings.voice = voice_id
# 3. (No step 3, as there's no params object to apply)

Some files were not shown because too many files have changed in this diff Show More