@ai-sdk/google 4.0.82 → 4.0.84
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 +26 -0
- package/dist/index.js +98 -22
- package/dist/index.js.map +1 -1
- package/docs/15-google.mdx +12 -0
- package/package.json +3 -3
- package/src/google-image-model.ts +16 -0
- package/src/realtime/google-realtime-event-mapper.ts +125 -29
package/docs/15-google.mdx
CHANGED
|
@@ -1147,6 +1147,18 @@ emitted as custom events. Inspect `rawType` for `goAway`,
|
|
|
1147
1147
|
`sessionResumptionUpdate`, and `generationComplete` and use the event's `raw`
|
|
1148
1148
|
payload for provider-specific details.
|
|
1149
1149
|
|
|
1150
|
+
With input audio transcription enabled, `parseServerEvent` emits
|
|
1151
|
+
`input-transcription-completed` events with a stable `itemId` for each user
|
|
1152
|
+
utterance. Each `transcript` contains the accumulated text so far. If you consume
|
|
1153
|
+
these events directly, replace the text for that `itemId` with `transcript` on
|
|
1154
|
+
each event. The realtime session handles this automatically.
|
|
1155
|
+
|
|
1156
|
+
Google's `finished` transcription marker ends an utterance even when the marker
|
|
1157
|
+
contains no text. For payloads without this marker, response completion and
|
|
1158
|
+
interruption provide fallback boundaries. These fallback boundaries cannot
|
|
1159
|
+
unambiguously associate all delayed transcription fragments. Input item IDs use
|
|
1160
|
+
their own sequence and need not match the model's response IDs.
|
|
1161
|
+
|
|
1150
1162
|
### Live Translation
|
|
1151
1163
|
|
|
1152
1164
|
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.84",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@ai-sdk/provider": "4.0.
|
|
39
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
38
|
+
"@ai-sdk/provider": "4.0.19",
|
|
39
|
+
"@ai-sdk/provider-utils": "5.0.50"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@ai-sdk/test-server": "2.0.2",
|
|
@@ -32,6 +32,12 @@ interface GoogleImageModelConfig {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const googleImageModelsWithFileInputSupport = new Set<string>([
|
|
36
|
+
'gemini-2.5-flash-image',
|
|
37
|
+
'gemini-3-pro-image-preview',
|
|
38
|
+
'gemini-3.1-flash-image-preview',
|
|
39
|
+
]);
|
|
40
|
+
|
|
35
41
|
export class GoogleImageModel implements ImageModelV4 {
|
|
36
42
|
readonly specificationVersion = 'v4';
|
|
37
43
|
|
|
@@ -56,6 +62,16 @@ export class GoogleImageModel implements ImageModelV4 {
|
|
|
56
62
|
return 1;
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
get supportsFileInputs(): boolean | undefined {
|
|
66
|
+
return googleImageModelsWithFileInputSupport.has(this.modelId)
|
|
67
|
+
? true
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get supportsMaskInputs(): boolean | undefined {
|
|
72
|
+
return this.supportsFileInputs === true ? false : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
59
75
|
get provider(): string {
|
|
60
76
|
return this.config.provider;
|
|
61
77
|
}
|
|
@@ -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) {
|
|
@@ -481,14 +567,24 @@ export function buildGoogleSessionConfig(
|
|
|
481
567
|
setup.outputAudioTranscription = {};
|
|
482
568
|
}
|
|
483
569
|
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
|
|
570
|
+
// Background-reasoning models require a `thinkingLevel` or `thinkingBudget`.
|
|
571
|
+
// Default to the lowest-latency level when the effective thinking config (the
|
|
572
|
+
// typed option, else a raw `providerOptions.generationConfig.thinkingConfig`)
|
|
573
|
+
// sets neither. Merged last so it survives a raw
|
|
574
|
+
// `providerOptions.generationConfig`.
|
|
575
|
+
const rawGenerationConfig = restProviderOptions.generationConfig;
|
|
576
|
+
const explicitThinkingConfig =
|
|
488
577
|
googleOptions?.thinkingConfig ??
|
|
489
|
-
(
|
|
490
|
-
|
|
578
|
+
(isRecord(rawGenerationConfig) &&
|
|
579
|
+
isRecord(rawGenerationConfig.thinkingConfig)
|
|
580
|
+
? rawGenerationConfig.thinkingConfig
|
|
491
581
|
: undefined);
|
|
582
|
+
const thinkingConfig =
|
|
583
|
+
isThinkingLiveModel(modelId) &&
|
|
584
|
+
explicitThinkingConfig?.thinkingLevel == null &&
|
|
585
|
+
explicitThinkingConfig?.thinkingBudget == null
|
|
586
|
+
? { ...explicitThinkingConfig, thinkingLevel: 'low' as const }
|
|
587
|
+
: googleOptions?.thinkingConfig;
|
|
492
588
|
const applyThinkingConfig = () => {
|
|
493
589
|
if (thinkingConfig == null) return;
|
|
494
590
|
const target = isRecord(setup.generationConfig)
|