Skip to content

fix(google): handle content blocking and generation failures#1609

Open
rosetta-livekit-bot[bot] wants to merge 3 commits into
mainfrom
fix-google-generation-failures
Open

fix(google): handle content blocking and generation failures#1609
rosetta-livekit-bot[bot] wants to merge 3 commits into
mainfrom
fix-google-generation-failures

Conversation

@rosetta-livekit-bot
Copy link
Copy Markdown
Contributor

@rosetta-livekit-bot rosetta-livekit-bot Bot commented May 26, 2026

Ported from python.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 26, 2026

🦋 Changeset detected

Latest commit: f4fd256

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

This PR includes changesets to release 33 packages
Name Type
@livekit/agents-plugin-google Patch
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugin-trugen 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

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment thread plugins/google/src/llm.ts Outdated
Comment on lines 439 to 442
if (finishReason === 'STOP' && !chunksYielded) {
throw new APIStatusError({
message: 'Google LLM: no response generated',
options: {
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot May 26, 2026

Choose a reason for hiding this comment

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

🔴 Removing && retryable guard causes spurious error on final streaming chunk after content was already yielded

The chunksYielded variable is local to each iteration of the for await loop (reset to false on every chunk). Previously, the && retryable condition prevented this check from firing when earlier chunks had already successfully yielded content (which sets retryable = false). Now, if the final streaming chunk has finishReason: 'STOP' but its parts are not parseable by #parsePart (e.g., empty parts array [], or parts containing only executableCode/codeExecutionResult/inlineData which return null from #parsePart at plugins/google/src/llm.ts:532-534), the code throws a non-retryable APIStatusError — discarding all previously-yielded content from the stream and failing the entire LLM request.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread plugins/google/src/llm.ts
Comment on lines +411 to +417
throw new APIStatusError({
message: 'Google LLM: no content in the response',
options: {
retryable,
requestId,
},
});
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot May 26, 2026

Choose a reason for hiding this comment

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

🔴 Always-throw on missing content destroys valid streaming responses when a later chunk lacks candidates

The old code used continue when retryable was false (meaning content had already been successfully yielded), gracefully skipping chunks without candidates[0].content.parts. The new code unconditionally throws. In a multi-chunk streaming response, if an intermediate or final chunk arrives without candidates or without content.parts (e.g., a metadata-only chunk), but earlier chunks already put valid content into the queue (setting retryable = false), the new code throws a non-retryable APIStatusError — aborting a response that was otherwise completing successfully. The usageMetadata at line 449 that follows this check would also never be reached for such chunks.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread plugins/google/src/llm.ts
Comment on lines +399 to 408
if (!chunk.candidates || !chunk.candidates[0]?.content?.parts) {
this.logger.warn(`No content in the response: ${JSON.stringify(chunk)}`);
throw new APIStatusError({
message: `Google LLM: generation blocked - ${chunk.candidates[0].finishReason}`,
message: 'Google LLM: no content in the response',
options: {
retryable: false,
retryable,
requestId,
},
});
}
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.

🔴 Safety-blocked responses are misidentified as retryable "no content" errors due to check reordering

The old code explicitly checked for blocked finish reasons (SAFETY, SPII, etc.) before the no-content guard, with a comment explaining why: "safety-blocked responses often lack content.parts, so this must run before the no-content guard to avoid wasting retries." The new code reverses this order — the no-content check at line 399 (!chunk.candidates || !chunk.candidates[0]?.content?.parts) now runs first. When Gemini blocks a response for safety and the candidate has no content.parts (which the old comment says is the common case), the no-content guard fires and throws with retryable: true (since no chunks have been yielded yet). The blocked-reason check at line 419 is never reached.

This causes two problems:

  1. The framework retries the same blocked prompt up to maxRetry times (see agents/src/llm/llm.ts:163-216), wasting time and API calls on a prompt that will always be blocked.
  2. The eventual error message says "no content in the response" instead of "generation blocked by Gemini: SAFETY", hiding the actual cause from users.

The second guard at line 429 (!candidate.content?.parts) is also dead code — if it's reached, the guard at line 399 already guaranteed content.parts is truthy.

Prompt for agents
The root cause is that the no-content check at line 399-408 runs before the blocked-reason check at line 419-427, which means safety-blocked responses without content.parts are caught by the no-content guard and incorrectly marked as retryable.

The fix is to restore the original check order: check for blocked finish reasons BEFORE checking for missing content. This ensures safety-blocked responses are immediately thrown as non-retryable with the correct error message.

Specifically, in the `run()` method of `LLMStream` (plugins/google/src/llm.ts), the blocked-reason check (currently lines 419-427) should be moved above the no-content check (currently lines 399-408). The blocked-reason check needs to use optional chaining since candidates may not exist: `chunk.candidates?.[0]?.finishReason` (as it was in the old code).

Additionally, the second no-content guard at lines 429-437 (`if (!candidate.content?.parts)`) is dead code because the guard at line 399 already ensures `content.parts` is truthy whenever execution reaches that point. Consider whether this guard is still needed or if it should be merged with the first one.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant