@ai-sdk/google 4.0.21 → 4.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1079,6 +1079,11 @@ Google realtime models may require provider-specific audio formats, depending
1079
1079
  on the model and modality. See [Realtime](/docs/ai-sdk-core/realtime) for the
1080
1080
  complete setup and tool calling pattern.
1081
1081
 
1082
+ Gemini lifecycle signals that do not have provider-neutral equivalents are
1083
+ emitted as custom events. Inspect `rawType` for `goAway`,
1084
+ `sessionResumptionUpdate`, and `generationComplete` and use the event's `raw`
1085
+ payload for provider-specific details.
1086
+
1082
1087
  ### Live Translation
1083
1088
 
1084
1089
  Use `gemini-3.5-live-translate-preview` for low-latency speech-to-speech
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/google",
3
- "version": "4.0.21",
3
+ "version": "4.0.22",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -337,6 +337,8 @@ export function convertToGoogleMessages(
337
337
  case 'assistant': {
338
338
  systemMessagesAllowed = false;
339
339
 
340
+ let modelResponseHasSignedFunctionCall = false;
341
+
340
342
  contents.push({
341
343
  role: 'model',
342
344
  parts: content
@@ -459,13 +461,27 @@ export function convertToGoogleMessages(
459
461
  providerOpts?.serverToolType != null
460
462
  ? String(providerOpts.serverToolType)
461
463
  : undefined;
464
+ const isServerToolCall =
465
+ serverToolCallId != null && serverToolType != null;
466
+ const shouldSkipMissingSignatureMitigation =
467
+ // Gemini 3 returns a single signature for a parallel
468
+ // function-call response on the first standard function
469
+ // call. Subsequent standard function calls in the same
470
+ // model response legitimately have no signature.
471
+ !isServerToolCall &&
472
+ thoughtSignature == null &&
473
+ modelResponseHasSignedFunctionCall;
462
474
  const effectiveThoughtSignature =
463
475
  thoughtSignature ??
464
- (isGemini3Model
476
+ (isGemini3Model && !shouldSkipMissingSignatureMitigation
465
477
  ? injectSkipSignature(part.toolName)
466
478
  : undefined);
467
479
 
468
- if (serverToolCallId && serverToolType) {
480
+ if (!isServerToolCall && thoughtSignature != null) {
481
+ modelResponseHasSignedFunctionCall = true;
482
+ }
483
+
484
+ if (isServerToolCall) {
469
485
  return {
470
486
  toolCall: {
471
487
  toolType: serverToolType,
@@ -17,6 +17,7 @@ type GoogleRealtimeFunctionCall = {
17
17
  };
18
18
 
19
19
  type GoogleRealtimeServerContent = {
20
+ generationComplete?: boolean;
20
21
  interrupted?: boolean;
21
22
  modelTurn?: {
22
23
  parts?: Array<{
@@ -37,6 +38,12 @@ type GoogleRealtimeWireEvent = {
37
38
  toolCallCancellation?: unknown;
38
39
  serverContent?: GoogleRealtimeServerContent;
39
40
  inputTranscription?: { text?: string };
41
+ goAway?: { timeLeft?: string };
42
+ sessionResumptionUpdate?: {
43
+ newHandle?: string;
44
+ resumable?: boolean;
45
+ lastConsumedClientMessageIndex?: string;
46
+ };
40
47
  };
41
48
 
42
49
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -127,6 +134,22 @@ export class GoogleRealtimeEventMapper {
127
134
  };
128
135
  }
129
136
 
137
+ if (data.goAway != null) {
138
+ return {
139
+ type: 'custom',
140
+ rawType: 'goAway',
141
+ raw,
142
+ };
143
+ }
144
+
145
+ if (data.sessionResumptionUpdate != null) {
146
+ return {
147
+ type: 'custom',
148
+ rawType: 'sessionResumptionUpdate',
149
+ raw,
150
+ };
151
+ }
152
+
130
153
  if (data.serverContent != null) {
131
154
  return this.parseServerContent(data.serverContent, raw);
132
155
  }
@@ -203,6 +226,17 @@ export class GoogleRealtimeEventMapper {
203
226
  });
204
227
  }
205
228
 
229
+ // `generationComplete` means generation has stopped, but playback and the
230
+ // turn can remain open. Keep it distinct from `response-done`, which is
231
+ // emitted only when Google sends `turnComplete`.
232
+ if (serverContent.generationComplete) {
233
+ events.push({
234
+ type: 'custom',
235
+ rawType: 'generationComplete',
236
+ raw,
237
+ });
238
+ }
239
+
206
240
  if (serverContent.turnComplete) {
207
241
  if (this.hasAudio) {
208
242
  events.push({