refactor(voice): add VoiceProviderConfig base for pluggable providers - #114
Open
xinghaohuang91 wants to merge 5 commits into
Open
refactor(voice): add VoiceProviderConfig base for pluggable providers#114xinghaohuang91 wants to merge 5 commits into
xinghaohuang91 wants to merge 5 commits into
Conversation
xinghaohuang91
requested review from
ryanrishi,
ryanrouleau and
wenzhu1587
as code owners
August 26, 2026 17:47
…onfig type ConversationRelayProviderConfig now inherits VoiceProviderConfig, and VoiceChannel.__init__ accepts VoiceProviderConfig | dict | None instead of the concrete VoiceChannelConfig — so a future provider's config class can be passed without widening the channel's type signature again. VoiceChannelConfig is now an alias for ConversationRelayProviderConfig (flipped from the other direction). Since ConversationRelayProviderConfig is the only implementation today, VoiceChannel casts to it after normalizing dict/None input rather than adding an isinstance check — that check becomes necessary once a second provider config exists. Also adds empty placeholders for the provider split itself: VoiceProvider and VoiceProviderConfig.create_provider() (in provider.py), and ConversationRelayProvider (in conversation_relay.py). VoiceChannel builds self._provider via config.create_provider() but doesn't use it yet — VoiceChannel still owns all ConversationRelay logic directly. A follow-up PR moves that logic onto self._provider. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
xinghaohuang91
force-pushed
the
feat/conversation-relay-provider-config
branch
from
August 26, 2026 18:30
518dc45 to
2d12015
Compare
There was a problem hiding this comment.
Pull request overview
This PR lays groundwork for a future “pluggable voice providers” architecture by introducing a VoiceProviderConfig base type and a (currently placeholder) provider implementation, while keeping ConversationRelay as the only concrete provider today.
Changes:
- Added
VoiceProvider/VoiceProviderConfigscaffolding and a placeholderConversationRelayProvider. - Renamed the concrete voice config model to
ConversationRelayProviderConfig(withVoiceChannelConfigkept as an alias) and addedcreate_provider(). - Widened
VoiceChannel.__init__to acceptVoiceProviderConfig | dict | Noneand instantiated a provider viaconfig.create_provider().
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/tac/channels/voice/provider.py | Introduces base provider + base config (VoiceProviderConfig) with a create_provider() hook. |
| src/tac/channels/voice/conversation_relay.py | Adds placeholder ConversationRelayProvider class for the default provider. |
| src/tac/channels/voice/config.py | Makes ConversationRelayProviderConfig inherit VoiceProviderConfig, implements create_provider(), and aliases VoiceChannelConfig. |
| src/tac/channels/voice/channel.py | Broadens config parameter type, normalizes dict/None input, and creates a provider instance. |
| src/tac/channels/voice/init.py | Re-exports the new provider symbols and ConversationRelay provider/config. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
create_provider(tac_config) instead of create_provider() — a provider that talks TwiML (ConversationRelayProvider) needs TACConfig to derive default URLs (voice_public_domain etc.), and a config object has no other way to reach it. ConversationRelayProvider's constructor takes tac_config too, in preparation for building its own TwiMLBuilderConversationRelay in a follow-up PR. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tion VoiceProvider.__init__(channel) stores self.channel instead of every method taking channel as a parameter — a provider only ever serves the one VoiceChannel that built it (create_provider() is called once, from VoiceChannel.__init__), so there's no case where threading it through each call buys anything. create_provider(channel, tac_config) passes it through; storing the reference before BaseChannel.__init__ finishes is safe since it's only read later, once the channel is fully constructed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
self.logger = get_logger(self.__class__.__module__), same convention as BaseChannel, instead of providers borrowing channel.logger. Log lines end up attributed to the provider that actually emitted them (e.g. tac.channels.voice.conversation_relay) rather than generically to tac.channels.voice.channel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3 tasks
ryanrouleau
approved these changes
Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
VoiceProviderConfigbase (empty for now) and makesConversationRelayProviderConfig(néeVoiceChannelConfig) inherit it.VoiceChannel.__init__'sconfigparameter toVoiceProviderConfig | dict[str, Any] | None, so a future provider's config class can be passed without widening the channel's type signature again.ConversationRelayProviderConfigis the only implementation today, soVoiceChannelcasts to it after normalizing dict/None input rather than adding anisinstancecheck — that check becomes necessary (and acreate_provider()factory method replaces the cast entirely) once a second provider config exists, in a follow-up PR.Test plan
make lint/make type-check/make checkall passtests/andgetting_started/have zero diff — public API and examples untouched