Skip to content

Commit 8f23fad

Browse files
authored
fix(google): align realtime behavior for Gemini 3.1 and harden session/tool handling (#1189)
1 parent 50a650b commit 8f23fad

3 files changed

Lines changed: 40 additions & 16 deletions

File tree

.changeset/clever-feet-rush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@livekit/agents-plugin-google": patch
3+
---
4+
5+
fix(google): align realtime behavior for Gemini 3.1 and harden session/tool handling

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type LiveAPIModels =
1919
| 'gemini-live-2.5-flash-preview-native-audio-09-2025' // Public preview https://docs.cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/2-5-flash-live-api#live-2.5-flash-preview
2020
| 'gemini-live-2.5-flash-preview-native-audio' // still works, possibly an alias, but not mentioned in any docs or changelog
2121
// Gemini API models
22+
| 'gemini-3.1-flash-live-preview' // https://ai.google.dev/gemini-api/docs/models/gemini-3.1-flash-live-preview
2223
| 'gemini-2.5-flash-native-audio-preview-12-2025' // https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-live
2324
| 'gemini-2.5-flash-native-audio-preview-09-2025' // https://ai.google.dev/gemini-api/docs/models#gemini-2.5-flash-live
2425
| 'gemini-2.0-flash-exp'; // still works in Gemini API but not VertexAI

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

Lines changed: 34 additions & 16 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',
@@ -684,6 +682,13 @@ export class RealtimeSession extends llm.RealtimeSession {
684682
}
685683

686684
async generateReply(instructions?: string): Promise<llm.GenerationCreatedEvent> {
685+
if (this.options.model === 'gemini-3.1-flash-live-preview') {
686+
this.#logger.warn(
687+
'generateReply is not compatible with gemini-3.1-flash-live-preview and will be ignored.',
688+
);
689+
throw new Error("generateReply is not compatible with 'gemini-3.1-flash-live-preview'");
690+
}
691+
687692
if (this.pendingGenerationFut && !this.pendingGenerationFut.done) {
688693
this.#logger.warn(
689694
'generateReply called while another generation is pending, cancelling previous.',
@@ -992,13 +997,16 @@ export class RealtimeSession extends llm.RealtimeSession {
992997
}
993998
break;
994999
case 'realtime_input':
995-
const { mediaChunks, activityStart, activityEnd, text } = msg.value;
1000+
const { mediaChunks, audio, activityStart, activityEnd, text } = msg.value;
9961001
if (this.pendingToolCallIds.size > 0) break;
9971002
if (mediaChunks) {
9981003
for (const mediaChunk of mediaChunks) {
9991004
await session.sendRealtimeInput({ media: mediaChunk });
10001005
}
10011006
}
1007+
if (audio) {
1008+
await session.sendRealtimeInput({ audio });
1009+
}
10021010
if (text) {
10031011
await session.sendRealtimeInput({ text });
10041012
}
@@ -1152,6 +1160,16 @@ export class RealtimeSession extends llm.RealtimeSession {
11521160
),
11531161
};
11541162
}
1163+
if (obj.type === 'realtime_input' && obj.value?.audio) {
1164+
const ac = obj.value.audio as { mimeType?: string; data?: string };
1165+
obj.value = {
1166+
...obj.value,
1167+
audio: {
1168+
...ac,
1169+
data: typeof ac.data === 'string' ? this.truncateString(ac.data, maxLength) : ac.data,
1170+
},
1171+
};
1172+
}
11551173
return obj;
11561174
}
11571175

@@ -1265,17 +1283,15 @@ export class RealtimeSession extends llm.RealtimeSession {
12651283
},
12661284
languageCode: opts.language,
12671285
},
1268-
tools: [
1269-
{
1270-
functionDeclarations: this.geminiDeclarations,
1271-
...this.options.geminiTools,
1272-
},
1273-
],
1286+
tools:
1287+
this.geminiDeclarations.length > 0 || this.options.geminiTools
1288+
? [{ functionDeclarations: this.geminiDeclarations, ...this.options.geminiTools }]
1289+
: undefined,
12741290
inputAudioTranscription: opts.inputAudioTranscription,
12751291
outputAudioTranscription: opts.outputAudioTranscription,
1276-
sessionResumption: {
1277-
handle: this.sessionResumptionHandle,
1278-
},
1292+
sessionResumption: this.sessionResumptionHandle
1293+
? { handle: this.sessionResumptionHandle }
1294+
: {},
12791295
};
12801296

12811297
// Add generation fields at TOP LEVEL (NO generationConfig!)
@@ -1527,6 +1543,8 @@ export class RealtimeSession extends llm.RealtimeSession {
15271543
}),
15281544
);
15291545
}
1546+
// This closes message/text/audio/function streams so the consumer can continue.
1547+
this.markCurrentGenerationDone();
15301548
}
15311549

15321550
private handleToolCallCancellation(cancellation: types.LiveServerToolCallCancellation): void {

0 commit comments

Comments
 (0)