From 65e4e365dcc5e451d6ffbc7d89d4686f399604d4 Mon Sep 17 00:00:00 2001 From: Paul Kompfner Date: Wed, 11 Mar 2026 21:32:39 -0400 Subject: [PATCH] Add optional `service` field to `ServiceUpdateSettingsFrame` for targeting a specific service instance When `service` is set and doesn't match, the service forwards the frame instead of consuming it. This allows targeting a specific service when multiple services of the same type exist in the pipeline. --- src/pipecat/frames/frames.py | 5 +++++ src/pipecat/services/llm_service.py | 4 +++- src/pipecat/services/openai_realtime_beta/openai.py | 6 +++++- src/pipecat/services/stt_service.py | 4 +++- src/pipecat/services/tts_service.py | 4 +++- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index 390eb93dd..f58dc957f 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -2154,10 +2154,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 diff --git a/src/pipecat/services/llm_service.py b/src/pipecat/services/llm_service.py index 7944f413a..a479fcfc6 100644 --- a/src/pipecat/services/llm_service.py +++ b/src/pipecat/services/llm_service.py @@ -403,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. diff --git a/src/pipecat/services/openai_realtime_beta/openai.py b/src/pipecat/services/openai_realtime_beta/openai.py index 0a6315986..cacf8debd 100644 --- a/src/pipecat/services/openai_realtime_beta/openai.py +++ b/src/pipecat/services/openai_realtime_beta/openai.py @@ -386,7 +386,11 @@ class OpenAIRealtimeBetaLLMService(LLMService): # fields, not our Settings fields, so we construct SessionProperties # directly. The frame.delta path falls through to super, which calls # _update_settings → our override handles the rest. - if isinstance(frame, LLMUpdateSettingsFrame) and frame.delta is None: + if ( + isinstance(frame, LLMUpdateSettingsFrame) + and frame.delta is None + and (frame.service is None or frame.service is self) + ): self._session_properties = events.SessionProperties(**frame.settings) await self._send_session_update() await self.push_frame(frame, direction) diff --git a/src/pipecat/services/stt_service.py b/src/pipecat/services/stt_service.py index ebf007f6f..a16aa0eaa 100644 --- a/src/pipecat/services/stt_service.py +++ b/src/pipecat/services/stt_service.py @@ -357,7 +357,9 @@ class STTService(AIService): await self._handle_vad_user_stopped_speaking(frame) await self.push_frame(frame, direction) elif isinstance(frame, STTUpdateSettingsFrame): - 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. diff --git a/src/pipecat/services/tts_service.py b/src/pipecat/services/tts_service.py index da1bcaf87..a79156018 100644 --- a/src/pipecat/services/tts_service.py +++ b/src/pipecat/services/tts_service.py @@ -738,7 +738,9 @@ class TTSService(AIService): self._turn_context_id = saved_turn_context_id self._processing_text = processing_text elif isinstance(frame, TTSUpdateSettingsFrame): - 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.