From 409e76281020dca6265becdebe7dbd4727dcf637 Mon Sep 17 00:00:00 2001 From: BSnizND <199837910+BsnizND@users.noreply.github.com> Date: Sun, 26 Apr 2026 16:35:54 -0700 Subject: [PATCH] Fix Google Live tool response names --- .../google/realtime-voice-provider.test.ts | 24 +++++++++++++++++++ extensions/google/realtime-voice-provider.ts | 16 +++++++++++++ 2 files changed, 40 insertions(+) diff --git a/extensions/google/realtime-voice-provider.test.ts b/extensions/google/realtime-voice-provider.test.ts index 4656e956214..be99695e981 100644 --- a/extensions/google/realtime-voice-provider.test.ts +++ b/extensions/google/realtime-voice-provider.test.ts @@ -385,9 +385,33 @@ describe("buildGoogleRealtimeVoiceProvider", () => { functionResponses: [ { id: "call-1", + name: "lookup", response: { result: "ok" }, }, ], }); }); + + it("does not send malformed Live API tool responses without a matching call name", async () => { + const provider = buildGoogleRealtimeVoiceProvider(); + const onError = vi.fn(); + const bridge = provider.createBridge({ + providerConfig: { apiKey: "gemini-key" }, + onAudio: vi.fn(), + onClearAudio: vi.fn(), + onError, + }); + + await bridge.connect(); + + bridge.submitToolResult("missing-call", { result: "ok" }); + + expect(session.sendToolResponse).not.toHaveBeenCalled(); + expect(onError).toHaveBeenCalledWith( + expect.objectContaining({ + message: + "Google Live function response is missing a matching function call for missing-call", + }), + ); + }); }); diff --git a/extensions/google/realtime-voice-provider.ts b/extensions/google/realtime-voice-provider.ts index 83333ce6b52..8a8ccfae708 100644 --- a/extensions/google/realtime-voice-provider.ts +++ b/extensions/google/realtime-voice-provider.ts @@ -314,6 +314,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge { private sessionReadyFired = false; private consecutiveSilenceMs = 0; private audioStreamEnded = false; + private pendingFunctionNames = new Map(); constructor(private readonly config: GoogleRealtimeVoiceBridgeConfig) {} @@ -323,6 +324,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge { this.sessionReadyFired = false; this.consecutiveSilenceMs = 0; this.audioStreamEnded = false; + this.pendingFunctionNames.clear(); const ai = createGoogleGenAI({ apiKey: this.config.apiKey, @@ -375,6 +377,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge { onclose: () => { this.connected = false; this.sessionConfigured = false; + this.pendingFunctionNames.clear(); const reason = this.intentionallyClosed ? "completed" : "error"; this.session = null; this.config.onClose?.(reason); @@ -449,10 +452,21 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge { if (!this.session) { return; } + const name = this.pendingFunctionNames.get(callId); + if (!name) { + this.config.onError?.( + new Error( + `Google Live function response is missing a matching function call for ${callId}`, + ), + ); + return; + } + this.pendingFunctionNames.delete(callId); this.session.sendToolResponse({ functionResponses: [ { id: callId, + name, response: result && typeof result === "object" ? (result as Record) @@ -471,6 +485,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge { this.pendingAudio = []; this.consecutiveSilenceMs = 0; this.audioStreamEnded = false; + this.pendingFunctionNames.clear(); const session = this.session; this.session = null; session?.close(); @@ -557,6 +572,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge { continue; } const callId = call.id?.trim() || `google-live-${randomUUID()}`; + this.pendingFunctionNames.set(callId, name); this.config.onToolCall?.({ itemId: callId, callId,