Conversation
OpenAIStreamer read model and voice from the tts.openai config section but took
base_url and api_key from the environment. The synchronous path in tts_tool.py
deliberately resolves the opposite way —
base_url = config_base_url or fallback_base or DEFAULT_OPENAI_BASE_URL
# "Config override wins over the auth-chain fallback (restores the
# pre-refactor precedence, where tts.openai.base_url beat the resolved
# default)"
— so a self-hosted OpenAI-compatible TTS server works for ordinary replies and
then streams from api.openai.com, asking it for a local model name in a local
voice. It fails there; with a model name that happens to exist it would succeed
against the wrong provider, in the wrong voice, and be billed.
Apply the same precedence in stream(): configured value first, environment as
the fallback. Resolution is independent per field, so a configured endpoint
with no configured key still picks the key up from the environment, and blank
YAML values do not shadow it.
Fixes NousResearch#73530
|
Superseded by #73512, which landed the same fix with wider scope — That also covers the Apologies for the duplicate — I searched the streaming-TTS issues and PRs but not the |
What does this PR do?
OpenAIStreamerreadsmodelandvoicefrom thetts.openaiconfig section but takesbase_urlandapi_keyfrom the environment. The synchronous path resolves the opposite way, and says so:So
tts.openai.base_urlis honoured forsynthesizeand ignored forstream. A self-hosted OpenAI-compatible TTS server works for ordinary replies and then streams fromapi.openai.com— asking it for a local model name in a local voice. That request fails; with a model name that happens to exist it would instead succeed against the wrong provider, in the wrong voice, and be billed.This applies the synchronous path's precedence in
stream(): configured value first, environment as fallback.Related Issue
Fixes #73530
Type of Change
Changes Made
tools/tts_streaming.py(+15/-4):OpenAIStreamer.stream()resolvesbase_urlandapi_keyfrom the provider's config section first, falling back to the environment. Adds_section_value(), which returns a config value only when it is a non-blank string, so an empty YAML value cannot shadow the environment. Resolution is independent per field.tests/tools/test_tts_streaming.py(+75): four behaviours — configured endpoint wins; nothing configured falls back to env unchanged;base_urlandapi_keyresolve independently; blank values are ignored.Deliberately not changed:
available()still gates onOPENAI_API_KEY. A local server that ignores auth arguably shouldn't need an unrelated cloud key present, but that is a separate behavioural question and is not required to fix the wrong-endpoint bug. Noted in #73530.How to Test
Config with a self-hosted OpenAI-compatible TTS endpoint, and no
OPENAI_BASE_URLin the environment (OPENAI_API_KEYset for the LLM, as is typical):Before: connects to
https://api.openai.com/v1. After: connects to the configured endpoint.The two behaviour tests fail without the change and pass with it.
Platforms tested
macOS 26.5 (Python 3.11), against a self-hosted Qwen3-TTS server on Linux/CUDA. The change is pure config resolution with no platform-specific behaviour.
Full run of the TTS/voice suites: 478 passed. Three failures in
tests/tools/test_voice_mode.py::TestPulseSocketReachableare pre-existing on macOS (PulseAudio sockets) and reproduce identically with this change stashed.Notes
Found while wiring streaming TTS into the
hermes-livekitgateway plugin against a local Qwen3-TTS server. Anything resolving a streamer inherits this, including the gateway consumer in #73358 — that PR callsresolve_streaming_provider(tts_config)and does not touchbase_urlitself, so it is correct as written and simply picks up whichever endpoint this function returns.