In services that don't handle runtime settings updates—or don't handle them for *all* available settings—log a warning about which fields specifically aren't handled. Revert new apply-settings-updates logic across various services, to reduce PR testing scope. This logic can be added service by service gradually as future work.

Note that for services that previously handled applying updates (through methods like `set_model` and `set_language`), we're keeping the update-applying logic (some or most of which is already well-tested) and expanding it to cover all relevant settings fields. Services under this bucket are:
- Deepgram STT
- Deepgram Sagemaker STT
- Elevenlabs STT
- Google STT
- Gradium STT
- OpenAI STT
- Speechmatics STT
This commit is contained in:
Paul Kompfner
2026-02-17 09:35:58 -05:00
parent 66b7b4a5d4
commit 3a77b4c1d8
19 changed files with 218 additions and 44 deletions

View File

@@ -123,6 +123,20 @@ class AIService(FrameProcessor):
return changed return changed
def _warn_unhandled_updated_settings(self, unhandled: Set[str]):
"""Log a warning for settings changes that won't take effect at runtime.
Convenience helper for ``_update_settings`` overrides. Call with the
set of field names that changed but that the service does not (yet)
apply at runtime.
Args:
unhandled: Field names that changed but are not applied.
"""
if unhandled:
fields = ", ".join(sorted(unhandled))
logger.warning(f"{self.name}: runtime update of [{fields}] is not currently supported")
async def process_frame(self, frame: Frame, direction: FrameDirection): async def process_frame(self, frame: Frame, direction: FrameDirection):
"""Process frames and handle service lifecycle. """Process frames and handle service lifecycle.

View File

@@ -185,10 +185,9 @@ class AssemblyAISTTService(WebsocketSTTService):
return True return True
async def _update_settings(self, update: STTSettings) -> set[str]: async def _update_settings(self, update: STTSettings) -> set[str]:
"""Apply a settings update and reconnect if anything changed. """Apply a settings update.
Any change triggers a WebSocket reconnect since all connection Settings are stored but not applied to the active connection.
parameters are encoded in the WebSocket URL.
Args: Args:
update: A :class:`STTSettings` (or ``AssemblyAISTTSettings``) delta. update: A :class:`STTSettings` (or ``AssemblyAISTTSettings``) delta.
@@ -201,15 +200,18 @@ class AssemblyAISTTService(WebsocketSTTService):
if not changed: if not changed:
return changed return changed
# Re-apply manual turn mode config if vad_force_turn_endpoint is active # TODO: someday we could reconnect here to apply updated settings.
# and connection_params were updated. # Code might look something like the below:
if self._vad_force_turn_endpoint and "connection_params" in changed: # # Re-apply manual turn mode config if vad_force_turn_endpoint is active
self._settings.connection_params = self._configure_manual_turn_mode( # # and connection_params were updated.
self._settings.connection_params # if self._vad_force_turn_endpoint and "connection_params" in changed:
) # self._settings.connection_params = self._configure_manual_turn_mode(
# self._settings.connection_params
# )
# await self._disconnect()
# await self._connect()
await self._disconnect() self._warn_unhandled_updated_settings(changed)
await self._connect()
return changed return changed

View File

@@ -60,6 +60,7 @@ from pipecat.processors.aggregators.openai_llm_context import (
) )
from pipecat.processors.frame_processor import FrameDirection from pipecat.processors.frame_processor import FrameDirection
from pipecat.services.llm_service import LLMService from pipecat.services.llm_service import LLMService
from pipecat.services.settings import LLMSettings
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
try: try:
@@ -302,6 +303,29 @@ class AWSNovaSonicLLMService(LLMService):
with wave.open(file_path.open("rb"), "rb") as wav_file: with wave.open(file_path.open("rb"), "rb") as wav_file:
self._assistant_response_trigger_audio = wav_file.readframes(wav_file.getnframes()) self._assistant_response_trigger_audio = wav_file.readframes(wav_file.getnframes())
#
# settings
#
async def _update_settings(self, update: LLMSettings) -> set[str]:
"""Apply a settings update.
Settings are stored but not applied to the active connection.
"""
changed = await super()._update_settings(update)
if not changed:
return changed
# TODO: someday we could reconnect here to apply updated settings.
# Code might look something like the below:
# await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
return changed
# #
# standard AIService frame handling # standard AIService frame handling
# #

View File

@@ -141,16 +141,22 @@ class AWSTranscribeSTTService(WebsocketSTTService):
return encoding_map.get(encoding, encoding) return encoding_map.get(encoding, encoding)
async def _update_settings(self, update: STTSettings) -> set[str]: async def _update_settings(self, update: STTSettings) -> set[str]:
"""Apply a settings update, reconnecting if needed. """Apply a settings update.
Any change to connection-relevant settings (model, language, etc.) Settings are stored but not applied to the active connection.
triggers a WebSocket reconnect so the new configuration takes effect.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if changed and self._websocket: if not changed:
await self._disconnect() return changed
await self._connect()
# TODO: someday we could reconnect here to apply updated settings.
# Code might look something like the below:
# if changed and self._websocket:
# await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
return changed return changed

View File

@@ -124,25 +124,28 @@ class AzureSTTService(STTService):
return True return True
async def _update_settings(self, update: STTSettings) -> set[str]: async def _update_settings(self, update: STTSettings) -> set[str]:
"""Apply a settings update, reconfiguring the recognizer if needed. """Apply a settings update.
When ``language`` changes the ``SpeechConfig`` is updated and the Settings are stored but not applied to the active recognizer.
speech recognizer is restarted so that the new language takes effect.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if "language" in changed: if "language" in changed:
# Convert Language enum to Azure language code if needed. # Convert Language enum to Azure language code for consistency.
lang = self._settings.language lang = self._settings.language
if isinstance(lang, Language): if isinstance(lang, Language):
lang = language_to_azure_language(lang) lang = language_to_azure_language(lang)
self._settings.language = lang self._settings.language = lang
self._speech_config.speech_recognition_language = lang
# Restart the recognizer with the new config. # TODO: someday we could reconnect here to apply updated settings.
if self._speech_recognizer: # Code might look something like the below:
self._speech_recognizer.stop_continuous_recognition_async() # if "language" in changed:
self._speech_recognizer.start_continuous_recognition_async() # self._speech_config.speech_recognition_language = self._settings.language
# if self._speech_recognizer:
# self._speech_recognizer.stop_continuous_recognition_async()
# self._speech_recognizer.start_continuous_recognition_async()
self._warn_unhandled_updated_settings(changed)
return changed return changed

View File

@@ -295,7 +295,7 @@ class CartesiaSTTService(WebsocketSTTService):
await self._disconnect_websocket() await self._disconnect_websocket()
async def _update_settings(self, update: STTSettings) -> set[str]: async def _update_settings(self, update: STTSettings) -> set[str]:
"""Apply a settings update and reconnect if anything changed. """Apply a settings update.
Args: Args:
update: A :class:`STTSettings` (or ``CartesiaSTTSettings``) delta. update: A :class:`STTSettings` (or ``CartesiaSTTSettings``) delta.
@@ -304,9 +304,15 @@ class CartesiaSTTService(WebsocketSTTService):
Set of field names whose values actually changed. Set of field names whose values actually changed.
""" """
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if changed:
await self._disconnect() # TODO: someday we could reconnect here to apply updated settings.
await self._connect() # Code might look something like the below:
# if changed:
# await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
return changed return changed
async def _connect_websocket(self): async def _connect_websocket(self):

View File

@@ -344,6 +344,25 @@ class CartesiaTTSService(AudioContextWordTTSService):
""" """
return True return True
async def _update_settings(self, update: TTSSettings) -> set[str]:
"""Apply a settings update.
Settings are stored but not applied to the active connection.
"""
changed = await super()._update_settings(update)
if not changed:
return changed
# TODO: someday we could reconnect here to apply updated settings.
# Code might look something like the below:
# await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
return changed
def language_to_service_language(self, language: Language) -> Optional[str]: def language_to_service_language(self, language: Language) -> Optional[str]:
"""Convert a Language enum to Cartesia language format. """Convert a Language enum to Cartesia language format.

View File

@@ -27,6 +27,7 @@ from pipecat.frames.frames import (
UserStartedSpeakingFrame, UserStartedSpeakingFrame,
UserStoppedSpeakingFrame, UserStoppedSpeakingFrame,
) )
from pipecat.services.settings import STTSettings
from pipecat.services.stt_service import WebsocketSTTService from pipecat.services.stt_service import WebsocketSTTService
from pipecat.transcriptions.language import Language from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
@@ -329,6 +330,25 @@ class DeepgramFluxSTTService(WebsocketSTTService):
""" """
return True return True
async def _update_settings(self, update: STTSettings) -> set[str]:
"""Apply a settings update.
Settings are stored but not applied to the active connection.
"""
changed = await super()._update_settings(update)
if not changed:
return changed
# TODO: someday we could reconnect here to apply updated settings.
# Code might look something like the below:
# await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
return changed
async def start(self, frame: StartFrame): async def start(self, frame: StartFrame):
"""Start the Deepgram Flux STT service. """Start the Deepgram Flux STT service.

View File

@@ -516,6 +516,12 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
await self.push_error(error_msg=f"Unknown error occurred: {e}", exception=e) await self.push_error(error_msg=f"Unknown error occurred: {e}", exception=e)
self._context_id = None self._context_id = None
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
self._warn_unhandled_updated_settings(changed - handled)
return changed return changed
async def start(self, frame: StartFrame): async def start(self, frame: StartFrame):

View File

@@ -382,8 +382,7 @@ class GladiaSTTService(WebsocketSTTService):
async def _update_settings(self, update: GladiaSTTSettings) -> set[str]: async def _update_settings(self, update: GladiaSTTSettings) -> set[str]:
"""Apply settings update. """Apply settings update.
Gladia sessions are fixed at creation time, so any change requires Settings are stored but not applied to the active session.
a full session teardown and reconnect.
Args: Args:
update: A settings delta. update: A settings delta.
@@ -396,11 +395,14 @@ class GladiaSTTService(WebsocketSTTService):
if not changed: if not changed:
return changed return changed
# Gladia sessions are fixed — need to tear down and recreate # TODO: someday we could reconnect here to apply updated settings.
self._session_url = None # Code might look something like the below:
self._session_id = None # self._session_url = None
await self._disconnect() # self._session_id = None
await self._connect() # await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
return changed return changed

View File

@@ -804,6 +804,25 @@ class GeminiLiveLLMService(LLMService):
""" """
return True return True
async def _update_settings(self, update: LLMSettings) -> set[str]:
"""Apply a settings update.
Settings are stored but not applied to the active connection.
"""
changed = await super()._update_settings(update)
if not changed:
return changed
# TODO: someday we could reconnect here to apply updated settings.
# Code might look something like the below:
# await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
return changed
def set_audio_input_paused(self, paused: bool): def set_audio_input_paused(self, paused: bool):
"""Set the audio input pause state. """Set the audio input pause state.

View File

@@ -133,6 +133,8 @@ class GradiumTTSService(InterruptibleWordTTSService):
if self._voice_id != prev_voice: if self._voice_id != prev_voice:
await self._disconnect() await self._disconnect()
await self._connect() await self._connect()
else:
self._warn_unhandled_updated_settings(changed)
return changed return changed
def _build_msg(self, text: str = "") -> dict: def _build_msg(self, text: str = "") -> dict:

View File

@@ -165,6 +165,25 @@ class InworldHttpTTSService(WordTTSService):
""" """
return True return True
async def _update_settings(self, update: TTSSettings) -> set[str]:
"""Apply a settings update.
Settings are stored but not applied to the active connection.
"""
changed = await super()._update_settings(update)
if not changed:
return changed
# TODO: someday we could reconnect here to apply updated settings.
# Code might look something like the below:
# await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
return changed
async def start(self, frame: StartFrame): async def start(self, frame: StartFrame):
"""Start the Inworld TTS service. """Start the Inworld TTS service.

View File

@@ -215,6 +215,25 @@ class PlayHTTTSService(InterruptibleTTSService):
""" """
return True return True
async def _update_settings(self, update: TTSSettings) -> set[str]:
"""Apply a settings update.
Settings are stored but not applied to the active connection.
"""
changed = await super()._update_settings(update)
if not changed:
return changed
# TODO: someday we could reconnect here to apply updated settings.
# Code might look something like the below:
# await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
return changed
def language_to_service_language(self, language: Language) -> Optional[str]: def language_to_service_language(self, language: Language) -> Optional[str]:
"""Convert a Language enum to PlayHT service language format. """Convert a Language enum to PlayHT service language format.

View File

@@ -279,6 +279,8 @@ class RimeTTSService(AudioContextWordTTSService):
self._settings.speaker = self._voice_id self._settings.speaker = self._voice_id
await self._disconnect() await self._disconnect()
await self._connect() await self._connect()
else:
self._warn_unhandled_updated_settings(changed)
return changed return changed
def _build_msg(self, text: str = "") -> dict: def _build_msg(self, text: str = "") -> dict:

View File

@@ -338,11 +338,16 @@ class SarvamSTTService(STTService):
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if not changed: # TODO: someday we could reconnect here to apply updated settings.
return changed # Code might look something like the below:
# if not changed:
# return changed
# await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
await self._disconnect()
await self._connect()
return changed return changed
async def set_prompt(self, prompt: Optional[str]): async def set_prompt(self, prompt: Optional[str]):

View File

@@ -958,6 +958,7 @@ class SarvamTTSService(InterruptibleTTSService):
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if "voice" in changed: if "voice" in changed:
await self._send_config() await self._send_config()
self._warn_unhandled_updated_settings(changed - {"voice"})
return changed return changed
async def _connect(self): async def _connect(self):

View File

@@ -225,7 +225,7 @@ class SonioxSTTService(WebsocketSTTService):
``input_params`` is given, its ``model`` is propagated *up* to the ``input_params`` is given, its ``model`` is propagated *up* to the
top-level field. top-level field.
Any change triggers a WebSocket reconnect. Settings are stored but not applied to the active connection.
Args: Args:
update: A settings delta. update: A settings delta.
@@ -249,8 +249,12 @@ class SonioxSTTService(WebsocketSTTService):
self._settings.model = self._settings.input_params.model self._settings.model = self._settings.input_params.model
self.set_model_name(self._settings.model) self.set_model_name(self._settings.model)
await self._disconnect() # TODO: someday we could reconnect here to apply updated settings.
await self._connect() # Code might look something like the below:
# await self._disconnect()
# await self._connect()
self._warn_unhandled_updated_settings(changed)
return changed return changed

View File

@@ -329,6 +329,7 @@ class UltravoxRealtimeLLMService(LLMService):
changed = await super()._update_settings(update) changed = await super()._update_settings(update)
if "output_medium" in changed: if "output_medium" in changed:
await self._update_output_medium(self._settings.output_medium) await self._update_output_medium(self._settings.output_medium)
self._warn_unhandled_updated_settings(changed - {"output_medium"})
return changed return changed
# #