@ai-sdk/google 3.0.67 → 3.0.68

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.
@@ -0,0 +1,711 @@
1
+ import type {
2
+ JSONValue,
3
+ LanguageModelV3FinishReason,
4
+ LanguageModelV3Source,
5
+ LanguageModelV3StreamPart,
6
+ SharedV3ProviderMetadata,
7
+ SharedV3Warning,
8
+ } from '@ai-sdk/provider';
9
+ import type { ParseResult } from '@ai-sdk/provider-utils';
10
+ import type {
11
+ GoogleInteractionsEvent,
12
+ GoogleInteractionsUsage,
13
+ } from './google-interactions-api';
14
+ import { convertGoogleInteractionsUsage } from './convert-google-interactions-usage';
15
+ import {
16
+ annotationsToSources,
17
+ builtinToolResultToSources,
18
+ } from './extract-google-interactions-sources';
19
+ import { mapGoogleInteractionsFinishReason } from './map-google-interactions-finish-reason';
20
+ import type {
21
+ GoogleInteractionsAnnotation,
22
+ GoogleInteractionsBuiltinToolResultContent,
23
+ GoogleInteractionsStatus,
24
+ } from './google-interactions-prompt';
25
+
26
+ const BUILTIN_TOOL_CALL_TYPES = new Set([
27
+ 'google_search_call',
28
+ 'code_execution_call',
29
+ 'url_context_call',
30
+ 'file_search_call',
31
+ 'google_maps_call',
32
+ 'mcp_server_tool_call',
33
+ ]);
34
+
35
+ const BUILTIN_TOOL_RESULT_TYPES = new Set([
36
+ 'google_search_result',
37
+ 'code_execution_result',
38
+ 'url_context_result',
39
+ 'file_search_result',
40
+ 'google_maps_result',
41
+ 'mcp_server_tool_result',
42
+ ]);
43
+
44
+ function builtinToolNameFromCallType(type: string): string {
45
+ return type.replace(/_call$/, '');
46
+ }
47
+
48
+ function builtinToolNameFromResultType(type: string): string {
49
+ return type.replace(/_result$/, '');
50
+ }
51
+
52
+ type OpenBlockState =
53
+ | { kind: 'text'; id: string; emittedSourceKeys: Set<string> }
54
+ | {
55
+ kind: 'reasoning';
56
+ id: string;
57
+ signature?: string;
58
+ }
59
+ | {
60
+ kind: 'image';
61
+ id: string;
62
+ data?: string;
63
+ mimeType?: string;
64
+ uri?: string;
65
+ }
66
+ | {
67
+ kind: 'function_call';
68
+ id: string;
69
+ toolCallId: string;
70
+ toolName: string | undefined;
71
+ arguments: Record<string, unknown>;
72
+ signature?: string;
73
+ /**
74
+ * Whether `tool-input-start` has been emitted. Deferred until we know
75
+ * the tool name -- `content.start` for a function_call only carries
76
+ * `type: 'function_call'`; `id`, `name`, and `arguments` arrive on
77
+ * `content.delta`.
78
+ */
79
+ startEmitted: boolean;
80
+ }
81
+ | {
82
+ kind: 'builtin_tool_call';
83
+ id: string;
84
+ blockType: string;
85
+ toolCallId: string;
86
+ toolName: string;
87
+ arguments: Record<string, unknown>;
88
+ callEmitted: boolean;
89
+ }
90
+ | {
91
+ kind: 'builtin_tool_result';
92
+ id: string;
93
+ blockType: string;
94
+ callId: string;
95
+ toolName: string;
96
+ result: unknown;
97
+ isError?: boolean;
98
+ resultEmitted: boolean;
99
+ }
100
+ | { kind: 'unknown'; id: string };
101
+
102
+ /**
103
+ * Builds a `TransformStream<ParseResult<GoogleInteractionsEvent>, LanguageModelV3StreamPart>`
104
+ * over the seven Interactions SSE event types.
105
+ *
106
+ * Surfaces text + thought (reasoning), function_call, image, built-in tool
107
+ * call/result blocks, and `text_annotation` -> `source` parts.
108
+ */
109
+ export function buildGoogleInteractionsStreamTransform({
110
+ warnings,
111
+ generateId,
112
+ includeRawChunks,
113
+ serviceTier: headerServiceTier,
114
+ }: {
115
+ warnings: Array<SharedV3Warning>;
116
+ generateId: () => string;
117
+ includeRawChunks?: boolean;
118
+ /**
119
+ * Defensive fallback for service tier read from the `x-gemini-service-tier`
120
+ * HTTP response header. The Interactions API surfaces the applied tier in
121
+ * the `interaction.complete` event body (see `service_tier` below); this
122
+ * parameter exists so we still surface a tier if the API later starts
123
+ * sending the header (matching `google-language-model.ts` commit
124
+ * 1adfb76d2d).
125
+ */
126
+ serviceTier?: string;
127
+ }): TransformStream<
128
+ ParseResult<GoogleInteractionsEvent>,
129
+ LanguageModelV3StreamPart
130
+ > {
131
+ let interactionId: string | undefined;
132
+ let usage: GoogleInteractionsUsage | undefined;
133
+ let serviceTier: string | undefined = headerServiceTier;
134
+ let finishStatus: GoogleInteractionsStatus | string | undefined;
135
+ let hasFunctionCall = false;
136
+
137
+ /*
138
+ * Per-index open content slots. The Interactions API frames concurrent
139
+ * content blocks (e.g. text alongside thought) by `index`; we track each
140
+ * open slot independently so a text delta at index N never collides with a
141
+ * thought delta at index M.
142
+ */
143
+ const openBlocks = new Map<number, OpenBlockState>();
144
+
145
+ /*
146
+ * De-duplicate sources across the whole stream. Citations often re-appear
147
+ * across multiple `text_annotation` deltas as the model's text grows;
148
+ * surface each unique URL once.
149
+ */
150
+ const emittedSourceKeys = new Set<string>();
151
+
152
+ function sourceKey(source: LanguageModelV3Source): string {
153
+ return source.sourceType === 'url'
154
+ ? `url:${source.url}`
155
+ : `doc:${source.filename ?? source.title}`;
156
+ }
157
+
158
+ return new TransformStream<
159
+ ParseResult<GoogleInteractionsEvent>,
160
+ LanguageModelV3StreamPart
161
+ >({
162
+ start(controller) {
163
+ controller.enqueue({ type: 'stream-start', warnings });
164
+ },
165
+
166
+ transform(chunk, controller) {
167
+ if (includeRawChunks) {
168
+ controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
169
+ }
170
+
171
+ if (!chunk.success) {
172
+ finishStatus = 'failed';
173
+ controller.enqueue({ type: 'error', error: chunk.error });
174
+ return;
175
+ }
176
+
177
+ const value = chunk.value;
178
+ const eventType = (value as { event_type?: string }).event_type;
179
+
180
+ switch (eventType) {
181
+ case 'interaction.start': {
182
+ const event = value as Extract<
183
+ GoogleInteractionsEvent,
184
+ { event_type: 'interaction.start' }
185
+ >;
186
+ const interaction = event.interaction;
187
+ /*
188
+ * The Interactions API returns `id: ""` (empty string) on streaming
189
+ * `interaction.start` / `interaction.complete` events when running
190
+ * with `store: false` — there is no server-side record. Treat empty
191
+ * string the same as missing so providerMetadata stays clean.
192
+ */
193
+ interactionId =
194
+ interaction?.id != null && interaction.id.length > 0
195
+ ? interaction.id
196
+ : undefined;
197
+
198
+ const created = (interaction as { created?: string } | undefined)
199
+ ?.created;
200
+ let timestamp: Date | undefined;
201
+ if (typeof created === 'string') {
202
+ const parsed = new Date(created);
203
+ if (!Number.isNaN(parsed.getTime())) {
204
+ timestamp = parsed;
205
+ }
206
+ }
207
+
208
+ controller.enqueue({
209
+ type: 'response-metadata',
210
+ ...(interactionId != null ? { id: interactionId } : {}),
211
+ modelId: (interaction as { model?: string } | undefined)?.model,
212
+ ...(timestamp ? { timestamp } : {}),
213
+ });
214
+ break;
215
+ }
216
+
217
+ case 'content.start': {
218
+ const event = value as Extract<
219
+ GoogleInteractionsEvent,
220
+ { event_type: 'content.start' }
221
+ >;
222
+ const block = event.content as
223
+ | {
224
+ type?: string;
225
+ id?: string;
226
+ call_id?: string;
227
+ name?: string;
228
+ arguments?: Record<string, unknown>;
229
+ signature?: string;
230
+ result?: unknown;
231
+ is_error?: boolean;
232
+ annotations?: Array<GoogleInteractionsAnnotation>;
233
+ }
234
+ | undefined;
235
+ const index = event.index;
236
+ const blockId = `${interactionId ?? 'interaction'}:${index}`;
237
+
238
+ if (block?.type === 'text') {
239
+ openBlocks.set(index, {
240
+ kind: 'text',
241
+ id: blockId,
242
+ emittedSourceKeys: new Set<string>(),
243
+ });
244
+ controller.enqueue({ type: 'text-start', id: blockId });
245
+
246
+ // text content blocks may already carry annotations on open.
247
+ const initialSources = annotationsToSources({
248
+ annotations: block.annotations,
249
+ generateId,
250
+ });
251
+ for (const source of initialSources) {
252
+ const key = sourceKey(source);
253
+ if (emittedSourceKeys.has(key)) continue;
254
+ emittedSourceKeys.add(key);
255
+ controller.enqueue(source);
256
+ }
257
+ } else if (block?.type === 'image') {
258
+ const img = block as {
259
+ data?: string;
260
+ mime_type?: string;
261
+ uri?: string;
262
+ };
263
+ openBlocks.set(index, {
264
+ kind: 'image',
265
+ id: blockId,
266
+ ...(img.data != null ? { data: img.data } : {}),
267
+ ...(img.mime_type != null ? { mimeType: img.mime_type } : {}),
268
+ ...(img.uri != null ? { uri: img.uri } : {}),
269
+ });
270
+ } else if (block?.type === 'thought') {
271
+ const signature = (block as { signature?: string }).signature;
272
+ openBlocks.set(index, {
273
+ kind: 'reasoning',
274
+ id: blockId,
275
+ ...(signature != null ? { signature } : {}),
276
+ });
277
+ controller.enqueue({ type: 'reasoning-start', id: blockId });
278
+ } else if (block?.type === 'function_call') {
279
+ const fc = block;
280
+ const toolCallId = fc.id ?? blockId;
281
+ hasFunctionCall = true;
282
+ const state: Extract<OpenBlockState, { kind: 'function_call' }> = {
283
+ kind: 'function_call',
284
+ id: blockId,
285
+ toolCallId,
286
+ toolName: fc.name,
287
+ arguments: fc.arguments ?? {},
288
+ ...(fc.signature != null ? { signature: fc.signature } : {}),
289
+ startEmitted: false,
290
+ };
291
+ openBlocks.set(index, state);
292
+ if (state.toolName != null) {
293
+ controller.enqueue({
294
+ type: 'tool-input-start',
295
+ id: toolCallId,
296
+ toolName: state.toolName,
297
+ });
298
+ state.startEmitted = true;
299
+ }
300
+ } else if (
301
+ block?.type != null &&
302
+ BUILTIN_TOOL_CALL_TYPES.has(block.type)
303
+ ) {
304
+ const toolName =
305
+ block.type === 'mcp_server_tool_call'
306
+ ? (block.name ?? 'mcp_server_tool')
307
+ : builtinToolNameFromCallType(block.type);
308
+ const toolCallId = block.id ?? blockId;
309
+ const state: Extract<
310
+ OpenBlockState,
311
+ { kind: 'builtin_tool_call' }
312
+ > = {
313
+ kind: 'builtin_tool_call',
314
+ id: blockId,
315
+ blockType: block.type,
316
+ toolCallId,
317
+ toolName,
318
+ arguments: block.arguments ?? {},
319
+ callEmitted: false,
320
+ };
321
+ openBlocks.set(index, state);
322
+ } else if (
323
+ block?.type != null &&
324
+ BUILTIN_TOOL_RESULT_TYPES.has(block.type)
325
+ ) {
326
+ const toolName =
327
+ block.type === 'mcp_server_tool_result'
328
+ ? (block.name ?? 'mcp_server_tool')
329
+ : builtinToolNameFromResultType(block.type);
330
+ const callId = block.call_id ?? blockId;
331
+ const state: Extract<
332
+ OpenBlockState,
333
+ { kind: 'builtin_tool_result' }
334
+ > = {
335
+ kind: 'builtin_tool_result',
336
+ id: blockId,
337
+ blockType: block.type,
338
+ callId,
339
+ toolName,
340
+ result: block.result ?? null,
341
+ ...(block.is_error != null ? { isError: block.is_error } : {}),
342
+ resultEmitted: false,
343
+ };
344
+ openBlocks.set(index, state);
345
+ } else {
346
+ openBlocks.set(index, { kind: 'unknown', id: blockId });
347
+ }
348
+ break;
349
+ }
350
+
351
+ case 'content.delta': {
352
+ const event = value as Extract<
353
+ GoogleInteractionsEvent,
354
+ { event_type: 'content.delta' }
355
+ >;
356
+ const open = openBlocks.get(event.index);
357
+ if (open == null) break;
358
+
359
+ const delta = event.delta as
360
+ | {
361
+ type?: string;
362
+ text?: string;
363
+ signature?: string;
364
+ content?: { type?: string; text?: string };
365
+ id?: string;
366
+ name?: string;
367
+ arguments?: Record<string, unknown>;
368
+ annotations?: Array<GoogleInteractionsAnnotation>;
369
+ call_id?: string;
370
+ result?: unknown;
371
+ is_error?: boolean;
372
+ data?: string;
373
+ mime_type?: string;
374
+ uri?: string;
375
+ }
376
+ | undefined;
377
+
378
+ if (open.kind === 'text' && delta?.type === 'text') {
379
+ const text = delta.text ?? '';
380
+ if (text.length > 0) {
381
+ controller.enqueue({
382
+ type: 'text-delta',
383
+ id: open.id,
384
+ delta: text,
385
+ });
386
+ }
387
+ } else if (
388
+ open.kind === 'text' &&
389
+ delta?.type === 'text_annotation'
390
+ ) {
391
+ const sources = annotationsToSources({
392
+ annotations: delta.annotations,
393
+ generateId,
394
+ });
395
+ for (const source of sources) {
396
+ const key = sourceKey(source);
397
+ if (emittedSourceKeys.has(key)) continue;
398
+ emittedSourceKeys.add(key);
399
+ open.emittedSourceKeys.add(key);
400
+ controller.enqueue(source);
401
+ }
402
+ } else if (open.kind === 'image' && delta?.type === 'image') {
403
+ /*
404
+ * `image` ContentDelta carries the entire image payload as a
405
+ * complete object (`data` base64 + `mime_type`, or `uri`) per
406
+ * `googleapis/js-genai`
407
+ * `src/interactions/resources/interactions.ts`
408
+ * `ContentDelta.Image`. Accumulate the latest snapshot; emit the
409
+ * file stream part on `content.stop`.
410
+ */
411
+ if (delta.data != null) open.data = delta.data;
412
+ if (delta.mime_type != null) open.mimeType = delta.mime_type;
413
+ if (delta.uri != null) open.uri = delta.uri;
414
+ } else if (open.kind === 'reasoning') {
415
+ if (delta?.type === 'thought_summary') {
416
+ const item = delta.content;
417
+ if (item?.type === 'text' && typeof item.text === 'string') {
418
+ controller.enqueue({
419
+ type: 'reasoning-delta',
420
+ id: open.id,
421
+ delta: item.text,
422
+ });
423
+ }
424
+ } else if (delta?.type === 'thought_signature') {
425
+ const signature = delta.signature;
426
+ if (signature != null) {
427
+ open.signature = signature;
428
+ }
429
+ }
430
+ } else if (
431
+ open.kind === 'function_call' &&
432
+ delta?.type === 'function_call'
433
+ ) {
434
+ /*
435
+ * `function_call` ContentDelta carries the entire call as a
436
+ * complete object (id, name, arguments) per
437
+ * `googleapis/js-genai` `src/interactions/resources/interactions.ts`
438
+ * `ContentDelta.FunctionCall` (line ~458) -- there is no token
439
+ * streaming of the JSON arguments. We accumulate the latest
440
+ * snapshot and emit a single `tool-input-delta` carrying the
441
+ * stringified args at content.stop.
442
+ *
443
+ * The `name` typically arrives here (not on `content.start`), so
444
+ * defer `tool-input-start` emission until we observe it.
445
+ */
446
+ if (delta.id != null) {
447
+ open.toolCallId = delta.id;
448
+ }
449
+ if (delta.name != null) {
450
+ open.toolName = delta.name;
451
+ }
452
+ if (delta.arguments != null) {
453
+ open.arguments = delta.arguments;
454
+ }
455
+ if (delta.signature != null) {
456
+ open.signature = delta.signature;
457
+ }
458
+ if (!open.startEmitted && open.toolName != null) {
459
+ controller.enqueue({
460
+ type: 'tool-input-start',
461
+ id: open.toolCallId,
462
+ toolName: open.toolName,
463
+ });
464
+ open.startEmitted = true;
465
+ }
466
+ hasFunctionCall = true;
467
+ } else if (
468
+ open.kind === 'builtin_tool_call' &&
469
+ delta?.type === open.blockType
470
+ ) {
471
+ if (delta.id != null) open.toolCallId = delta.id;
472
+ if (delta.arguments != null) open.arguments = delta.arguments;
473
+ if (
474
+ delta.name != null &&
475
+ open.blockType === 'mcp_server_tool_call'
476
+ ) {
477
+ open.toolName = delta.name;
478
+ }
479
+ } else if (
480
+ open.kind === 'builtin_tool_result' &&
481
+ delta?.type === open.blockType
482
+ ) {
483
+ if (delta.call_id != null) open.callId = delta.call_id;
484
+ if (delta.result !== undefined) open.result = delta.result;
485
+ if (delta.is_error != null) open.isError = delta.is_error;
486
+ if (
487
+ delta.name != null &&
488
+ open.blockType === 'mcp_server_tool_result'
489
+ ) {
490
+ open.toolName = delta.name;
491
+ }
492
+ }
493
+ break;
494
+ }
495
+
496
+ case 'content.stop': {
497
+ const event = value as Extract<
498
+ GoogleInteractionsEvent,
499
+ { event_type: 'content.stop' }
500
+ >;
501
+ const open = openBlocks.get(event.index);
502
+ if (open == null) break;
503
+
504
+ if (open.kind === 'text') {
505
+ const textProviderMetadata =
506
+ interactionId != null ? { google: { interactionId } } : undefined;
507
+ controller.enqueue({
508
+ type: 'text-end',
509
+ id: open.id,
510
+ ...(textProviderMetadata
511
+ ? { providerMetadata: textProviderMetadata }
512
+ : {}),
513
+ });
514
+ } else if (open.kind === 'reasoning') {
515
+ const google: Record<string, string> = {};
516
+ if (open.signature != null) google.signature = open.signature;
517
+ if (interactionId != null) google.interactionId = interactionId;
518
+ const providerMetadata =
519
+ Object.keys(google).length > 0 ? { google } : undefined;
520
+ controller.enqueue({
521
+ type: 'reasoning-end',
522
+ id: open.id,
523
+ ...(providerMetadata ? { providerMetadata } : {}),
524
+ });
525
+ } else if (open.kind === 'image') {
526
+ const google: Record<string, string> = {};
527
+ if (interactionId != null) google.interactionId = interactionId;
528
+ const providerMetadata =
529
+ Object.keys(google).length > 0 ? { google } : undefined;
530
+ if (open.data != null && open.data.length > 0) {
531
+ controller.enqueue({
532
+ type: 'file',
533
+ mediaType: open.mimeType ?? 'image/png',
534
+ data: open.data,
535
+ ...(providerMetadata ? { providerMetadata } : {}),
536
+ });
537
+ } else if (open.uri != null && open.uri.length > 0) {
538
+ /*
539
+ * V3 `LanguageModelV3File` only supports inline data (`string` /
540
+ * `Uint8Array`). URL-only image outputs cannot be represented as
541
+ * a file stream part on the v3 spec; surface the URI through
542
+ * provider metadata so callers can still recover it.
543
+ */
544
+ const uriProviderMetadata = {
545
+ google: {
546
+ ...(interactionId != null ? { interactionId } : {}),
547
+ imageUri: open.uri,
548
+ },
549
+ };
550
+ controller.enqueue({
551
+ type: 'file',
552
+ mediaType: open.mimeType ?? 'image/png',
553
+ data: '',
554
+ providerMetadata: uriProviderMetadata,
555
+ });
556
+ }
557
+ } else if (open.kind === 'function_call') {
558
+ const toolName = open.toolName ?? 'unknown';
559
+ const argsJson = JSON.stringify(open.arguments ?? {});
560
+ if (!open.startEmitted) {
561
+ controller.enqueue({
562
+ type: 'tool-input-start',
563
+ id: open.toolCallId,
564
+ toolName,
565
+ });
566
+ }
567
+ controller.enqueue({
568
+ type: 'tool-input-delta',
569
+ id: open.toolCallId,
570
+ delta: argsJson,
571
+ });
572
+ controller.enqueue({
573
+ type: 'tool-input-end',
574
+ id: open.toolCallId,
575
+ });
576
+ const google: Record<string, string> = {};
577
+ if (open.signature != null) google.signature = open.signature;
578
+ if (interactionId != null) google.interactionId = interactionId;
579
+ const providerMetadata =
580
+ Object.keys(google).length > 0 ? { google } : undefined;
581
+ controller.enqueue({
582
+ type: 'tool-call',
583
+ toolCallId: open.toolCallId,
584
+ toolName,
585
+ input: argsJson,
586
+ ...(providerMetadata ? { providerMetadata } : {}),
587
+ });
588
+ } else if (open.kind === 'builtin_tool_call' && !open.callEmitted) {
589
+ controller.enqueue({
590
+ type: 'tool-call',
591
+ toolCallId: open.toolCallId,
592
+ toolName: open.toolName,
593
+ input: JSON.stringify(open.arguments ?? {}),
594
+ providerExecuted: true,
595
+ });
596
+ open.callEmitted = true;
597
+ } else if (
598
+ open.kind === 'builtin_tool_result' &&
599
+ !open.resultEmitted
600
+ ) {
601
+ controller.enqueue({
602
+ type: 'tool-result',
603
+ toolCallId: open.callId,
604
+ toolName: open.toolName,
605
+ result: (open.result ?? null) as NonNullable<JSONValue>,
606
+ });
607
+ open.resultEmitted = true;
608
+
609
+ const sources = builtinToolResultToSources({
610
+ block: {
611
+ type: open.blockType,
612
+ call_id: open.callId,
613
+ result: open.result,
614
+ } as unknown as GoogleInteractionsBuiltinToolResultContent,
615
+ generateId,
616
+ });
617
+ for (const source of sources) {
618
+ const key = sourceKey(source);
619
+ if (emittedSourceKeys.has(key)) continue;
620
+ emittedSourceKeys.add(key);
621
+ controller.enqueue(source);
622
+ }
623
+ }
624
+ openBlocks.delete(event.index);
625
+ break;
626
+ }
627
+
628
+ case 'interaction.status_update': {
629
+ const event = value as Extract<
630
+ GoogleInteractionsEvent,
631
+ { event_type: 'interaction.status_update' }
632
+ >;
633
+ finishStatus = event.status;
634
+ break;
635
+ }
636
+
637
+ case 'interaction.complete': {
638
+ const event = value as Extract<
639
+ GoogleInteractionsEvent,
640
+ { event_type: 'interaction.complete' }
641
+ >;
642
+ const interaction = event.interaction as {
643
+ id?: string;
644
+ status?: GoogleInteractionsStatus;
645
+ usage?: GoogleInteractionsUsage;
646
+ service_tier?: string;
647
+ };
648
+ if (interaction?.id != null && interaction.id.length > 0) {
649
+ interactionId = interaction.id;
650
+ }
651
+ if (interaction?.status != null) {
652
+ finishStatus = interaction.status;
653
+ }
654
+ if (interaction?.usage != null) {
655
+ usage = interaction.usage;
656
+ }
657
+ /*
658
+ * The Interactions API surfaces the applied service tier on
659
+ * `interaction.complete.interaction.service_tier` (NOT on the
660
+ * `x-gemini-service-tier` HTTP header that `:generateContent`
661
+ * uses). Body wins over header fallback.
662
+ */
663
+ if (interaction?.service_tier != null) {
664
+ serviceTier = interaction.service_tier;
665
+ }
666
+ break;
667
+ }
668
+
669
+ case 'error': {
670
+ const event = value as Extract<
671
+ GoogleInteractionsEvent,
672
+ { event_type: 'error' }
673
+ >;
674
+ finishStatus = 'failed';
675
+ const errorPayload = event.error ?? {
676
+ message: 'Unknown interaction error',
677
+ };
678
+ controller.enqueue({ type: 'error', error: errorPayload });
679
+ break;
680
+ }
681
+
682
+ default:
683
+ break;
684
+ }
685
+ },
686
+
687
+ flush(controller) {
688
+ const finishReason: LanguageModelV3FinishReason = {
689
+ unified: mapGoogleInteractionsFinishReason({
690
+ status: finishStatus,
691
+ hasFunctionCall,
692
+ }),
693
+ raw: finishStatus,
694
+ };
695
+
696
+ const providerMetadata: SharedV3ProviderMetadata = {
697
+ google: {
698
+ ...(interactionId != null ? { interactionId } : {}),
699
+ ...(serviceTier != null ? { serviceTier } : {}),
700
+ },
701
+ };
702
+
703
+ controller.enqueue({
704
+ type: 'finish',
705
+ finishReason,
706
+ usage: convertGoogleInteractionsUsage(usage),
707
+ providerMetadata,
708
+ });
709
+ },
710
+ });
711
+ }