@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.
- package/CHANGELOG.md +14 -0
- package/dist/index.js +85 -22
- package/dist/index.js.map +1 -1
- package/docs/15-google.mdx +30 -13
- package/package.json +2 -2
- package/src/google-provider.ts +1 -1
- package/src/realtime/google-realtime-event-mapper.ts +109 -23
package/docs/15-google.mdx
CHANGED
|
@@ -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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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.
|
|
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.
|
|
39
|
+
"@ai-sdk/provider-utils": "5.0.51"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@ai-sdk/test-server": "2.0.2",
|
package/src/google-provider.ts
CHANGED
|
@@ -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,
|
|
75
|
-
* arrives. `turnComplete` merely marks the current turn closed
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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) {
|