Skip to content

Broad refactor of service settings and how they’re updated at runtime#3714

Merged
kompfner merged 63 commits intomainfrom
pk/service-settings-refactor
Feb 23, 2026
Merged

Broad refactor of service settings and how they’re updated at runtime#3714
kompfner merged 63 commits intomainfrom
pk/service-settings-refactor

Conversation

@kompfner
Copy link
Copy Markdown
Contributor

@kompfner kompfner commented Feb 11, 2026

Maybe the best way to understand these changes is to check out the COMMUNITY_INTEGRATIONS.md changes.

Does not (yet) touch InputParams, to avoid scope creep, but what to do with those is an open question. InputParams represents what you can specify at init time, and *Settings represents what can (theoretically) be updated at runtime. These often overlap heavily. In the pattern introduced in this PR, self._settings is the source of truth, and the init-time InputParams are used just to seed self._settings (which was kind of the pattern before, but never really formalized or adhered to especially strictly)

NOTE: manual testing of every service in progress, there may be some more changes that creep in.

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 11, 2026

Codecov Report

❌ Patch coverage is 14.02878% with 1195 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/pipecat/services/elevenlabs/tts.py 0.00% 77 Missing ⚠️
src/pipecat/services/elevenlabs/stt.py 0.00% 68 Missing ⚠️
src/pipecat/services/inworld/tts.py 0.00% 64 Missing ⚠️
src/pipecat/services/minimax/tts.py 0.00% 56 Missing ⚠️
src/pipecat/services/cartesia/tts.py 0.00% 53 Missing ⚠️
src/pipecat/services/grok/realtime/llm.py 0.00% 41 Missing ⚠️
src/pipecat/services/google/stt.py 33.33% 40 Missing ⚠️
src/pipecat/services/google/tts.py 43.28% 38 Missing ⚠️
src/pipecat/services/azure/tts.py 0.00% 34 Missing ⚠️
src/pipecat/services/deepgram/flux/stt.py 0.00% 34 Missing ⚠️
... and 49 more
Files with missing lines Coverage Δ
src/pipecat/services/groq/stt.py 0.00% <ø> (ø)
src/pipecat/services/openai/base_llm.py 59.89% <100.00%> (+2.36%) ⬆️
src/pipecat/services/playht/tts.py 0.00% <ø> (ø)
src/pipecat/services/resembleai/tts.py 0.00% <ø> (ø)
src/pipecat/services/rime/tts.py 0.00% <ø> (ø)
src/pipecat/services/sambanova/llm.py 45.33% <ø> (ø)
src/pipecat/services/sambanova/stt.py 44.44% <ø> (ø)
src/pipecat/services/sarvam/stt.py 0.00% <ø> (ø)
src/pipecat/services/sarvam/tts.py 0.00% <ø> (ø)
src/pipecat/services/settings.py 98.92% <ø> (ø)
... and 69 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/pipecat/services/asyncai/tts.py Outdated

self.set_model_name(model)
self.set_voice(voice_id)
self._voice_id = voice_id
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously it wasn't clear whether set_voice/set_model/set_language were meant to be public APIs that users could directly invoke—in which case they should handle applying the change by reconnecting if necessary, etc.—or whether they were meant to only be "bookkeeping" methods, invokable only by subclasses, to update the underlying instance variables (_voice_id, etc).

I've made the decision here to make set_voice/set_model/set_language the "public" methods (even though we probably would suggest that users to use *UpdateSettingsFrames instead) and if we just need to update bookkeeping, access the instance variable directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do use set_voice() directly in one example.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I see that it is deprecated now, should we keep using it in our example ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm...actually, in the example (example 35), we kind of need the voice change to take effect immediately. It can't be queued. The voice switches occur partway through the TTS service processing an LLM response. So using an STTUpdateSettingsFrame is not really a drop-in replacement for set_voice()...

But also...that example doesn't quite work? The timing of the voice switches is always wrong. I've confirmed that this was the case before the refactor, too.

With these things in mind, I'm going to punt on updating that example for now.

@kompfner kompfner force-pushed the pk/service-settings-refactor branch 13 times, most recently from 25536e8 to b1f4c32 Compare February 12, 2026 19:15
language_codes: List of Google STT language code strings
(e.g. ``["en-US"]``).

.. deprecated:: 0.0.103
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: update this (and all other occurrences of this) if it looks like these changes will land in a different version

@kompfner kompfner force-pushed the pk/service-settings-refactor branch 10 times, most recently from 7a8f453 to 6d7794f Compare February 13, 2026 16:41
model: Any = field(default_factory=lambda: NOT_GIVEN)
"""AI model identifier (e.g. ``"gpt-4o"``, ``"eleven_turbo_v2_5"``)."""

extra: Dict[str, Any] = field(default_factory=dict)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this PR doesn't go through every service and add handling for the extra field. It will be up to each service to add it when it's a priority. This can be done piecemeal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. 👍

…ce settings discoverable and strongly-typed. Service settings can be updated at runtime with `*UpdateSettingsFrame`s.

Does not (yet) touch `InputParams`, to avoid scope creep and touching something currently part of the public API. But there is a lot of overlap between `*Settings` object fields and `InputParams` fields.

Other than discoverability/typing, these are some other improvements brought by this refactor:
- There is now a single code path (see `_update_settings_from_typed`) where services can respond to settings changes (by, say, reconnecting if needed), improving maintainability and guaranteeing one and only one reconnection no matter which settings changed
- `set_language`/`set_model`/`set_voice`—which we're assuming are usable as public methods, though *not* recommended over `*UpdateSettingsFrame`—all use the same code path as settings updates. They're also now all consistent in that, if a service needs to respond to a change (by, say, reconnecting if needed), any of these methods will kick off that process. Note that this is technically a behavior change.
- Several services now properly react to changed settings by reconnecting:
  - `AWSTranscribeSTTService`
  - `AzureSTTService`
  - `SonioxSTTService`
  - `GladiaSTTService`
  - `SpeechmaticsSTTService`
  - `AssemblyAISTTService`
  - `CartesiaSTTService`
  - `FishAudioTTSService` (would previously only reconnect when `model` changed)
  - `GoogleSTTService`
  - `SpeechmaticsSTTService` (which previously only handled *some* settings updates through a nonstandard public `update_params` method)
  - `GradiumSTTService`
  - `NvidiaSegmentedSTTService` (which previously only handled changes to language)
- Bookkeeping across various services has been reduced, mostly by deduping ivars; the `self._settings` ivar is treated as the source of truth

NOTE: I pretty much guarantee that there are services missed in this PR in terms of bringing to consistency with how updates are handled (like whether changes in certain fields trigger reconnects when they need to). We can squash remaining inconsistencies as we stumble onto them, service by service. The goal here is to get things *mostly* in order, and establish the infrastructure and patterns we'll need going forward.
…d-service-settings path.

`filter_incomplete_user_turns` and `user_turn_completion_config` were only handled in the legacy dict-based `_update_settings` code path. This adds them to `LLMSettings` and introduces `LLMService._update_settings_from_typed` so the typed path handles them too.
…or better editor support.

Standardize all STT, TTS, and LLM service classes to declare `_settings` with the narrowed Settings type as a class-level annotation. This gives editors and type checkers the specific type when hovering or autocompleting on `self._settings` in each service and its subclasses. Inline `self._settings: Type = ...` assignments are replaced with plain `self._settings = ...`.
@vmfullyfaltu
Copy link
Copy Markdown

would this work with service switcher with 2 different STT services?

Great question! I actually think today you'd have to figure out the active service and push to that one, since the service switcher blocks most inbound frames—including settings update frames—to inactive services.

This is a really great consideration, though—I think we can come up with something better than that. We'll tackle this work soon. Thanks!

@kompfner understood and thank you for your reply. would this work in flows to swap deepgram keyterms as you navigate from node to node through the settings frame?

…le settings

Update docstrings for ServiceSettings, LLMSettings, TTSSettings, and STTSettings to make clear these capture only the subset of service configuration that can be changed while the pipeline is running via UpdateSettingsFrame, not all constructor parameters.
Simplify the reconnect example to show a common pattern (reconnect on any change) and improve the _warn_unhandled_updated_settings example to show selective handling of specific fields.
…cessfully disconnect/reconnect to apply runtime settings updates. For now, marking them as not yet supporting runtime settings updates.
@kompfner kompfner force-pushed the pk/service-settings-refactor branch from 13731f8 to bcf11ec Compare February 23, 2026 21:02
Copy link
Copy Markdown
Contributor

@filipi87 filipi87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Paul, this PR is huge, amazing job. 👏🔥

There are still a couple of comments open that I believe haven’t been addressed yet.

I am not sure whether you’ve already added them to your TODO list to be implemented as follow up PRs, or if they’re not actually needed, but I believe these are the main ones:

  • Move _settings to the initializer, as Aleix suggested, so we only need to invoke self._sync_model_name_to_metrics() inside AiService.
  • Make ServiceUpdateSettingsFrame uninterruptible.
  • The name update inside ServiceUpdateSettingsFrame feels a bit generic. Would it make more sense to call it new_settings, delta_settings, or service_settings?
  • GeminiTTSService still holds a reference to self._model, which is never updated. I believe the same might be true for HathoraSTTService and HathoraTTSService.
  • Inside ServiceSettings.apply_update, I added two comments suggesting defaulting to NOT_GIVEN. It might be worth applying that change.

Other considerations:

  • Inside AsyncAITTSSettings, since the API expects a nested output_format, would it be clearer to keep that nested structure closer to what the service API expects ? For example, keeping output_format inside AsyncAITTSSettings instead of flattening the fields. Maybe the same apply for other services.

In any case, I am leaving my approval, as I agree we should merge this as soon as possible. Any improvements can be implemented in follow up PRs, which should be much easier to maintain.

Again, great work. 🔥🚀

Split the single "changed" entry into separate "added", "changed", and "deprecated" entries for clarity. Add a note about the subtle behavior change in the deprecated set_model/set_voice/set_language methods.
… frame reference

Use wildcard `*UpdateSettingsFrame` to cover all frame types. Clarify that NOT_GIVEN only appears in update deltas, not in the service's current settings state.
input_params: Soniox ``SonioxInputParams`` for detailed configuration.
"""

input_params: SonioxInputParams | _NotGiven = field(default_factory=lambda: NOT_GIVEN)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: this doesn't follow our new pattern. We should include the fields of the input params directly in the settings object. Audit the other services for this mistake, too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found that Soniox STT and Gladia STT weren't following the new pattern. Updating in #3812.

self.set_model_name(merged_options["model"])
self._settings = merged_options
merged_live_options = LiveOptions(**merged_options)
self._settings = DeepgramSTTSettings(
Copy link
Copy Markdown
Contributor Author

@kompfner kompfner Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: previously looks like we stored all the LiveOptions fields directly in self._settings, meaning that users could specify those fields directly in the update dictionary, not nested under live_options. Fix this broken backward compatibility (use from_mapping), and audit other services for the same kind of mistake. To audit, maybe look for:

  • other uses of LiveOptions
  • to_dict() in service initializers
  • *Settings objects that contain any 3rd-party SDK objects in their fields

@kompfner
Copy link
Copy Markdown
Contributor Author

Thanks @filipi87! Will go ahead and merge just to make conflicts more manageable, but will open various follow-up PRs in a hopefully more targeted and rapid-fire manner.

@kompfner kompfner merged commit cdd65b6 into main Feb 23, 2026
5 checks passed
@kompfner kompfner deleted the pk/service-settings-refactor branch February 23, 2026 22:15
@kompfner
Copy link
Copy Markdown
Contributor Author

The name update inside ServiceUpdateSettingsFrame feels a bit generic. Would it make more sense to call it new_settings, delta_settings, or service_settings?

Renamed in #3812 @filipi87

@kompfner
Copy link
Copy Markdown
Contributor Author

Hm...wonder if we should make the *UpdateSettingsFrames uninterruptible? In my testing, if the settings update is queued while the bot is speaking, and then I interrupt the bot, the update never takes effect. As a user, that might not be the desired outcome.

I think this makes sense. We did the same for the summarization result. Otherwise, most of the time it wouldn’t be applied.

#3819

@kompfner
Copy link
Copy Markdown
Contributor Author

kompfner commented Feb 25, 2026

would this work with service switcher with 2 different STT services?

Great question! I actually think today you'd have to figure out the active service and push to that one, since the service switcher blocks most inbound frames—including settings update frames—to inactive services.
This is a really great consideration, though—I think we can come up with something better than that. We'll tackle this work soon. Thanks!

@kompfner understood and thank you for your reply. would this work in flows to swap deepgram keyterms as you navigate from node to node through the settings frame?

Though I did not test that specific thing, in theory, yes, the *UpdateSettingsFrames should be useful for exactly things like that. In terms of updating deepgram settings specifically, and updating things in a way that user interruptions shouldn't interfere with, there are a couple of follow-up PRs to this one that may be of interest:

@kompfner
Copy link
Copy Markdown
Contributor Author

@filipi87 I think I addressed all the comments except the suggestion of passing settings to super().__init__ to let AIService be the sole handler of syncing model name to metrics, which will be the biggest change. Will tackle tomorrow.

@kompfner
Copy link
Copy Markdown
Contributor Author

@filipi87 I think I addressed all the comments except the suggestion of passing settings to super().__init__ to let AIService be the sole handler of syncing model name to metrics, which will be the biggest change. Will tackle tomorrow.

And here it is: #3834

markbackman added a commit that referenced this pull request Feb 28, 2026
The ServiceSettings refactor (PR #3714) changed self._settings from
dicts to dataclass subclasses, but tracing code still used .items(),
in containment, and subscript access, causing AttributeError on
every traced call. Use given_fields() for iteration and attribute
access for named fields.
markbackman added a commit that referenced this pull request Feb 28, 2026
The ServiceSettings refactor (PR #3714) changed self._settings from
dicts to dataclass subclasses, but tracing code still used .items(),
in containment, and subscript access, causing AttributeError on
every traced call. Use given_fields() for iteration and attribute
access for named fields.
markbackman added a commit that referenced this pull request Mar 2, 2026
The ServiceSettings refactor (PR #3714) changed self._settings from
dicts to dataclass subclasses, but tracing code still used .items(),
in containment, and subscript access, causing AttributeError on
every traced call. Use given_fields() for iteration and attribute
access for named fields.
markbackman added a commit that referenced this pull request Mar 2, 2026
The ServiceSettings refactor (PR #3714) changed self._settings from
dicts to dataclass subclasses, but tracing code still used .items(),
in containment, and subscript access, causing AttributeError on
every traced call. Use given_fields() for iteration and attribute
access for named fields.
blainekasten pushed a commit to blainekasten/pipecat that referenced this pull request Mar 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants