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..73dad9207 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 { @@ -380,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({ @@ -391,32 +399,20 @@ 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.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 || !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, - }, - }); - } + if (!chunk.candidates) { continue; } @@ -427,40 +423,47 @@ export class LLMStream extends llm.LLMStream { } const candidate = chunk.candidates[0]; - const finishReason = candidate.finishReason; + if (!candidate) { + continue; + } - let chunksYielded = false; - for (const part of candidate.content!.parts!) { - const chatChunk = this.#parsePart(requestId, part); - if (chatChunk) { - chunksYielded = true; - retryable = false; - this.queue.put(chatChunk); - } + if (candidate.finishReason) { + finishReason = candidate.finishReason; } - if (finishReason === 'STOP' && !chunksYielded && retryable) { + if (candidate.finishReason && BLOCKED_REASONS.includes(candidate.finishReason)) { throw new APIStatusError({ - message: 'Google LLM: no response generated', + message: `Google LLM: generation blocked by Gemini: ${candidate.finishReason}`, options: { - retryable, + retryable: false, 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 (!candidate.content?.parts) { + continue; } + + for (const part of candidate.content.parts) { + const chatChunk = this.#parsePart(requestId, part); + responseGenerated = true; + if (chatChunk) { + retryable = false; + this.queue.put(chatChunk); + } + } + } + + 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) {