#22889: feat(talk): add provider-agnostic talk config contract
app: android
app: ios
app: macos
app: web-ui
gateway
maintainer
size: XL
Cluster:
Model Configuration Fixes
## Summary
Describe the problem and fix in 2–5 bullets:
- Problem: `talk.*` contract was ElevenLabs-specific (`voiceId/modelId/outputFormat/apiKey`) and blocked clean multi-provider evolution.
- Why it matters: iOS/macOS/Android/gateway need a stable provider-agnostic shape while preserving existing deployments during rollout.
- What changed: added normalized `talk.provider` + `talk.providers[provider]` contract, read-time normalization/migration, gateway response normalization with temporary legacy fields, and client parsing that prefers normalized then falls back to legacy.
- What did NOT change (scope boundary): no removal of legacy fields yet (phase 1 only), no provider behavior switch in runtime TTS selection beyond config parsing compatibility.
## Change Type (select all)
- [x] Bug fix
- [x] Feature
- [x] Refactor
- [ ] Docs
- [ ] Security hardening
- [ ] Chore/infra
## Scope (select all touched areas)
- [x] Gateway / orchestration
- [ ] Skills / tool execution
- [ ] Auth / tokens
- [ ] Memory / storage
- [ ] Integrations
- [x] API / contracts
- [x] UI / DX
- [ ] CI/CD / infra
## Linked Issue/PR
- Closes #
- Related #22817
## User-visible / Behavior Changes
- Gateway `talk.config` now returns normalized provider-agnostic contract (`talk.provider`, `talk.providers`) and still includes legacy ElevenLabs keys for compatibility.
- iOS/macOS/Android now read normalized talk config first and fallback to legacy fields only when normalized payload is absent.
- iOS keychain talk API key storage is provider-routed and keeps ElevenLabs legacy key migration fallback.
## Security Impact (required)
- New permissions/capabilities? (`No`)
- Secrets/tokens handling changed? (`Yes`)
- New/changed network calls? (`No`)
- Command/tool execution surface changed? (`No`)
- Data access scope changed? (`No`)
- If any `Yes`, explain risk + mitigation:
- Talk API key routing now supports provider-specific key lookup/write paths. Mitigation: gateway keeps existing secret redaction behavior, legacy key fallback remains during migration, and compatibility tests cover parse order and fallback semantics.
## Repro + Verification
### Environment
- OS: macOS
- Runtime/container: Node 22 + pnpm
- Model/provider: talk config normalization (default provider `elevenlabs`)
- Integration/channel (if any): gateway talk.config + iOS/macOS/Android talk clients
- Relevant config (redacted): legacy `talk.voiceId/modelId/outputFormat/apiKey` and normalized `talk.provider/providers.*`
### Steps
1. Configure gateway with legacy talk keys only.
2. Call `talk.config` and validate normalized shape is present and legacy fields remain in payload.
3. Configure gateway with normalized talk shape and confirm clients pick normalized provider config before legacy fallback.
### Expected
- Existing legacy configs continue to work unchanged.
- Normalized provider-agnostic shape is returned and consumed first by clients.
### Actual
- Matches expected in e2e/unit parsing tests and targeted config normalization tests.
## Evidence
Attach at least one:
- [x] Failing test/log before + passing after
- [x] Trace/log snippets
- [ ] Screenshot/recording
- [ ] Perf numbers (if relevant)
## Human Verification (required)
What you personally verified (not just CI), and how:
- Verified scenarios:
- `pnpm test src/config/talk.normalize.test.ts src/config/config-misc.test.ts src/config/schema.hints.test.ts`
- `pnpm test:e2e src/gateway/server.talk-config.e2e.test.ts`
- `pnpm tsgo`
- manual git-history verification that legacy fields predate this PR and are actively consumed on `origin/main`.
- Edge cases checked:
- legacy-only config normalization
- normalized passthrough
- active provider env/default merge behavior
- client parse precedence (normalized first, legacy fallback)
- What you did **not** verify:
- full Android/macOS runtime integration tests on device/simulator in this PR flow.
## Compatibility / Migration
- Backward compatible? (`Yes`)
- Config/env changes? (`Yes`)
- Migration needed? (`No`)
- If yes, exact upgrade steps:
- Optional: start writing normalized `talk.provider` + `talk.providers` now; legacy keys continue to work during phase 1.
## Failure Recovery (if this breaks)
- How to disable/revert this change quickly:
- Revert commit `00d5f4478` from branch `feat/talk-provider-agnostic-config`.
- Files/config to restore:
- `src/config/talk.ts`, `src/gateway/server-methods/talk.ts`, and client talk parsing files in `apps/ios`, `apps/macos`, `apps/android`.
- Known bad symptoms reviewers should watch for:
- Talk clients ignoring configured provider voice/model values.
- Missing key resolution when only legacy fields are present.
## Risks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write `None`.
- Risk:
- Divergence between normalized and legacy values when both are present.
- Mitigation:
- normalization enforces single active provider resolution and response builder mirrors active provider config into legacy compatibility fields during phase 1.
- Risk:
- Client regressions from payload shape transition.
- Mitigation:
- explicit normalized-first + legacy-fallback parsing tests on iOS/macOS/Android.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR adds provider-agnostic talk config structure while maintaining backward compatibility with legacy ElevenLabs-specific fields. The implementation follows a clean migration strategy:
**Backend changes:**
- Introduced normalized `talk.provider` and `talk.providers[provider]` structure in config schema and types
- Added read-time normalization that auto-migrates legacy ElevenLabs fields to normalized structure (defaults to `elevenlabs` provider)
- Gateway responses include both normalized fields and legacy compatibility fields (legacy fields mirror active provider config)
- Environment variable merging (`ELEVENLABS_API_KEY`) only applies when active provider is `elevenlabs`
**Client changes (iOS/macOS/Android):**
- Clients parse normalized provider config first, then fall back to legacy fields when normalized shape absent
- iOS keychain storage migrated to provider-routed keys with automatic legacy key migration on read
- Non-ElevenLabs providers log warnings and fall back to system voice (phase 1 scope boundary)
**Test coverage:**
- Unit tests cover legacy-to-normalized migration, normalized passthrough, and env variable merging
- E2e tests verify gateway response normalization and precedence rules
- Client parsing tests confirm normalized-first, legacy-fallback behavior
The migration is zero-downtime: existing configs continue working unchanged, and the normalized shape is additive.
<h3>Confidence Score: 4/5</h3>
- Safe to merge with thorough testing and well-designed migration strategy
- This is a well-executed phased migration with comprehensive test coverage and backward compatibility. Score of 4 (not 5) reflects minor edge case: multiple providers without explicit `provider` field lacks test coverage and clear behavior specification. Implementation correctly handles the normalized/legacy precedence rules, environment variable scoping, and keychain migration. The scope boundary is clear (phase 1, no runtime provider switching), and the rollback plan is straightforward.
- No files require special attention - the implementation is consistent across backend normalization, gateway responses, and client parsing
<sub>Last reviewed commit: 00d5f44</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#16274: feat(voice): Fix persistent speech errors, silent playback, and feedb…
by ryotsukuda333 · 2026-02-14
73.5%
#20475: fix(macos): resolve 120%+ CPU regression and gateway stability
by teknomage8 · 2026-02-19
72.9%
#14744: fix(context): key MODEL_CACHE by provider/modelId to prevent collis...
by lailoo · 2026-02-12
72.7%
#22086: fix(tts): honor explicit config provider and model/voice settings
by AIflow-Labs · 2026-02-20
72.4%
#23816: fix(agents): model fallback skipped during session overrides and pr...
by ramezgaberiel · 2026-02-22
72.3%
#20212: feat: Add Kilo Gateway provider
by jrf0110 · 2026-02-18
71.8%
#23778: feat: chat UI facelift — speech, themes, config categories, and polish
by BunsDev · 2026-02-22
71.6%
#18852: fix: Voice-call state persistence is fire-and-forget, causing silen...
by coygeek · 2026-02-17
71.5%
#19024: fix: Fix normalise toolid
by chetaniitbhilai · 2026-02-17
71.3%
#17878: Refactor: share allowlist normalization
by iyoda · 2026-02-16
71.2%