@ai-sdk/google 4.0.83 → 4.0.85

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.
@@ -135,10 +135,9 @@ The following optional provider options are available for Google models:
135
135
 
136
136
  Optional. Enable structured output. Default is true.
137
137
 
138
- This is useful when the JSON Schema contains elements that are
139
- not supported by the OpenAPI schema version that
140
- Google uses. You can use this to disable
141
- structured outputs if you need to.
138
+ You can disable structured outputs when a schema is not supported or exceeds
139
+ Google's schema complexity limits. The model will still be instructed to
140
+ return JSON, but the schema will not be sent to Google.
142
141
 
143
142
  See [Troubleshooting: Schema Limitations](#schema-limitations) for more details.
144
143
 
@@ -995,14 +994,21 @@ Example response excerpt:
995
994
 
996
995
  #### Schema Limitations
997
996
 
998
- The Google Generative AI API uses a subset of the OpenAPI 3.0 schema,
999
- which does not support features such as unions.
1000
- The errors that you get in this case look like this:
997
+ The Google Generative AI API supports a subset of JSON Schema. Google notes that
998
+ [very large or deeply nested schemas may be rejected](https://ai.google.dev/gemini-api/docs/structured-output#limitations).
999
+ When a schema exceeds Google's complexity limit, the API may return the following
1000
+ generic error:
1001
1001
 
1002
- `GenerateContentRequest.generation_config.response_schema.properties[occupation].type: must be specified`
1002
+ `Request contains an invalid argument.`
1003
+
1004
+ You can work around this by simplifying the schema sent to Google. For example,
1005
+ remove unnecessary constraints or reduce nesting while preserving the fields
1006
+ and types the model needs to generate. If you need stricter client-side
1007
+ validation than the schema sent to Google, use [`jsonSchema`](/docs/reference/ai-sdk-core/json-schema)
1008
+ with a custom validation function or validate the generated output separately.
1003
1009
 
1004
1010
  By default, structured outputs are enabled (and for tool calling they are required).
1005
- You can disable structured outputs for object generation as a workaround:
1011
+ You can also disable provider-enforced structured outputs for object generation:
1006
1012
 
1007
1013
  ```ts highlight="3,8"
1008
1014
  const { output } = await generateText({
@@ -1032,10 +1038,9 @@ const { output } = await generateText({
1032
1038
  });
1033
1039
  ```
1034
1040
 
1035
- The following Zod features are known to not work with Google:
1036
-
1037
- - `z.union`
1038
- - `z.record`
1041
+ When `structuredOutputs` is `false`, Google still receives the JSON response MIME type
1042
+ but not the schema. Describe the required schema in your prompt. AI SDK
1043
+ will still validate the response against the schema in `Output.object`.
1039
1044
 
1040
1045
  ### Model Capabilities
1041
1046
 
@@ -1147,6 +1152,18 @@ emitted as custom events. Inspect `rawType` for `goAway`,
1147
1152
  `sessionResumptionUpdate`, and `generationComplete` and use the event's `raw`
1148
1153
  payload for provider-specific details.
1149
1154
 
1155
+ With input audio transcription enabled, `parseServerEvent` emits
1156
+ `input-transcription-completed` events with a stable `itemId` for each user
1157
+ utterance. Each `transcript` contains the accumulated text so far. If you consume
1158
+ these events directly, replace the text for that `itemId` with `transcript` on
1159
+ each event. The realtime session handles this automatically.
1160
+
1161
+ Google's `finished` transcription marker ends an utterance even when the marker
1162
+ contains no text. For payloads without this marker, response completion and
1163
+ interruption provide fallback boundaries. These fallback boundaries cannot
1164
+ unambiguously associate all delayed transcription fragments. Input item IDs use
1165
+ their own sequence and need not match the model's response IDs.
1166
+
1150
1167
  ### Live Translation
1151
1168
 
1152
1169
  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.83",
3
+ "version": "4.0.85",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@ai-sdk/provider": "4.0.19",
39
- "@ai-sdk/provider-utils": "5.0.50"
39
+ "@ai-sdk/provider-utils": "5.0.51"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@ai-sdk/test-server": "2.0.2",
@@ -266,7 +266,7 @@ export function createGoogle(
266
266
  }),
267
267
  ...options.headers,
268
268
  },
269
- `ai-sdk/google/${VERSION}`,
269
+ `ai-sdk-google/${VERSION}`,
270
270
  );
271
271
 
272
272
  const getSupportedUrls = (
@@ -25,8 +25,8 @@ type GoogleRealtimeServerContent = {
25
25
  text?: string;
26
26
  }>;
27
27
  };
28
- outputTranscription?: { text?: string };
29
- inputTranscription?: { text?: string };
28
+ outputTranscription?: { text?: string; finished?: boolean };
29
+ inputTranscription?: { text?: string; finished?: boolean };
30
30
  turnComplete?: boolean;
31
31
  waitingForInput?: boolean;
32
32
  };
@@ -38,7 +38,7 @@ type GoogleRealtimeWireEvent = {
38
38
  };
39
39
  toolCallCancellation?: unknown;
40
40
  serverContent?: GoogleRealtimeServerContent;
41
- inputTranscription?: { text?: string };
41
+ inputTranscription?: { text?: string; finished?: boolean };
42
42
  goAway?: { timeLeft?: string };
43
43
  sessionResumptionUpdate?: {
44
44
  newHandle?: string;
@@ -56,6 +56,11 @@ type GoogleRealtimeWireEvent = {
56
56
  */
57
57
  export class GoogleRealtimeEventMapper {
58
58
  private turnCounter = 0;
59
+ private inputTranscriptionCounter = 0;
60
+ private inputTranscriptionBuffer = '';
61
+ private inputTranscriptionBoundary: 'none' | 'interrupted' | 'turn-complete' =
62
+ 'none';
63
+ private preserveInputTranscriptionAcrossTurnComplete = false;
59
64
  private hasAudio = false;
60
65
  private hasText = false;
61
66
  private hasTranscript = false;
@@ -71,12 +76,11 @@ export class GoogleRealtimeEventMapper {
71
76
  }
72
77
 
73
78
  /**
74
- * Rolls over to the next turn lazily, only once new model content actually
75
- * arrives. `turnComplete` merely marks the current turn closed; the counter
76
- * is not advanced until the next response begins. This keeps a transcript
77
- * that arrives shortly after `turnComplete` attached to the turn it belongs
78
- * to, since Google delivers transcription independently with no guaranteed
79
- * ordering relative to `turnComplete`.
79
+ * Rolls over to the next turn lazily, once model content for the next turn
80
+ * arrives. `turnComplete` merely marks the current turn closed.
81
+ * This keeps an output transcript that arrives shortly after `turnComplete`
82
+ * attached to the turn it belongs to, since Google delivers transcription
83
+ * independently with no guaranteed ordering relative to `turnComplete`.
80
84
  */
81
85
  private beginTurnIfClosed(): void {
82
86
  if (!this.turnClosed) return;
@@ -85,6 +89,63 @@ export class GoogleRealtimeEventMapper {
85
89
  this.hasText = false;
86
90
  this.hasTranscript = false;
87
91
  this.turnClosed = false;
92
+ // Once the next response starts, its completion must delimit its own
93
+ // input even if the interrupted response never sent a turnComplete.
94
+ this.preserveInputTranscriptionAcrossTurnComplete = false;
95
+ }
96
+
97
+ // Google streams input transcription as a sequence of non-accumulating
98
+ // fragments for a single user utterance (e.g. "The quick brown fox",
99
+ // " jumps over the", " lazy dog."). Concatenate consecutive fragments into
100
+ // one running transcript under a stable synthetic id so the realtime reducer
101
+ // (which overwrites the message for a given id) surfaces them as a single
102
+ // coherent user message rather than one message per fragment. Google's
103
+ // `finished` signal is the authoritative utterance boundary. For older
104
+ // payloads without that signal, response boundaries remain a fallback.
105
+ //
106
+ // Input transcriptions can arrive independently of response turn events, so
107
+ // their synthetic IDs use a sequence independent of `turnCounter`.
108
+ private accumulateInputTranscription({
109
+ text,
110
+ finished,
111
+ }: {
112
+ text?: string;
113
+ finished?: boolean;
114
+ }): { itemId: string; transcript: string } | undefined {
115
+ // Google can send `finished` separately from text. Process it even when
116
+ // text is absent or empty, without emitting an empty user message.
117
+ if (!text) {
118
+ if (finished === true) {
119
+ this.finishInputTranscription();
120
+ }
121
+ return undefined;
122
+ }
123
+
124
+ const hasFinishedSignal = finished != null;
125
+ if (
126
+ this.inputTranscriptionBuffer !== '' &&
127
+ (this.inputTranscriptionBoundary === 'interrupted' ||
128
+ (this.inputTranscriptionBoundary === 'turn-complete' &&
129
+ !hasFinishedSignal))
130
+ ) {
131
+ this.finishInputTranscription();
132
+ }
133
+ this.inputTranscriptionBoundary = 'none';
134
+ this.inputTranscriptionBuffer += text;
135
+ const result = {
136
+ itemId: `google-input-${this.inputTranscriptionCounter}`,
137
+ transcript: this.inputTranscriptionBuffer,
138
+ };
139
+ if (finished === true) {
140
+ this.finishInputTranscription();
141
+ }
142
+ return result;
143
+ }
144
+
145
+ private finishInputTranscription(): void {
146
+ if (this.inputTranscriptionBuffer === '') return;
147
+ this.inputTranscriptionCounter++;
148
+ this.inputTranscriptionBuffer = '';
88
149
  }
89
150
 
90
151
  parseServerEvent(
@@ -151,13 +212,17 @@ export class GoogleRealtimeEventMapper {
151
212
  return this.parseServerContent(data.serverContent, raw);
152
213
  }
153
214
 
154
- if (data.inputTranscription?.text != null) {
155
- return {
156
- type: 'input-transcription-completed',
157
- itemId: `google-input-${this.turnCounter}`,
158
- transcript: data.inputTranscription.text,
159
- raw,
160
- };
215
+ if (data.inputTranscription != null) {
216
+ const transcription = this.accumulateInputTranscription(
217
+ data.inputTranscription,
218
+ );
219
+ if (transcription != null) {
220
+ return {
221
+ type: 'input-transcription-completed',
222
+ ...transcription,
223
+ raw,
224
+ };
225
+ }
161
226
  }
162
227
 
163
228
  return { type: 'custom', rawType: String(Object.keys(data)[0]), raw };
@@ -170,6 +235,12 @@ export class GoogleRealtimeEventMapper {
170
235
  const events: RealtimeModelV4ServerEvent[] = [];
171
236
 
172
237
  if (serverContent.interrupted) {
238
+ // A barge-in ends the current user utterance; the next input
239
+ // transcription fragment belongs to a fresh utterance. The interrupted
240
+ // response's trailing turnComplete must not split that new utterance.
241
+ this.inputTranscriptionBoundary = 'interrupted';
242
+ this.preserveInputTranscriptionAcrossTurnComplete = true;
243
+ this.turnClosed = true;
173
244
  events.push({
174
245
  type: 'speech-started',
175
246
  raw,
@@ -214,13 +285,17 @@ export class GoogleRealtimeEventMapper {
214
285
  });
215
286
  }
216
287
 
217
- if (serverContent.inputTranscription?.text) {
218
- events.push({
219
- type: 'input-transcription-completed',
220
- itemId: `google-input-${this.turnCounter}`,
221
- transcript: serverContent.inputTranscription.text,
222
- raw,
223
- });
288
+ if (serverContent.inputTranscription != null) {
289
+ const transcription = this.accumulateInputTranscription(
290
+ serverContent.inputTranscription,
291
+ );
292
+ if (transcription != null) {
293
+ events.push({
294
+ type: 'input-transcription-completed',
295
+ ...transcription,
296
+ raw,
297
+ });
298
+ }
224
299
  }
225
300
 
226
301
  // `generationComplete` means generation has stopped, but playback and the
@@ -292,6 +367,17 @@ export class GoogleRealtimeEventMapper {
292
367
  // Mark the turn closed but defer advancing the counter until the next
293
368
  // response actually begins (see `beginTurnIfClosed`).
294
369
  this.turnClosed = true;
370
+ if (this.preserveInputTranscriptionAcrossTurnComplete) {
371
+ this.preserveInputTranscriptionAcrossTurnComplete = false;
372
+ } else {
373
+ // For payloads without `finished`, the completed response remains the
374
+ // fallback signal that the next transcription is a new utterance.
375
+ // A transcription carrying `finished` can still arrive late and close
376
+ // the preceding utterance after this response boundary.
377
+ if (this.inputTranscriptionBoundary !== 'interrupted') {
378
+ this.inputTranscriptionBoundary = 'turn-complete';
379
+ }
380
+ }
295
381
  }
296
382
 
297
383
  if (events.length === 0) {