Hi team,
I think there’s a bug in the OpenAI TTS plugin that breaks any OpenAI-compatible backend. Two people have hit it on the forum without anyone finding the cause, so I dug into it.
The plugin decides how to read the response based on the model name rather than what the server actually sends back. In tts.py:
AUDIO_STREAM_MODELS = {"tts-1", "tts-1-hd"}
def synthesize(self, text, *, conn_options=DEFAULT_API_CONNECT_OPTIONS):
if self._opts.model in AUDIO_STREAM_MODELS:
return AudioChunkedStream(tts=self, input_text=text, conn_options=conn_options)
return SSEChunkedStream(tts=self, input_text=text, conn_options=conn_options)
Anything that isn’t tts-1 or tts-1-hd goes to SSEChunkedStream, which sends stream_format=“sse” and only reads lines starting with data: . But stream_format is specific to OpenAI. Other servers just ignore the unknown field and return normal MP3 or WAV bytes, so no line ever matches and push() never gets called.
From there pushed_duration() is 0, so the emitter raises no audio frames were pushed for text: … Since APIError defaults to retryable=True and max_retry is 3, the provider gets called four times for a single sentence. All four succeed and get billed, and then the turn fails anyway. The HTTP response is a clean 200 the whole time, which is why it looks like a provider problem.
Worth noting that DEFAULT_MODEL is gpt-4o-mini-tts, so the default also lands on the SSE path. Right now the only way to reach the working branch is to name the model tts-1 or tts-1-hd.
To reproduce:
from livekit.plugins.openai import TTS
tts = TTS(
base_url="https://api.deepinfra.com/v1",
api_key="<key>",
model="hexgrad/Kokoro-82M",
voice="af_bella",
)
Any OpenAI-compatible server does the same thing (DeepInfra, Kokoro-FastAPI, vLLM, LM Studio, Speaches). A plain curl to the same endpoint returns valid
audio.
Reported before, cause never found:
- OpenAI TTS Plugin Returns “no audio frames were pushed” with OpenAI-Compatible Endpoints (Kokoro + DeepInfra) (June, closed without an answer)
- LiveKit OpenAI Plugin with DeepInfra Backend: API Returns Usage Logs but No Audio Output (August)
Confirmed on the current release, 1.6.9.
Would it make sense to pick the parser from the response Content-Type instead of the model name? Or simply expose stream_format as a constructor argument, so people using other backends can pass stream_format=“audio”. Either would fix it. Happy to open a PR if you’d like.
For anyone stuck in the meantime:
class CompatTTS(TTS):
def synthesize(self, text, *, conn_options=DEFAULT_API_CONNECT_OPTIONS):
return AudioChunkedStream(tts=self, input_text=text, conn_options=conn_options)