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
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):
"""Process frames and handle service lifecycle.

View File

@@ -185,10 +185,9 @@ class AssemblyAISTTService(WebsocketSTTService):
return True
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
parameters are encoded in the WebSocket URL.
Settings are stored but not applied to the active connection.
Args:
update: A :class:`STTSettings` (or ``AssemblyAISTTSettings``) delta.
@@ -201,15 +200,18 @@ class AssemblyAISTTService(WebsocketSTTService):
if not changed:
return changed
# Re-apply manual turn mode config if vad_force_turn_endpoint is active
# and connection_params were updated.
if self._vad_force_turn_endpoint and "connection_params" in changed:
self._settings.connection_params = self._configure_manual_turn_mode(
self._settings.connection_params
)
# TODO: someday we could reconnect here to apply updated settings.
# Code might look something like the below:
# # Re-apply manual turn mode config if vad_force_turn_endpoint is active
# # and connection_params were updated.
# 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()
await self._connect()
self._warn_unhandled_updated_settings(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.services.llm_service import LLMService
from pipecat.services.settings import LLMSettings
from pipecat.utils.time import time_now_iso8601
try:
@@ -302,6 +303,29 @@ class AWSNovaSonicLLMService(LLMService):
with wave.open(file_path.open("rb"), "rb") as wav_file:
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
#

View File

@@ -141,16 +141,22 @@ class AWSTranscribeSTTService(WebsocketSTTService):
return encoding_map.get(encoding, encoding)
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.)
triggers a WebSocket reconnect so the new configuration takes effect.
Settings are stored but not applied to the active connection.
"""
changed = await super()._update_settings(update)
if changed and self._websocket:
await self._disconnect()
await self._connect()
if not changed:
return changed
# 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

View File

@@ -124,25 +124,28 @@ class AzureSTTService(STTService):
return True
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
speech recognizer is restarted so that the new language takes effect.
Settings are stored but not applied to the active recognizer.
"""
changed = await super()._update_settings(update)
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
if isinstance(lang, Language):
lang = language_to_azure_language(lang)
self._settings.language = lang
self._speech_config.speech_recognition_language = lang
# Restart the recognizer with the new config.
if self._speech_recognizer:
self._speech_recognizer.stop_continuous_recognition_async()
self._speech_recognizer.start_continuous_recognition_async()
# TODO: someday we could reconnect here to apply updated settings.
# Code might look something like the below:
# if "language" in changed:
# 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

View File

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

View File

@@ -344,6 +344,25 @@ class CartesiaTTSService(AudioContextWordTTSService):
"""
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]:
"""Convert a Language enum to Cartesia language format.

View File

@@ -27,6 +27,7 @@ from pipecat.frames.frames import (
UserStartedSpeakingFrame,
UserStoppedSpeakingFrame,
)
from pipecat.services.settings import STTSettings
from pipecat.services.stt_service import WebsocketSTTService
from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601
@@ -329,6 +330,25 @@ class DeepgramFluxSTTService(WebsocketSTTService):
"""
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):
"""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)
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
async def start(self, frame: StartFrame):

View File

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

View File

@@ -804,6 +804,25 @@ class GeminiLiveLLMService(LLMService):
"""
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):
"""Set the audio input pause state.

View File

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

View File

@@ -165,6 +165,25 @@ class InworldHttpTTSService(WordTTSService):
"""
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):
"""Start the Inworld TTS service.

View File

@@ -215,6 +215,25 @@ class PlayHTTTSService(InterruptibleTTSService):
"""
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]:
"""Convert a Language enum to PlayHT service language format.

View File

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

View File

@@ -338,11 +338,16 @@ class SarvamSTTService(STTService):
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:
# 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
async def set_prompt(self, prompt: Optional[str]):

View File

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

View File

@@ -225,7 +225,7 @@ class SonioxSTTService(WebsocketSTTService):
``input_params`` is given, its ``model`` is propagated *up* to the
top-level field.
Any change triggers a WebSocket reconnect.
Settings are stored but not applied to the active connection.
Args:
update: A settings delta.
@@ -249,8 +249,12 @@ class SonioxSTTService(WebsocketSTTService):
self._settings.model = self._settings.input_params.model
self.set_model_name(self._settings.model)
await self._disconnect()
await self._connect()
# 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

View File

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