Add Settings subclasses to all services and auto-discovered init tests

- Add dedicated Settings subclasses to 20 LLM services that were
  borrowing parent Settings classes (e.g. AzureLLMSettings,
  GroqLLMSettings) so users don't need cross-module imports
- Fix field defaults to NOT_GIVEN in BaseWhisperSTTSettings,
  OpenAIRealtimeSTTSettings, and NvidiaSegmentedSTTSettings for
  delta-mode safety
- Fix incomplete default_settings in AWS, Cartesia, ElevenLabs,
  Fish, and Whisper services so validate_complete() passes
- Add auto-discovered tests that verify all Settings classes default
  to NOT_GIVEN (delta safety) and all services initialize with
  complete settings (store completeness)
This commit is contained in:
Mark Backman
2026-03-04 18:04:59 -05:00
parent 034e81ff18
commit a4375274b2
28 changed files with 436 additions and 72 deletions

View File

@@ -98,6 +98,7 @@ class AWSTranscribeSTTService(WebsocketSTTService):
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = AWSTranscribeSTTSettings(
model=None,
language=self.language_to_service_language(Language.EN) or "en-US",
)

View File

@@ -6,6 +6,7 @@
"""Azure OpenAI service implementation for the Pipecat AI framework."""
from dataclasses import dataclass
from typing import Optional
from loguru import logger
@@ -16,6 +17,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class AzureLLMSettings(OpenAILLMSettings):
"""Settings for Azure OpenAI LLM service."""
pass
class AzureLLMService(OpenAILLMService):
"""A service for interacting with Azure OpenAI using the OpenAI-compatible interface.
@@ -30,7 +38,7 @@ class AzureLLMService(OpenAILLMService):
endpoint: str,
model: Optional[str] = None,
api_version: str = "2024-09-01-preview",
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[AzureLLMSettings] = None,
**kwargs,
):
"""Initialize the Azure LLM service.
@@ -49,11 +57,11 @@ class AzureLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="gpt-4o")
default_settings = AzureLLMSettings(model="gpt-4o")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", AzureLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -6,9 +6,11 @@
"""Azure OpenAI Realtime LLM service implementation."""
from dataclasses import dataclass
from loguru import logger
from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMService
from pipecat.services.openai.realtime.llm import OpenAIRealtimeLLMService, OpenAIRealtimeLLMSettings
try:
from websockets.asyncio.client import connect as websocket_connect
@@ -18,6 +20,13 @@ except ModuleNotFoundError as e:
raise Exception(f"Missing module: {e}")
@dataclass
class AzureRealtimeLLMSettings(OpenAIRealtimeLLMSettings):
"""Settings for Azure Realtime LLM service."""
pass
class AzureRealtimeLLMService(OpenAIRealtimeLLMService):
"""Azure OpenAI Realtime LLM service with Azure-specific authentication.

View File

@@ -301,6 +301,7 @@ class CartesiaTTSService(AudioContextTTSService):
# 1. Initialize default_settings with hardcoded defaults
default_settings = CartesiaTTSSettings(
model="sonic-3",
voice=None,
language=language_to_cartesia_language(Language.EN),
generation_config=None,
pronunciation_dict_id=None,
@@ -745,6 +746,7 @@ class CartesiaHttpTTSService(TTSService):
# 1. Initialize default_settings with hardcoded defaults
default_settings = CartesiaTTSSettings(
model="sonic-3",
voice=None,
language=language_to_cartesia_language(Language.EN),
generation_config=None,
pronunciation_dict_id=None,

View File

@@ -6,6 +6,7 @@
"""Cerebras LLM service implementation using OpenAI-compatible interface."""
from dataclasses import dataclass
from typing import Optional
from loguru import logger
@@ -16,6 +17,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class CerebrasLLMSettings(OpenAILLMSettings):
"""Settings for Cerebras LLM service."""
pass
class CerebrasLLMService(OpenAILLMService):
"""A service for interacting with Cerebras's API using the OpenAI-compatible interface.
@@ -29,7 +37,7 @@ class CerebrasLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.cerebras.ai/v1",
model: Optional[str] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[CerebrasLLMSettings] = None,
**kwargs,
):
"""Initialize the Cerebras LLM service.
@@ -47,11 +55,11 @@ class CerebrasLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="gpt-oss-120b")
default_settings = CerebrasLLMSettings(model="gpt-oss-120b")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", CerebrasLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -6,6 +6,7 @@
"""DeepSeek LLM service implementation using OpenAI-compatible interface."""
from dataclasses import dataclass
from typing import Optional
from loguru import logger
@@ -16,6 +17,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class DeepSeekLLMSettings(OpenAILLMSettings):
"""Settings for DeepSeek LLM service."""
pass
class DeepSeekLLMService(OpenAILLMService):
"""A service for interacting with DeepSeek's API using the OpenAI-compatible interface.
@@ -29,7 +37,7 @@ class DeepSeekLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.deepseek.com/v1",
model: Optional[str] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[DeepSeekLLMSettings] = None,
**kwargs,
):
"""Initialize the DeepSeek LLM service.
@@ -47,11 +55,11 @@ class DeepSeekLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="deepseek-chat")
default_settings = DeepSeekLLMSettings(model="deepseek-chat")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", DeepSeekLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -418,6 +418,14 @@ class ElevenLabsTTSService(AudioContextTTSService):
# 1. Initialize default_settings with hardcoded defaults
default_settings = ElevenLabsTTSSettings(
model="eleven_turbo_v2_5",
voice=None,
language=None,
stability=None,
similarity_boost=None,
style=None,
use_speaker_boost=None,
speed=None,
apply_text_normalization=None,
)
# Track init-only URL params through the override chain
@@ -986,6 +994,15 @@ class ElevenLabsHttpTTSService(TTSService):
# 1. Initialize default_settings with hardcoded defaults
default_settings = ElevenLabsHttpTTSSettings(
model="eleven_turbo_v2_5",
voice=None,
language=None,
optimize_streaming_latency=None,
stability=None,
similarity_boost=None,
style=None,
use_speaker_boost=None,
speed=None,
apply_text_normalization=None,
)
# 2. Apply direct init arg overrides (deprecated)

View File

@@ -6,6 +6,7 @@
"""Fireworks AI service implementation using OpenAI-compatible interface."""
from dataclasses import dataclass
from typing import Optional
from loguru import logger
@@ -16,6 +17,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class FireworksLLMSettings(OpenAILLMSettings):
"""Settings for Fireworks LLM service."""
pass
class FireworksLLMService(OpenAILLMService):
"""A service for interacting with Fireworks AI using the OpenAI-compatible interface.
@@ -29,7 +37,7 @@ class FireworksLLMService(OpenAILLMService):
api_key: str,
model: Optional[str] = None,
base_url: str = "https://api.fireworks.ai/inference/v1",
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[FireworksLLMSettings] = None,
**kwargs,
):
"""Initialize the Fireworks LLM service.
@@ -47,11 +55,11 @@ class FireworksLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="accounts/fireworks/models/firefunction-v2")
default_settings = FireworksLLMSettings(model="accounts/fireworks/models/firefunction-v2")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", FireworksLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -173,6 +173,7 @@ class FishAudioTTSService(InterruptibleTTSService):
default_settings = FishAudioTTSSettings(
model="s1",
voice=None,
language=None,
latency="normal",
normalize=True,
prosody_speed=1.0,

View File

@@ -12,6 +12,7 @@ streaming responses, and tool usage.
"""
import json
from dataclasses import dataclass
from typing import List, Optional, Union
from loguru import logger
@@ -41,6 +42,13 @@ except ModuleNotFoundError as e:
raise Exception(f"Missing module: {e}")
@dataclass
class GeminiLiveVertexLLMSettings(GeminiLiveLLMSettings):
"""Settings for Gemini Live Vertex LLM service."""
pass
class GeminiLiveVertexLLMService(GeminiLiveLLMService):
"""Provides access to Google's Gemini Live model via Vertex AI.
@@ -63,7 +71,7 @@ class GeminiLiveVertexLLMService(GeminiLiveLLMService):
system_instruction: Optional[str] = None,
tools: Optional[Union[List[dict], ToolsSchema]] = None,
params: Optional[InputParams] = None,
settings: Optional[GeminiLiveLLMSettings] = 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,
@@ -122,7 +130,7 @@ class GeminiLiveVertexLLMService(GeminiLiveLLMService):
# double deprecation warnings from the parent.
# 1. Initialize default_settings with hardcoded defaults
default_settings = GeminiLiveLLMSettings(
default_settings = GeminiLiveVertexLLMSettings(
model="google/gemini-live-2.5-flash-native-audio",
frequency_penalty=None,
max_tokens=4096,
@@ -146,12 +154,12 @@ class GeminiLiveVertexLLMService(GeminiLiveLLMService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GeminiLiveLLMSettings, "model")
_warn_deprecated_param("model", GeminiLiveVertexLLMSettings, "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GeminiLiveLLMSettings)
_warn_deprecated_param("params", GeminiLiveVertexLLMSettings)
if not settings:
default_settings.frequency_penalty = params.frequency_penalty
default_settings.max_tokens = params.max_tokens

View File

@@ -12,6 +12,7 @@ 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
@@ -32,6 +33,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class GoogleOpenAILLMSettings(OpenAILLMSettings):
"""Settings for Google OpenAI-compatible LLM service."""
pass
class GoogleLLMOpenAIBetaService(OpenAILLMService):
"""Google LLM service using OpenAI-compatible API format.
@@ -56,7 +64,7 @@ class GoogleLLMOpenAIBetaService(OpenAILLMService):
api_key: str,
base_url: str = "https://generativelanguage.googleapis.com/v1beta/openai/",
model: Optional[str] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[GoogleOpenAILLMSettings] = None,
**kwargs,
):
"""Initialize the Google LLM service.
@@ -85,11 +93,11 @@ class GoogleLLMOpenAIBetaService(OpenAILLMService):
)
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="gemini-2.0-flash")
default_settings = GoogleOpenAILLMSettings(model="gemini-2.0-flash")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", GoogleOpenAILLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -12,6 +12,7 @@ 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"
@@ -39,6 +40,13 @@ except ModuleNotFoundError as e:
raise Exception(f"Missing module: {e}")
@dataclass
class GoogleVertexLLMSettings(GoogleLLMSettings):
"""Settings for Google Vertex LLM service."""
pass
class GoogleVertexLLMService(GoogleLLMService):
"""Google Vertex AI LLM service extending GoogleLLMService.
@@ -104,7 +112,7 @@ class GoogleVertexLLMService(GoogleLLMService):
location: Optional[str] = None,
project_id: Optional[str] = None,
params: Optional[GoogleLLMService.InputParams] = None,
settings: Optional[GoogleLLMSettings] = None,
settings: Optional[GoogleVertexLLMSettings] = None,
system_instruction: Optional[str] = None,
tools: Optional[list] = None,
tool_config: Optional[dict] = None,
@@ -183,7 +191,7 @@ class GoogleVertexLLMService(GoogleLLMService):
self._location = location
# 1. Initialize default_settings with hardcoded defaults
default_settings = GoogleLLMSettings(
default_settings = GoogleVertexLLMSettings(
model="gemini-2.5-flash",
max_tokens=4096,
temperature=None,
@@ -200,12 +208,12 @@ class GoogleVertexLLMService(GoogleLLMService):
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", GoogleLLMSettings, "model")
_warn_deprecated_param("model", GoogleVertexLLMSettings, "model")
default_settings.model = model
# 3. Apply params overrides — only if settings not provided
if params is not None:
_warn_deprecated_param("params", GoogleLLMSettings)
_warn_deprecated_param("params", GoogleVertexLLMSettings)
if not settings:
default_settings.max_tokens = params.max_tokens
default_settings.temperature = params.temperature

View File

@@ -70,6 +70,13 @@ class GrokContextAggregatorPair:
return self._assistant
@dataclass
class GrokLLMSettings(OpenAILLMSettings):
"""Settings for Grok LLM service."""
pass
class GrokLLMService(OpenAILLMService):
"""A service for interacting with Grok's API using the OpenAI-compatible interface.
@@ -85,7 +92,7 @@ class GrokLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.x.ai/v1",
model: Optional[str] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[GrokLLMSettings] = None,
**kwargs,
):
"""Initialize the GrokLLMService with API key and model.
@@ -103,11 +110,11 @@ class GrokLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="grok-3-beta")
default_settings = GrokLLMSettings(model="grok-3-beta")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", GrokLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -6,6 +6,7 @@
"""Groq LLM Service implementation using OpenAI-compatible interface."""
from dataclasses import dataclass
from typing import Optional
from loguru import logger
@@ -15,6 +16,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class GroqLLMSettings(OpenAILLMSettings):
"""Settings for Groq LLM service."""
pass
class GroqLLMService(OpenAILLMService):
"""A service for interacting with Groq's API using the OpenAI-compatible interface.
@@ -28,7 +36,7 @@ class GroqLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.groq.com/openai/v1",
model: Optional[str] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[GroqLLMSettings] = None,
**kwargs,
):
"""Initialize Groq LLM service.
@@ -46,11 +54,11 @@ class GroqLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="llama-3.3-70b-versatile")
default_settings = GroqLLMSettings(model="llama-3.3-70b-versatile")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", GroqLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -6,6 +6,7 @@
"""Mistral LLM service implementation using OpenAI-compatible interface."""
from dataclasses import dataclass
from typing import List, Optional, Sequence
from loguru import logger
@@ -18,6 +19,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class MistralLLMSettings(OpenAILLMSettings):
"""Settings for Mistral LLM service."""
pass
class MistralLLMService(OpenAILLMService):
"""A service for interacting with Mistral's API using the OpenAI-compatible interface.
@@ -31,7 +39,7 @@ class MistralLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.mistral.ai/v1",
model: Optional[str] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[MistralLLMSettings] = None,
**kwargs,
):
"""Initialize the Mistral LLM service.
@@ -49,11 +57,11 @@ class MistralLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="mistral-small-latest")
default_settings = MistralLLMSettings(model="mistral-small-latest")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", MistralLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -10,6 +10,7 @@ This module provides a service for interacting with NVIDIA's NIM (NVIDIA Inferen
Microservice) API while maintaining compatibility with the OpenAI-style interface.
"""
from dataclasses import dataclass
from typing import Optional
from pipecat.metrics.metrics import LLMTokenUsage
@@ -20,6 +21,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class NvidiaLLMSettings(OpenAILLMSettings):
"""Settings for NVIDIA LLM service."""
pass
class NvidiaLLMService(OpenAILLMService):
"""A service for interacting with NVIDIA's NIM (NVIDIA Inference Microservice) API.
@@ -34,7 +42,7 @@ class NvidiaLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://integrate.api.nvidia.com/v1",
model: Optional[str] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[NvidiaLLMSettings] = None,
**kwargs,
):
"""Initialize the NvidiaLLMService.
@@ -53,11 +61,11 @@ class NvidiaLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="nvidia/llama-3.1-nemotron-70b-instruct")
default_settings = NvidiaLLMSettings(model="nvidia/llama-3.1-nemotron-70b-instruct")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", NvidiaLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -8,7 +8,7 @@
import asyncio
from concurrent.futures import CancelledError as FuturesCancelledError
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any, AsyncGenerator, List, Mapping, Optional
from loguru import logger
@@ -23,7 +23,7 @@ from pipecat.frames.frames import (
StartFrame,
TranscriptionFrame,
)
from pipecat.services.settings import STTSettings, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
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
@@ -110,11 +110,11 @@ class NvidiaSegmentedSTTSettings(STTSettings):
boosted_lm_score: Score boost for specified words.
"""
profanity_filter: bool = False
automatic_punctuation: bool = True
verbatim_transcripts: bool = False
boosted_lm_words: Optional[List[str]] = None
boosted_lm_score: float = 4.0
profanity_filter: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
automatic_punctuation: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
verbatim_transcripts: bool | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
boosted_lm_words: List[str] | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
boosted_lm_score: float | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
class NvidiaSTTService(STTService):

View File

@@ -6,6 +6,7 @@
"""OLLama LLM service implementation for Pipecat AI framework."""
from dataclasses import dataclass
from typing import Optional
from loguru import logger
@@ -15,6 +16,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class OllamaLLMSettings(OpenAILLMSettings):
"""Settings for Ollama LLM service."""
pass
class OLLamaLLMService(OpenAILLMService):
"""OLLama LLM service that provides local language model capabilities.
@@ -27,7 +35,7 @@ class OLLamaLLMService(OpenAILLMService):
*,
model: Optional[str] = None,
base_url: str = "http://localhost:11434/v1",
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[OllamaLLMSettings] = None,
**kwargs,
):
"""Initialize OLLama LLM service.
@@ -45,11 +53,11 @@ class OLLamaLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="llama2")
default_settings = OllamaLLMSettings(model="llama2")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", OllamaLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -16,7 +16,7 @@ Provides two STT services:
import base64
import json
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any, AsyncGenerator, Literal, Optional, Union
from loguru import logger
@@ -35,7 +35,7 @@ from pipecat.frames.frames import (
VADUserStoppedSpeakingFrame,
)
from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.settings import STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
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 (
@@ -188,7 +188,7 @@ class OpenAIRealtimeSTTSettings(STTSettings):
prompt: Optional prompt text to guide transcription style.
"""
prompt: str | None | _NotGiven = None
prompt: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
class OpenAIRealtimeSTTService(WebsocketSTTService):

View File

@@ -7,10 +7,11 @@
"""Azure OpenAI Realtime Beta LLM service implementation."""
import warnings
from dataclasses import dataclass
from loguru import logger
from .openai import OpenAIRealtimeBetaLLMService
from .openai import OpenAIRealtimeBetaLLMService, OpenAIRealtimeBetaLLMSettings
try:
from websockets.asyncio.client import connect as websocket_connect
@@ -22,6 +23,13 @@ except ModuleNotFoundError as e:
raise Exception(f"Missing module: {e}")
@dataclass
class AzureRealtimeBetaLLMSettings(OpenAIRealtimeBetaLLMSettings):
"""Settings for Azure Realtime Beta LLM service."""
pass
class AzureRealtimeBetaLLMService(OpenAIRealtimeBetaLLMService):
"""Azure OpenAI Realtime Beta LLM service with Azure-specific authentication.

View File

@@ -10,6 +10,7 @@ This module provides an OpenPipe-specific implementation of the OpenAI LLM servi
enabling integration with OpenPipe's fine-tuning and monitoring capabilities.
"""
from dataclasses import dataclass
from typing import Dict, Optional
from loguru import logger
@@ -27,6 +28,13 @@ except ModuleNotFoundError as e:
raise Exception(f"Missing module: {e}")
@dataclass
class OpenPipeLLMSettings(OpenAILLMSettings):
"""Settings for OpenPipe LLM service."""
pass
class OpenPipeLLMService(OpenAILLMService):
"""OpenPipe-powered Large Language Model service.
@@ -44,7 +52,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[OpenAILLMSettings] = None,
settings: Optional[OpenPipeLLMSettings] = None,
**kwargs,
):
"""Initialize OpenPipe LLM service.
@@ -65,11 +73,11 @@ class OpenPipeLLMService(OpenAILLMService):
**kwargs: Additional arguments passed to parent OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="gpt-4.1")
default_settings = OpenPipeLLMSettings(model="gpt-4.1")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", OpenPipeLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -10,6 +10,7 @@ This module provides an OpenAI-compatible interface for interacting with OpenRou
extending the base OpenAI LLM service functionality.
"""
from dataclasses import dataclass
from typing import Any, Dict, Optional
from loguru import logger
@@ -19,6 +20,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class OpenRouterLLMSettings(OpenAILLMSettings):
"""Settings for OpenRouter LLM service."""
pass
class OpenRouterLLMService(OpenAILLMService):
"""A service for interacting with OpenRouter's API using the OpenAI-compatible interface.
@@ -32,7 +40,7 @@ class OpenRouterLLMService(OpenAILLMService):
api_key: Optional[str] = None,
model: Optional[str] = None,
base_url: str = "https://openrouter.ai/api/v1",
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[OpenRouterLLMSettings] = None,
**kwargs,
):
"""Initialize the OpenRouter LLM service.
@@ -51,11 +59,11 @@ class OpenRouterLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="openai/gpt-4o-2024-11-20")
default_settings = OpenRouterLLMSettings(model="openai/gpt-4o-2024-11-20")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", OpenRouterLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -11,6 +11,7 @@ an OpenAI-compatible interface. It handles Perplexity's unique token usage
reporting patterns while maintaining compatibility with the Pipecat framework.
"""
from dataclasses import dataclass
from typing import Optional
from pipecat.adapters.services.open_ai_adapter import OpenAILLMInvocationParams
@@ -22,6 +23,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class PerplexityLLMSettings(OpenAILLMSettings):
"""Settings for Perplexity LLM service."""
pass
class PerplexityLLMService(OpenAILLMService):
"""A service for interacting with Perplexity's API.
@@ -36,7 +44,7 @@ class PerplexityLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.perplexity.ai",
model: Optional[str] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[PerplexityLLMSettings] = None,
**kwargs,
):
"""Initialize the Perplexity LLM service.
@@ -54,11 +62,11 @@ class PerplexityLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="sonar")
default_settings = PerplexityLLMSettings(model="sonar")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", PerplexityLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -6,6 +6,7 @@
"""Qwen LLM service implementation using OpenAI-compatible interface."""
from dataclasses import dataclass
from typing import Optional
from loguru import logger
@@ -15,6 +16,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class QwenLLMSettings(OpenAILLMSettings):
"""Settings for Qwen LLM service."""
pass
class QwenLLMService(OpenAILLMService):
"""A service for interacting with Alibaba Cloud's Qwen LLM API using the OpenAI-compatible interface.
@@ -28,7 +36,7 @@ class QwenLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
model: Optional[str] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[QwenLLMSettings] = None,
**kwargs,
):
"""Initialize the Qwen LLM service.
@@ -46,11 +54,11 @@ class QwenLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="qwen-plus")
default_settings = QwenLLMSettings(model="qwen-plus")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", QwenLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -7,6 +7,7 @@
"""SambaNova LLM service implementation using OpenAI-compatible interface."""
import json
from dataclasses import dataclass
from typing import Any, Dict, Optional
from loguru import logger
@@ -27,6 +28,13 @@ from pipecat.services.settings import _warn_deprecated_param
from pipecat.utils.tracing.service_decorators import traced_llm
@dataclass
class SambaNovaLLMSettings(OpenAILLMSettings):
"""Settings for SambaNova LLM service."""
pass
class SambaNovaLLMService(OpenAILLMService): # type: ignore
"""A service for interacting with SambaNova using the OpenAI-compatible interface.
@@ -40,7 +48,7 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore
api_key: str,
model: Optional[str] = None,
base_url: str = "https://api.sambanova.ai/v1",
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[SambaNovaLLMSettings] = None,
**kwargs: Dict[Any, Any],
) -> None:
"""Initialize SambaNova LLM service.
@@ -58,11 +66,11 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="Llama-4-Maverick-17B-128E-Instruct")
default_settings = SambaNovaLLMSettings(model="Llama-4-Maverick-17B-128E-Instruct")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", SambaNovaLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -6,6 +6,7 @@
"""Together.ai LLM service implementation using OpenAI-compatible interface."""
from dataclasses import dataclass
from typing import Optional
from loguru import logger
@@ -15,6 +16,13 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.settings import _warn_deprecated_param
@dataclass
class TogetherLLMSettings(OpenAILLMSettings):
"""Settings for Together LLM service."""
pass
class TogetherLLMService(OpenAILLMService):
"""A service for interacting with Together.ai's API using the OpenAI-compatible interface.
@@ -28,7 +36,7 @@ class TogetherLLMService(OpenAILLMService):
api_key: str,
base_url: str = "https://api.together.xyz/v1",
model: Optional[str] = None,
settings: Optional[OpenAILLMSettings] = None,
settings: Optional[TogetherLLMSettings] = None,
**kwargs,
):
"""Initialize Together.ai LLM service.
@@ -46,11 +54,11 @@ class TogetherLLMService(OpenAILLMService):
**kwargs: Additional keyword arguments passed to OpenAILLMService.
"""
# 1. Initialize default_settings with hardcoded defaults
default_settings = OpenAILLMSettings(model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo")
default_settings = TogetherLLMSettings(model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo")
# 2. Apply direct init arg overrides (deprecated)
if model is not None:
_warn_deprecated_param("model", OpenAILLMSettings, "model")
_warn_deprecated_param("model", TogetherLLMSettings, "model")
default_settings.model = model
# 4. Apply settings delta (canonical API, always wins)

View File

@@ -10,7 +10,7 @@ This module provides common functionality for services implementing the Whisper
interface, including language mapping, metrics generation, and error handling.
"""
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import AsyncGenerator, Optional
from loguru import logger
@@ -18,7 +18,7 @@ from openai import AsyncOpenAI
from openai.types.audio import Transcription
from pipecat.frames.frames import ErrorFrame, Frame, TranscriptionFrame
from pipecat.services.settings import STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.settings import NOT_GIVEN, STTSettings, _NotGiven, _warn_deprecated_param
from pipecat.services.stt_latency import WHISPER_TTFS_P99
from pipecat.services.stt_service import SegmentedSTTService
from pipecat.transcriptions.language import Language, resolve_language
@@ -36,8 +36,8 @@ class BaseWhisperSTTSettings(STTSettings):
temperature: Sampling temperature between 0 and 1.
"""
prompt: str | None | _NotGiven = None
temperature: float | None | _NotGiven = None
prompt: str | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
temperature: float | None | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
def language_to_whisper_language(language: Language) -> Optional[str]:
@@ -183,6 +183,8 @@ class BaseWhisperSTTService(SegmentedSTTService):
default_settings = BaseWhisperSTTSettings(
model=None,
language=None,
prompt=None,
temperature=None,
)
# --- 2. Deprecated direct-arg overrides ---

181
tests/test_service_init.py Normal file
View File

@@ -0,0 +1,181 @@
#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Tests for service settings and initialization patterns.
Settings objects operate in two modes:
- **Store mode** (``self._settings``): the live state inside a service.
Every field must hold a real value (``None`` is fine, ``NOT_GIVEN`` is not).
- **Delta mode** (``FooSettings()`` with no args): a sparse update.
Every field must default to ``NOT_GIVEN`` so ``apply_update()`` skips
untouched fields and doesn't accidentally overwrite the store.
These tests verify both sides of that contract automatically:
1. **Delta defaults** — Instantiate every ``ServiceSettings`` subclass with
no arguments and assert that every field is ``NOT_GIVEN``. Catches the
bug where a field defaults to ``None`` instead of ``NOT_GIVEN``, which
would cause partial deltas to silently overwrite unrelated store values.
2. **Store completeness** — Instantiate every concrete service with dummy
args and assert that ``_settings`` contains no ``NOT_GIVEN`` values.
This is the same check that ``validate_complete()`` runs in ``start()``,
but caught here at unit-test time without needing a running pipeline.
Catches services that forget to initialize a field in ``default_settings``.
All Settings and Service classes are auto-discovered via ``pkgutil``;
new services are covered automatically with no per-service maintenance.
"""
import importlib
import inspect
import pkgutil
import warnings
from dataclasses import fields
import pytest
import pipecat.services
from pipecat.services.ai_service import AIService
from pipecat.services.settings import ServiceSettings, is_given
# Modules that define abstract base service classes (not concrete services).
_BASE_MODULES = frozenset(
{
"pipecat.services.ai_service",
"pipecat.services.llm_service",
"pipecat.services.stt_service",
"pipecat.services.tts_service",
"pipecat.services.image_gen_service",
"pipecat.services.vision_service",
}
)
# ---------------------------------------------------------------------------
# Auto-discovery
# ---------------------------------------------------------------------------
def _all_subclasses(cls):
result = set()
for sub in cls.__subclasses__():
result.add(sub)
result.update(_all_subclasses(sub))
return result
def _import_all_service_modules():
"""Import every module under pipecat.services (skipping missing deps)."""
package = pipecat.services
for _importer, modname, _ispkg in pkgutil.walk_packages(
package.__path__, prefix=package.__name__ + "."
):
try:
importlib.import_module(modname)
except Exception:
continue
_import_all_service_modules()
ALL_SETTINGS_CLASSES = sorted(_all_subclasses(ServiceSettings), key=lambda c: c.__qualname__)
assert ALL_SETTINGS_CLASSES, "No settings classes discovered"
# ---------------------------------------------------------------------------
# Service instantiation helpers
# ---------------------------------------------------------------------------
def _try_instantiate(cls):
"""Try to instantiate a service with dummy values for required args.
Inspects the __init__ signature and passes "test" for every required
keyword-only parameter. Services that need non-string required args
or fail for other reasons will raise and be skipped by the test.
"""
sig = inspect.signature(cls.__init__)
kwargs = {}
for name, param in sig.parameters.items():
if name == "self":
continue
if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
continue
if param.default is not param.empty:
continue
# Required parameter — pass a dummy string
kwargs[name] = "test"
return cls(**kwargs)
def _discover_service_classes():
"""Return concrete service classes that can be instantiated with dummy args."""
result = []
for cls in sorted(_all_subclasses(AIService), key=lambda c: c.__qualname__):
# Skip abstract base classes defined in framework modules.
if cls.__module__ in _BASE_MODULES:
continue
try:
svc = _try_instantiate(cls)
except Exception:
continue
if hasattr(svc, "_settings"):
result.append(cls)
return result
ALL_SERVICE_CLASSES = _discover_service_classes()
assert ALL_SERVICE_CLASSES, "No service classes could be instantiated"
# ---------------------------------------------------------------------------
# 1. Settings defaults: delta-mode safety
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("settings_cls", ALL_SETTINGS_CLASSES, ids=lambda c: c.__qualname__)
def test_delta_defaults_are_not_given(settings_cls):
"""Every field must default to NOT_GIVEN so empty deltas are no-ops.
A field that defaults to None instead of NOT_GIVEN will cause
apply_update() to overwrite the corresponding store value whenever
a partial delta is applied.
"""
instance = settings_cls()
for f in fields(instance):
if f.name == "extra":
continue
val = getattr(instance, f.name)
assert not is_given(val), (
f"{settings_cls.__qualname__}.{f.name} defaults to {val!r}, expected NOT_GIVEN"
)
# ---------------------------------------------------------------------------
# 2. Service construction: store-mode completeness
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("service_cls", ALL_SERVICE_CLASSES, ids=lambda c: c.__qualname__)
def test_service_settings_complete(service_cls):
"""After construction, _settings must have no NOT_GIVEN values.
This is what validate_complete() checks in start(). Catching it
here means we don't need a running pipeline to find missing defaults.
"""
try:
svc = _try_instantiate(service_cls)
except Exception:
pytest.skip("Cannot re-instantiate (environment issue)")
for f in fields(svc._settings):
if f.name == "extra":
continue
val = getattr(svc._settings, f.name)
assert is_given(val), (
f"{service_cls.__qualname__}._settings.{f.name} is NOT_GIVEN after construction"
)