Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/google-generation-failures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@livekit/agents-plugin-google': patch
---

fix(google): surface Gemini content blocking and empty generation failures
71 changes: 39 additions & 32 deletions plugins/google/src/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -391,35 +396,17 @@ 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)
) {
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,
},
});
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated

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;
}

if (chunk.candidates.length > 1) {
this.logger.warn(
'Google LLM: there are multiple candidates in the response, returning response from the first one.',
Expand All @@ -429,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;
Expand All @@ -439,7 +446,7 @@ export class LLMStream extends llm.LLMStream {
}
}

if (finishReason === 'STOP' && !chunksYielded && retryable) {
if (finishReason === FinishReason.STOP && !chunksYielded) {
throw new APIStatusError({
message: 'Google LLM: no response generated',
options: {
Expand Down
Loading