Symptom
FallbackSynthesizeStream throws APIConnectionError("TTS stream completed but no audio was received") and triggers markUnAvailable + cascade to the next provider on turns where the LLM emits zero spoken text — e.g., a turn that only contains a tool/function call.
In production this manifests as recurring spurious failover storms (and skewed provider availability metrics) on agents that use tool calls.
Root cause
In agents/src/tts/fallback_adapter.ts, forwardBufferToTTS may consume the LLM stream and forward only sentinel tokens (FLUSH_SENTINEL, END_OF_STREAM) without ever calling stream.pushText(...). The provider correctly returns no audio because nothing was sent to synthesize. The adapter then hits if (!sawRawAudio) and throws, which:
- marks the first provider unavailable,
- retries through the rest of the fallback chain,
- ultimately raises
all TTS instances failed,
- so a benign tool-only turn looks like a multi-provider outage.
Repro
Run an agent with a FallbackAdapter([ttsA, ttsB]) and any LLM/flow that produces a turn whose content is a tool call with no text field. The first synth attempt throws "no audio received"; the second does the same; the run errors with "all TTS instances failed".
Proposed fix
Track whether any text token was actually pushed inside forwardBufferToTTS. In the !sawRawAudio branch, if zero text tokens were ever forwarded, treat it as a clean no-op turn:
- emit
SynthesizeStream.END_OF_STREAM downstream,
- await the input LLM stream read,
- return without throwing.
Keep the existing behavior (throw + failover) for the real failure case (text was sent, no audio came back).
Affected version
@livekit/agents@1.4.0 (and earlier 1.3.x — same code path).
Symptom
FallbackSynthesizeStreamthrowsAPIConnectionError("TTS stream completed but no audio was received")and triggersmarkUnAvailable+ cascade to the next provider on turns where the LLM emits zero spoken text — e.g., a turn that only contains a tool/function call.In production this manifests as recurring spurious failover storms (and skewed provider availability metrics) on agents that use tool calls.
Root cause
In
agents/src/tts/fallback_adapter.ts,forwardBufferToTTSmay consume the LLM stream and forward only sentinel tokens (FLUSH_SENTINEL,END_OF_STREAM) without ever callingstream.pushText(...). The provider correctly returns no audio because nothing was sent to synthesize. The adapter then hitsif (!sawRawAudio)and throws, which:all TTS instances failed,Repro
Run an agent with a
FallbackAdapter([ttsA, ttsB])and any LLM/flow that produces a turn whose content is a tool call with notextfield. The first synth attempt throws "no audio received"; the second does the same; the run errors with "all TTS instances failed".Proposed fix
Track whether any text token was actually pushed inside
forwardBufferToTTS. In the!sawRawAudiobranch, if zero text tokens were ever forwarded, treat it as a clean no-op turn:SynthesizeStream.END_OF_STREAMdownstream,Keep the existing behavior (throw + failover) for the real failure case (text was sent, no audio came back).
Affected version
@livekit/agents@1.4.0(and earlier 1.3.x — same code path).