Skip to content

Commit 030ffbb

Browse files
committed
fix(google): keep tool responses for gemini 3.1
1 parent d0e6336 commit 030ffbb

3 files changed

Lines changed: 82 additions & 11 deletions

File tree

.changeset/swift-pandas-juggle.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 Gemini 3.1 realtime tool-response handling during `updateChatCtx`

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
// SPDX-FileCopyrightText: 2026 LiveKit, Inc.
22
//
33
// SPDX-License-Identifier: Apache-2.0
4+
import { llm } from '@livekit/agents';
45
import { describe, expect, it } from 'vitest';
56
import {
67
buildGenerateReplyClientEvents,
78
isRestrictedClientContentModel,
9+
RealtimeModel,
810
supportsServerSideChatContext,
911
} from './realtime_api.js';
12+
import type * as api_proto from './api_proto.js';
1013

1114
describe('Google realtime generateReply compatibility helpers', () => {
1215
it('detects restricted client-content models', () => {
@@ -135,4 +138,59 @@ describe('Google realtime generateReply compatibility helpers', () => {
135138
});
136139
expect(events[1]!.type).toBe('content');
137140
});
141+
142+
it('restricted models still send tool responses from updateChatCtx', async () => {
143+
const session = new RealtimeModel({
144+
apiKey: 'test',
145+
model: 'gemini-3.1-flash-live-preview',
146+
}).session() as unknown as {
147+
activeSession?: unknown;
148+
messageChannel: {
149+
items: api_proto.ClientEvents[];
150+
put(event: api_proto.ClientEvents): Promise<void>;
151+
};
152+
updateChatCtx(chatCtx: llm.ChatContext): Promise<void>;
153+
};
154+
155+
const events: api_proto.ClientEvents[] = [];
156+
Object.defineProperty(session, 'activeSession', {
157+
configurable: true,
158+
get: () => ({}),
159+
set: () => undefined,
160+
});
161+
session.messageChannel.put = async (event) => {
162+
events.push(event);
163+
};
164+
165+
const chatCtx = llm.ChatContext.empty();
166+
chatCtx.insert([
167+
llm.ChatMessage.create({
168+
role: 'assistant',
169+
content: 'The tool finished successfully.',
170+
}),
171+
llm.FunctionCallOutput.create({
172+
callId: 'call_123',
173+
isError: false,
174+
name: 'lookup_weather',
175+
output: '{"temperature_c":21}',
176+
}),
177+
]);
178+
179+
await session.updateChatCtx(chatCtx);
180+
181+
expect(events).toEqual([
182+
{
183+
type: 'tool_response',
184+
value: {
185+
functionResponses: [
186+
{
187+
id: 'call_123',
188+
name: 'lookup_weather',
189+
response: { output: '{"temperature_c":21}' },
190+
},
191+
],
192+
},
193+
},
194+
]);
195+
});
138196
});

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

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
7777
return a.size === b.size && [...a].every((x) => b.has(x));
7878
}
7979

80-
// Ref: python livekit-agents/livekit-plugins/livekit-plugins-google/livekit/plugins/google/realtime/api_proto.py
80+
// Restricted Gemini Live models reject mid-session sendClientContent.
8181
// Gemini 3.1+ rejects mid-session sendClientContent.
8282
// generateReply must use sendRealtimeInput, and the current JS SDK path cannot
8383
// keep chat context synchronized server-side for these models.
@@ -641,16 +641,6 @@ export class RealtimeSession extends llm.RealtimeSession {
641641
}
642642

643643
if (appendCtx.items.length > 0) {
644-
if (!supportsServerSideChatContext(this.options.model)) {
645-
this.#logger.warn(
646-
'updateChatCtx is not currently applied on restricted model ' +
647-
this.options.model +
648-
'. Storing chat context locally only until the JS SDK exposes a supported history path.',
649-
);
650-
this._chatCtx = chatCtx.copy();
651-
return;
652-
}
653-
654644
const [turns] = await appendCtx
655645
.copy({
656646
excludeFunctionCall: true,
@@ -659,6 +649,24 @@ export class RealtimeSession extends llm.RealtimeSession {
659649

660650
const toolResults = this.getToolResultsForRealtime(appendCtx, this.options.vertexai);
661651

652+
if (!supportsServerSideChatContext(this.options.model)) {
653+
if (turns.length > 0) {
654+
this.#logger.warn(
655+
'updateChatCtx is not currently applied on restricted model ' +
656+
this.options.model +
657+
'. Storing chat context locally only until the JS SDK exposes a supported history path.',
658+
);
659+
}
660+
if (toolResults) {
661+
this.sendClientEvent({
662+
type: 'tool_response',
663+
value: toolResults,
664+
});
665+
}
666+
this._chatCtx = chatCtx.copy();
667+
return;
668+
}
669+
662670
if (turns.length > 0) {
663671
const shouldSendRealtimeText = this.pendingInterruptText;
664672

0 commit comments

Comments
 (0)