Skip to content

fix(tts): honor config.yaml endpoints across all providers + bounded upstream buffering - #73512

Merged
teknium1 merged 8 commits into
mainfrom
fix/voice-tts-config-buffering
Jul 28, 2026
Merged

teknium1 merged 8 commits into
mainfrom
fix/voice-tts-config-buffering

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

fix(tts): honor config.yaml endpoints/keys across providers + bound upstream buffering and model caches

Consolidated salvage of five contributor PRs. One theme: TTS providers should honor config.yaml endpoints/keys the same way everywhere (class-level base_url parity), plus two bounded-memory fixes.

Config endpoints / keys (fixes #26175)

tts.openai.api_key + base_url from config.yaml — the resolver was env-only (VOICE_TOOLS_OPENAI_KEY/OPENAI_API_KEY) and ignored the tts.openai block. This was first reported and fixed by @zccyman in #26209 (earliest submitter — thank you!) and independently by @LeonSGP43 in #26233; the cherry-picked commit carries the #26233 diff (it shipped with tests) rebased onto the current 3-tuple _resolve_openai_audio_client_config (is_managed flag, post-#73072 layout). Resolution order now mirrors the STT resolver: config → env (still honoring config base_url) → managed gateway. _has_openai_audio_backend also counts a config key as an available backend.

OpenAI streaming path (salvaged from #70307, @aml1973) — OpenAIStreamer read env only; it now uses the shared audio-key resolver and prefers tts.openai.base_url. Follow-up commit completes parity: the streamer also honors tts.openai.api_key in both available() and stream().

Configurable ElevenLabs URLs (salvaged from #66311, @moeadham) — tts.elevenlabs.base_url (+ optional wss_url, derived from base_url when omitted) routes both the sync path and the chunked ElevenLabsStreamer through an ElevenLabsEnvironment, matching the STT side's ELEVENLABS_STT_BASE_URL/config pattern.

Class-level base_url parity audit — swept every cloud provider section: xAI, MiniMax, Gemini, OpenAI and DeepInfra already honored tts.<provider>.base_url; ElevenLabs and Mistral were the gaps. Mistral now passes tts.mistral.base_url as the SDK server_url. Every cloud TTS provider section now supports base_url consistently.

Bounded buffering (fixes #55171)

Bound upstream response bodies (salvaged from #55177, @ooiuuii) — the requests-based providers (xAI, MiniMax, Gemini) buffered the entire upstream body via response.content/response.json() with no cap. New _read_tts_response_bytes() streams with a hard 16 MiB cap (64 KiB chunks) and closes the response; JSON/file-write helpers layer on it. A hostile/broken endpoint can no longer OOM the process.

Bound the Piper/KittenTTS model caches (salvaged from #62977, @Vissirexa — TTS half only; the hindsight turn-buffer half is a different subsystem and was deliberately dropped) — _piper_voice_cache / _kittentts_model_cache had no eviction and each entry is a whole loaded model (tens of MB). New _tts_cache_get_or_load() runs them through a small LRU (_TTS_MODEL_CACHE_MAX=3) with recency refresh on hit.

Commits (contributor authorship preserved)

Commit Author Salvaged from
cd7bde218f @LeonSGP43 #26233 (dupe of earlier #26209 by @zccyman)
e6d631177d @aml1973 #70307
2290272b0d ours follow-up: streaming honors config api_key
851f75b7e2 @moeadham #66311
ba055b3a80 ours base_url parity audit (Mistral) + provider config tests
c6087a8981 @ooiuuii #55177
15673dfce7 @Vissirexa #62977 (model-cache half)
bd4744cfbc ours contributor email mappings

Tests

  • tests/tools/test_tts_openai_config.py (6) — config/env/gateway resolution order, error message, backend availability
  • tests/tools/test_tts_streaming.py (+3) — streamer honors config base_url and api_key; availability reflects config key
  • tests/tools/test_tts_provider_base_urls.py (5) — ElevenLabs environment plumbing (incl. derived wss), Mistral server_url passthrough/omission
  • tests/tools/test_tts_response_body_cap.py (10) — cap enforcement, streaming reads, JSON fallback
  • tests/tools/test_tts_model_cache_lru.py (3) — load/hit/evict/LRU-recency

All 65 targeted tests pass (pytest -o addopts="" -q tests/tools/test_tts_*); sabotage-verified (disabling the config branch fails the config tests). The two test_tts_xai_speech_tags failures under a combined -k tts run reproduce identically on origin/main (pre-existing test-isolation flake, not introduced here).

Post-merge

Infographic

fix(tts): honor config.yaml endpoints across all providers + bounded upstream buffering

LeonSGP43 and others added 8 commits July 28, 2026 09:24
Salvaged from PR #26233 (@LeonSGP43), rebased onto the current 3-tuple
_resolve_openai_audio_client_config (is_managed flag, post-#73072 layout).
Same fix independently submitted earlier in PR #26209 (@zccyman) — credit
to both.

Resolution order now mirrors the STT resolver: tts.openai.api_key/base_url
from config.yaml -> VOICE_TOOLS_OPENAI_KEY/OPENAI_API_KEY env (still
honoring config base_url) -> managed gateway. _has_openai_audio_backend
also counts a config api_key as an available backend.

Fixes #26175
Use the shared OpenAI audio key resolver and prefer tts.openai.base_url over the global environment fallback in the Desktop streaming path. Add focused regression coverage for credential and endpoint propagation.
Follow-up to salvaged PR #70307 (@aml1973): OpenAIStreamer now checks
tts.openai.api_key (config.yaml) ahead of the env resolver in both
available() and stream(), completing config parity between the sync
and streaming OpenAI TTS paths.
Salvaged from PR #66311 (@moeadham), rebased onto the current streaming
registry. tts.elevenlabs.base_url (+ optional wss_url, derived from
base_url when omitted) routes both the sync ElevenLabs path and the
chunked ElevenLabsStreamer through an ElevenLabsEnvironment, matching
the STT side's ELEVENLABS_STT_BASE_URL/config override pattern.
…g tests

Class-level sweep following the ElevenLabs salvage (#66311): every cloud
TTS provider section now honors tts.<provider>.base_url. xAI, MiniMax,
Gemini, OpenAI and DeepInfra already did; Mistral (SDK server_url) was
the remaining gap. Adds per-provider config tests locking in the
ElevenLabs environment plumbing and the Mistral server_url passthrough.
Salvaged from PR #62977 (@Vissirexa) — TTS model-cache half only (the
hindsight turn-buffer half is a different subsystem and was dropped).

_piper_voice_cache and _kittentts_model_cache were keyed by voice/model
with no eviction, and each entry is a whole loaded model (tens of MB).
A surface that sweeps voices pinned one model per voice for the process
lifetime. New _tts_cache_get_or_load() get-or-loads through a small LRU
(_TTS_MODEL_CACHE_MAX=3), refreshing recency on a hit and evicting the
least-recently-used model on a cold miss.
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on bd4744c

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 visual diff.

inline evidence upload failed.

Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso)

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/tools Tool registry, model_tools, toolsets tool/tts Text-to-speech and transcription labels Jul 28, 2026
@teknium1
teknium1 merged commit f3cc2bc into main Jul 28, 2026
41 checks passed
@teknium1
teknium1 deleted the fix/voice-tts-config-buffering branch July 28, 2026 18:54
francip added a commit to kortexa-ai/hermes-livekit that referenced this pull request Jul 28, 2026
NousResearch/hermes-agent#73512 landed the same fix with wider scope:
OpenAIStreamer now prefers tts.openai.base_url and tts.openai.api_key in both
stream() and available(), via the shared audio-key resolver, and ElevenLabs got
the equivalent treatment.

Verified against the updated checkout: resolve_streaming_provider() returns the
stock OpenAIStreamer and opens http://<host>:4003/v1 with no patch installed.
Keeping ours would have shadowed the upstream implementation for no benefit,
since both register under "openai" and ours installed at adapter init.

Our issue #73530 and PR #73536 are closed as superseded.
meltforce added a commit to meltforce/hermes-agent that referenced this pull request Aug 28, 2026
Every other STT provider reads a base URL from its own config block —
openai (`_resolve_openai_stt_client_config`), xai, elevenlabs and
deepinfra all do. `_transcribe_mistral` was the exception: it built
`Mistral(api_key=api_key)` with no `server_url`, so `stt.provider:
mistral` could only ever address api.mistral.ai.

The TTS half already has this. NousResearch#73512 introduced what its description
calls "class-level base_url parity" for TTS providers, and
`_generate_mistral_tts` has read `tts.mistral.base_url` into the SDK's
`server_url` since. This is the same change for the transcription side,
in the shape `_transcribe_elevenlabs` uses: config block, then env, then
the module constant.

Unset, no `server_url` is passed at all and the SDK keeps its own
endpoint, so an install that does not configure this behaves exactly as
before.

Why it matters: a self-hosted gateway or authenticating reverse proxy in
front of Mistral is reachable for TTS and unreachable for STT, which
makes the provider pair unusable in that deployment for no reason
visible from the configuration.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

7 participants