From dd61d9d24e861c8b442f8c3e864071833e4709d8 Mon Sep 17 00:00:00 2001 From: "rosetta-livekit-bot[bot]" <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 17:31:29 +0000 Subject: [PATCH 1/4] fix(google): surface generation failures --- .changeset/google-generation-failures.md | 5 +++++ plugins/google/src/llm.ts | 19 ++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 .changeset/google-generation-failures.md diff --git a/.changeset/google-generation-failures.md b/.changeset/google-generation-failures.md new file mode 100644 index 000000000..4fba96182 --- /dev/null +++ b/.changeset/google-generation-failures.md @@ -0,0 +1,5 @@ +--- +'@livekit/agents-plugin-google': patch +--- + +fix(google): surface Gemini content blocking and empty generation failures diff --git a/plugins/google/src/llm.ts b/plugins/google/src/llm.ts index 302b679af..2a19eb87e 100644 --- a/plugins/google/src/llm.ts +++ b/plugins/google/src/llm.ts @@ -408,16 +408,13 @@ export class LLMStream extends llm.LLMStream { if (!chunk.candidates || !chunk.candidates[0]?.content?.parts) { this.logger.warn(`No content in the response: ${JSON.stringify(chunk)}`); - if (retryable) { - throw new APIStatusError({ - message: 'Google LLM: no content in the response', - options: { - retryable: true, - requestId, - }, - }); - } - continue; + throw new APIStatusError({ + message: 'Google LLM: no content in the response', + options: { + retryable, + requestId, + }, + }); } if (chunk.candidates.length > 1) { @@ -439,7 +436,7 @@ export class LLMStream extends llm.LLMStream { } } - if (finishReason === 'STOP' && !chunksYielded && retryable) { + if (finishReason === 'STOP' && !chunksYielded) { throw new APIStatusError({ message: 'Google LLM: no response generated', options: { From 1dae91187746daeef978f6f2ee5ee97599c03b1e Mon Sep 17 00:00:00 2001 From: "rosetta-livekit-bot[bot]" <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 23:14:16 +0000 Subject: [PATCH 2/4] fix(google): use SDK finish reasons --- plugins/google/src/llm.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/plugins/google/src/llm.ts b/plugins/google/src/llm.ts index 2a19eb87e..d48d34401 100644 --- a/plugins/google/src/llm.ts +++ b/plugins/google/src/llm.ts @@ -2,7 +2,12 @@ // // SPDX-License-Identifier: Apache-2.0 import type * as types from '@google/genai'; -import { FunctionCallingConfigMode, type GenerateContentConfig, GoogleGenAI } from '@google/genai'; +import { + FinishReason, + FunctionCallingConfigMode, + type GenerateContentConfig, + GoogleGenAI, +} from '@google/genai'; import type { APIConnectOptions } from '@livekit/agents'; import { APIConnectionError, @@ -296,13 +301,13 @@ export class LLM extends llm.LLM { } } -const BLOCKED_REASONS = [ - 'SAFETY', - 'SPII', - 'PROHIBITED_CONTENT', - 'BLOCKLIST', - 'LANGUAGE', - 'RECITATION', +const BLOCKED_REASONS: types.FinishReason[] = [ + FinishReason.SAFETY, + FinishReason.SPII, + FinishReason.PROHIBITED_CONTENT, + FinishReason.BLOCKLIST, + FinishReason.LANGUAGE, + FinishReason.RECITATION, ]; export class LLMStream extends llm.LLMStream { From f4fd2568b9b6d6beba9406a51c09e759b3d3abef Mon Sep 17 00:00:00 2001 From: "rosetta-livekit-bot[bot]" <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 23:18:43 +0000 Subject: [PATCH 3/4] fix(google): align blocked generation handling --- plugins/google/src/llm.ts | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/plugins/google/src/llm.ts b/plugins/google/src/llm.ts index d48d34401..41003a893 100644 --- a/plugins/google/src/llm.ts +++ b/plugins/google/src/llm.ts @@ -396,21 +396,6 @@ export class LLMStream extends llm.LLMStream { }); } - // Check for blocked reasons first — safety-blocked responses often lack content.parts, - // so this must run before the no-content guard to avoid wasting retries. - if ( - chunk.candidates?.[0]?.finishReason && - BLOCKED_REASONS.includes(chunk.candidates[0].finishReason) - ) { - throw new APIStatusError({ - message: `Google LLM: generation blocked - ${chunk.candidates[0].finishReason}`, - options: { - retryable: false, - requestId, - }, - }); - } - if (!chunk.candidates || !chunk.candidates[0]?.content?.parts) { this.logger.warn(`No content in the response: ${JSON.stringify(chunk)}`); throw new APIStatusError({ @@ -431,8 +416,28 @@ export class LLMStream extends llm.LLMStream { const candidate = chunk.candidates[0]; const finishReason = candidate.finishReason; + if (finishReason && BLOCKED_REASONS.includes(finishReason)) { + throw new APIStatusError({ + message: `Google LLM: generation blocked by Gemini: ${finishReason}`, + options: { + retryable: false, + requestId, + }, + }); + } + + if (!candidate.content?.parts) { + throw new APIStatusError({ + message: 'Google LLM: no content in the response', + options: { + retryable, + requestId, + }, + }); + } + let chunksYielded = false; - for (const part of candidate.content!.parts!) { + for (const part of candidate.content.parts) { const chatChunk = this.#parsePart(requestId, part); if (chatChunk) { chunksYielded = true; @@ -441,7 +446,7 @@ export class LLMStream extends llm.LLMStream { } } - if (finishReason === 'STOP' && !chunksYielded) { + if (finishReason === FinishReason.STOP && !chunksYielded) { throw new APIStatusError({ message: 'Google LLM: no response generated', options: { From 43de72efb0b66d9bb8e9221d5a4a96dede430dfc Mon Sep 17 00:00:00 2001 From: "rosetta-livekit-bot[bot]" <282703043+rosetta-livekit-bot[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 17:49:09 +0000 Subject: [PATCH 4/4] fix(google): skip contentless stream chunks --- plugins/google/src/llm.ts | 78 +++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/plugins/google/src/llm.ts b/plugins/google/src/llm.ts index 41003a893..73dad9207 100644 --- a/plugins/google/src/llm.ts +++ b/plugins/google/src/llm.ts @@ -385,6 +385,9 @@ export class LLMStream extends llm.LLMStream { }, }); + let responseGenerated = false; + let finishReason: types.FinishReason | undefined; + for await (const chunk of response) { if (chunk.promptFeedback) { throw new APIStatusError({ @@ -396,17 +399,23 @@ export class LLMStream extends llm.LLMStream { }); } - 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: no content in the response', - options: { - retryable, - requestId, + if (chunk.usageMetadata) { + const usage = chunk.usageMetadata; + this.queue.put({ + id: requestId, + usage: { + completionTokens: usage.candidatesTokenCount || 0, + promptTokens: usage.promptTokenCount || 0, + promptCachedTokens: usage.cachedContentTokenCount || 0, + totalTokens: usage.totalTokenCount || 0, }, }); } + if (!chunk.candidates) { + continue; + } + if (chunk.candidates.length > 1) { this.logger.warn( 'Google LLM: there are multiple candidates in the response, returning response from the first one.', @@ -414,11 +423,17 @@ export class LLMStream extends llm.LLMStream { } const candidate = chunk.candidates[0]; - const finishReason = candidate.finishReason; + if (!candidate) { + continue; + } + + if (candidate.finishReason) { + finishReason = candidate.finishReason; + } - if (finishReason && BLOCKED_REASONS.includes(finishReason)) { + if (candidate.finishReason && BLOCKED_REASONS.includes(candidate.finishReason)) { throw new APIStatusError({ - message: `Google LLM: generation blocked by Gemini: ${finishReason}`, + message: `Google LLM: generation blocked by Gemini: ${candidate.finishReason}`, options: { retryable: false, requestId, @@ -427,47 +442,28 @@ export class LLMStream extends llm.LLMStream { } if (!candidate.content?.parts) { - throw new APIStatusError({ - message: 'Google LLM: no content in the response', - options: { - retryable, - requestId, - }, - }); + continue; } - let chunksYielded = false; for (const part of candidate.content.parts) { const chatChunk = this.#parsePart(requestId, part); + responseGenerated = true; if (chatChunk) { - chunksYielded = true; retryable = false; this.queue.put(chatChunk); } } + } - if (finishReason === FinishReason.STOP && !chunksYielded) { - throw new APIStatusError({ - message: 'Google LLM: no response generated', - options: { - retryable, - requestId, - }, - }); - } - - if (chunk.usageMetadata) { - const usage = chunk.usageMetadata; - this.queue.put({ - id: requestId, - usage: { - completionTokens: usage.candidatesTokenCount || 0, - promptTokens: usage.promptTokenCount || 0, - promptCachedTokens: usage.cachedContentTokenCount || 0, - totalTokens: usage.totalTokenCount || 0, - }, - }); - } + if (!responseGenerated) { + throw new APIStatusError({ + message: 'Google LLM: no response generated', + options: { + body: { finishReason }, + retryable, + requestId, + }, + }); } } catch (error: unknown) { if (error instanceof APIStatusError || error instanceof APIConnectionError) {