Skip to content

Commit e937ec9

Browse files
committed
fix(google): add Gemini 3.1 Flash Live compatibility
1 parent 396d702 commit e937ec9

3 files changed

Lines changed: 39 additions & 46 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@livekit/agents-plugin-google': patch
3+
---
4+
5+
fix: compatibility with gemini-3.1-flash-live-preview (#1179)
6+
7+
Two paths were broken with Gemini 3.1:
8+
9+
1. **`pushAudio()`** — was sending `mediaChunks` (deprecated); now sends the `audio` field directly, which Gemini 3.1 requires.
10+
11+
2. **`generateReply()`** — was using `sendClientContent` to trigger generation; Gemini 3.1 rejects this with "Request contains an invalid argument." Now uses `sendRealtimeInput({ text })` instead, which works across all Live API models.
12+
13+
Also fixes spurious empty `tools` and `sessionResumption` fields being sent in the session setup config when they have no values, which could cause rejections on strict model versions.

plugins/google/src/beta/realtime/api_proto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type LiveAPIModels =
2121
// Gemini API models
2222
| 'gemini-2.5-flash-native-audio-preview-12-2025' // https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-live
2323
| 'gemini-2.5-flash-native-audio-preview-09-2025' // https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-live
24+
| 'gemini-3.1-flash-live-preview' // https://ai.google.dev/gemini-api/docs/models
2425
| 'gemini-2.0-flash-exp'; // still works in Gemini API but not VertexAI
2526

2627
/**

plugins/google/src/beta/realtime/realtime_api.ts

Lines changed: 25 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -660,12 +660,10 @@ export class RealtimeSession extends llm.RealtimeSession {
660660
for (const f of this.resampleAudio(frame)) {
661661
for (const nf of this.bstream.write(f.data.buffer as ArrayBuffer)) {
662662
const realtimeInput: types.LiveClientRealtimeInput = {
663-
mediaChunks: [
664-
{
665-
mimeType: 'audio/pcm',
666-
data: Buffer.from(nf.data.buffer).toString('base64'),
667-
},
668-
],
663+
audio: {
664+
mimeType: 'audio/pcm',
665+
data: Buffer.from(nf.data.buffer).toString('base64'),
666+
},
669667
};
670668
this.sendClientEvent({
671669
type: 'realtime_input',
@@ -704,26 +702,12 @@ export class RealtimeSession extends llm.RealtimeSession {
704702
this.inUserActivity = false;
705703
}
706704

707-
// Gemini requires the last message to end with user's turn
708-
// so we need to add a placeholder user turn in order to trigger a new generation
709-
const turns: types.Content[] = [];
710-
if (instructions !== undefined) {
711-
turns.push({
712-
parts: [{ text: instructions }],
713-
role: 'model',
714-
});
715-
}
716-
turns.push({
717-
parts: [{ text: '.' }],
718-
role: 'user',
719-
});
720-
705+
// Trigger generation via realtime text input.
706+
// Gemini 3.1+ rejects sendClientContent — sendRealtimeInput({ text }) works on all Live models.
707+
// Instructions are included in the text so the model uses them as context for the next reply.
721708
this.sendClientEvent({
722-
type: 'content',
723-
value: {
724-
turns,
725-
turnComplete: true,
726-
},
709+
type: 'realtime_input',
710+
value: { text: instructions ?? '.' },
727711
});
728712

729713
const timeoutHandle = setTimeout(() => {
@@ -992,12 +976,10 @@ export class RealtimeSession extends llm.RealtimeSession {
992976
}
993977
break;
994978
case 'realtime_input':
995-
const { mediaChunks, activityStart, activityEnd, text } = msg.value;
979+
const { audio, activityStart, activityEnd, text } = msg.value;
996980
if (this.pendingToolCallIds.size > 0) break;
997-
if (mediaChunks) {
998-
for (const mediaChunk of mediaChunks) {
999-
await session.sendRealtimeInput({ media: mediaChunk });
1000-
}
981+
if (audio) {
982+
await session.sendRealtimeInput({ audio });
1001983
}
1002984
if (text) {
1003985
await session.sendRealtimeInput({ text });
@@ -1141,15 +1123,14 @@ export class RealtimeSession extends llm.RealtimeSession {
11411123
maxLength: number = 30,
11421124
): Record<string, unknown> {
11431125
const obj: any = { ...event };
1144-
if (obj.type === 'realtime_input' && obj.value?.mediaChunks) {
1126+
if (obj.type === 'realtime_input' && obj.value?.audio) {
1127+
const mc = obj.value.audio as { mimeType?: string; data?: string };
11451128
obj.value = {
11461129
...obj.value,
1147-
mediaChunks: (obj.value.mediaChunks as Array<{ mimeType?: string; data?: string }>).map(
1148-
(mc) => ({
1149-
...mc,
1150-
data: typeof mc.data === 'string' ? this.truncateString(mc.data, maxLength) : mc.data,
1151-
}),
1152-
),
1130+
audio: {
1131+
...mc,
1132+
data: typeof mc.data === 'string' ? this.truncateString(mc.data, maxLength) : mc.data,
1133+
},
11531134
};
11541135
}
11551136
return obj;
@@ -1265,17 +1246,15 @@ export class RealtimeSession extends llm.RealtimeSession {
12651246
},
12661247
languageCode: opts.language,
12671248
},
1268-
tools: [
1269-
{
1270-
functionDeclarations: this.geminiDeclarations,
1271-
...this.options.geminiTools,
1272-
},
1273-
],
1249+
tools:
1250+
this.geminiDeclarations.length > 0 || this.options.geminiTools
1251+
? [{ functionDeclarations: this.geminiDeclarations, ...this.options.geminiTools }]
1252+
: undefined,
12741253
inputAudioTranscription: opts.inputAudioTranscription,
12751254
outputAudioTranscription: opts.outputAudioTranscription,
1276-
sessionResumption: {
1277-
handle: this.sessionResumptionHandle,
1278-
},
1255+
sessionResumption: this.sessionResumptionHandle
1256+
? { handle: this.sessionResumptionHandle }
1257+
: undefined,
12791258
};
12801259

12811260
// Add generation fields at TOP LEVEL (NO generationConfig!)

0 commit comments

Comments
 (0)