Skip to content

fix(google): move generateContentStream inside try/catch in Gemini TTS - #1096

Merged
toubatbrian merged 2 commits into
livekit:mainfrom
dremonkey:fix/gemini-tts-error-handling
Mar 4, 2026
Merged

fix(google): move generateContentStream inside try/catch in Gemini TTS#1096
toubatbrian merged 2 commits into
livekit:mainfrom
dremonkey:fix/gemini-tts-error-handling

Conversation

@dremonkey

@dremonkey dremonkey commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Moves the generateContentStream call inside the existing try/catch block in ChunkedStream.run() (plugins/google/src/beta/gemini_tts.ts).

Before: When the Gemini API rejects a request before streaming (e.g. 429 rate limit), the @google/genai ApiError escapes run() without being converted to a LiveKit APIStatusError. The base class _mainTaskImpl doesn't recognize it as instanceof APIError, so retries are skipped and the error is emitted as non-recoverable.

After: All API errors — whether thrown during request initiation or stream iteration — go through the existing error handling code, get converted to APIStatusError with the correct retryable flag, and are eligible for retry.

This is a one-line structural change (moving the const responseStream = await ... from before try { to inside it). No logic changes.

Problem

When using Gemini TTS (e.g. gemini-2.5-flash-preview-tts) and the API returns a 429 rate limit error, the error is thrown by generateContentStream at line 216 — which sits outside the try/catch block (lines 222-275). The @google/genai ApiError is a different class from LiveKit's APIError, so the base class retry logic in _mainTaskImpl doesn't recognize it:

catch (error) {
    if (error instanceof APIError) {  // FALSE — @google/genai ApiError ≠ LiveKit APIError
        // retry logic (never reached)
    } else {
        this.emitError({ error, recoverable: false });  // always non-recoverable
        throw error;  // unhandled promise rejection
    }
}

Combined with StreamAdapter splitting text into N sentences (each a parallel TTS call), a single quota exhaustion produces N non-recoverable error events — exceeding AgentSession.maxUnrecoverableErrors (default: 3) and killing the session, even though STT, LLM, and text transcription still work.

Relevant log output

ERR AgentSession is closing due to unrecoverable error
    err: {
      "type": "tts_error",
      "error": {
        "status": 429,
        "statusText": "Too Many Requests",
        "errorDetails": [
          { "reason": "RATE_LIMIT_EXCEEDED", "domain": "googleapis.com" }
        ]
      },
      "recoverable": false
    }

Test plan

  • Trigger a 429 rate limit on Gemini TTS → verify the error is caught, converted to APIStatusError({ retryable: true }), and retried by the base class
  • Normal TTS operation unchanged — audio synthesis works as before
  • AbortError handling still works (line 227-229)

🤖 Generated with Claude Code

When the Gemini API rejects a request before streaming begins (e.g. 429
rate limit), the error escapes ChunkedStream.run() without being caught
or converted to an APIStatusError. This bypasses the base class retry
logic and emits non-recoverable errors that can kill the AgentSession.

Moving generateContentStream inside the existing try block ensures all
API errors go through the error handling code path (lines 226-272),
are converted to proper APIStatusError instances with the correct
retryable flag, and are eligible for retry by _mainTaskImpl.

Fixes livekit#1095
@changeset-bot

changeset-bot Bot commented Mar 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 93e3ecd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@livekit/agents-plugin-google Patch
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-xai Patch
@livekit/agents-plugins-test Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

@toubatbrian
toubatbrian merged commit a5bb4ff into livekit:main Mar 4, 2026
4 of 5 checks passed
This was referenced Mar 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants