Skip to content

fix(tools): honour tts.openai.base_url in the streaming path - #73536

Closed
francip wants to merge 1 commit into
NousResearch:mainfrom
kortexa-ai:fix/tts-streaming-base-url
Closed

francip wants to merge 1 commit into
NousResearch:mainfrom
kortexa-ai:fix/tts-streaming-base-url

Conversation

@francip

@francip francip commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

OpenAIStreamer reads model and voice from the tts.openai config section but takes base_url and api_key from the environment. The synchronous path resolves the opposite way, and says so:

config_base_url = oai_config.get("base_url")
if base_url is None:
    # Config override wins over the auth-chain fallback (restores the
    # pre-refactor precedence, where tts.openai.base_url beat the resolved
    # default); the auth-chain value is the last-resort default.
    base_url = config_base_url or fallback_base or DEFAULT_OPENAI_BASE_URL

So tts.openai.base_url is honoured for synthesize and ignored for stream. 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. 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

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • tools/tts_streaming.py (+15/-4): OpenAIStreamer.stream() resolves base_url and api_key from 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_url and api_key resolve independently; blank values are ignored.

Deliberately not changed: available() still gates on OPENAI_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_URL in the environment (OPENAI_API_KEY set for the LLM, as is typical):

tts:
  provider: openai
  openai:
    model: qwen3-tts-customvoice-1.7b
    voice: mira
    base_url: http://<host>:4003/v1
    api_key: dummy-no-auth
from tools.tts_streaming import resolve_streaming_provider
s = resolve_streaming_provider(tts_config)
next(s.stream("hello"))

Before: connects to https://api.openai.com/v1. After: connects to the configured endpoint.

pytest tests/tools/test_tts_streaming.py -v

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::TestPulseSocketReachable are pre-existing on macOS (PulseAudio sockets) and reproduce identically with this change stashed.

Notes

Found while wiring streaming TTS into the hermes-livekit gateway plugin against a local Qwen3-TTS server. Anything resolving a streamer inherits this, including the gateway consumer in #73358 — that PR calls resolve_streaming_provider(tts_config) and does not touch base_url itself, so it is correct as written and simply picks up whichever endpoint this function returns.

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
@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
@francip

francip commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #73512, which landed the same fix with wider scope — OpenAIStreamer now prefers tts.openai.base_url and tts.openai.api_key in both stream() and available(), routed through the shared audio-key resolver, with the equivalent treatment for ElevenLabs.

That also covers the available() gating I deliberately left out of this PR as a separate concern, so there is nothing left here worth carrying. Closing in favour of it.

Apologies for the duplicate — I searched the streaming-TTS issues and PRs but not the base_url/OpenAIStreamer symptom itself, so I missed #70307, which #73512 salvaged. Verified against upstream/main: the merged resolution order fixes the case this PR was opened for (self-hosted OpenAI-compatible TTS endpoint configured in tts.openai.base_url, no OPENAI_BASE_URL in the environment).

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

Development

Successfully merging this pull request may close these issues.

OpenAIStreamer ignores tts.openai.base_url, so streaming TTS escapes to api.openai.com

2 participants