@ai-sdk/google 4.0.0-beta.8 → 4.0.0-beta.82

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +608 -5
  2. package/README.md +6 -4
  3. package/dist/index.d.ts +297 -54
  4. package/dist/index.js +5409 -640
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal/index.d.ts +97 -26
  7. package/dist/internal/index.js +1653 -453
  8. package/dist/internal/index.js.map +1 -1
  9. package/docs/{15-google-generative-ai.mdx → 15-google.mdx} +784 -69
  10. package/package.json +16 -17
  11. package/src/{convert-google-generative-ai-usage.ts → convert-google-usage.ts} +13 -5
  12. package/src/convert-json-schema-to-openapi-schema.ts +1 -1
  13. package/src/convert-to-google-messages.ts +647 -0
  14. package/src/{google-generative-ai-embedding-options.ts → google-embedding-model-options.ts} +9 -2
  15. package/src/{google-generative-ai-embedding-model.ts → google-embedding-model.ts} +31 -18
  16. package/src/google-error.ts +1 -1
  17. package/src/google-files.ts +225 -0
  18. package/src/google-image-model-options.ts +35 -0
  19. package/src/{google-generative-ai-image-model.ts → google-image-model.ts} +116 -65
  20. package/src/{google-generative-ai-image-settings.ts → google-image-settings.ts} +2 -2
  21. package/src/google-json-accumulator.ts +371 -0
  22. package/src/{google-generative-ai-options.ts → google-language-model-options.ts} +50 -5
  23. package/src/{google-generative-ai-language-model.ts → google-language-model.ts} +691 -217
  24. package/src/google-prepare-tools.ts +72 -12
  25. package/src/google-prompt.ts +86 -0
  26. package/src/google-provider.ts +157 -53
  27. package/src/google-speech-api.ts +36 -0
  28. package/src/google-speech-model-options.ts +48 -0
  29. package/src/google-speech-model.ts +311 -0
  30. package/src/google-video-model-options.ts +43 -0
  31. package/src/{google-generative-ai-video-model.ts → google-video-model.ts} +25 -60
  32. package/src/{google-generative-ai-video-settings.ts → google-video-settings.ts} +2 -1
  33. package/src/index.ts +40 -9
  34. package/src/interactions/build-google-interactions-stream-transform.ts +818 -0
  35. package/src/interactions/cancel-google-interaction.ts +60 -0
  36. package/src/interactions/convert-google-interactions-usage.ts +47 -0
  37. package/src/interactions/convert-to-google-interactions-input.ts +557 -0
  38. package/src/interactions/extract-google-interactions-sources.ts +252 -0
  39. package/src/interactions/google-interactions-agent.ts +15 -0
  40. package/src/interactions/google-interactions-api.ts +530 -0
  41. package/src/interactions/google-interactions-language-model-options.ts +262 -0
  42. package/src/interactions/google-interactions-language-model.ts +776 -0
  43. package/src/interactions/google-interactions-prompt.ts +582 -0
  44. package/src/interactions/google-interactions-provider-metadata.ts +23 -0
  45. package/src/interactions/map-google-interactions-finish-reason.ts +31 -0
  46. package/src/interactions/parse-google-interactions-outputs.ts +252 -0
  47. package/src/interactions/poll-google-interactions.ts +129 -0
  48. package/src/interactions/prepare-google-interactions-tools.ts +245 -0
  49. package/src/interactions/stream-google-interactions.ts +242 -0
  50. package/src/interactions/synthesize-google-interactions-agent-stream.ts +185 -0
  51. package/src/internal/index.ts +3 -2
  52. package/src/{map-google-generative-ai-finish-reason.ts → map-google-finish-reason.ts} +3 -3
  53. package/src/realtime/google-realtime-event-mapper.ts +383 -0
  54. package/src/realtime/google-realtime-model-options.ts +3 -0
  55. package/src/realtime/google-realtime-model.ts +160 -0
  56. package/src/realtime/index.ts +2 -0
  57. package/src/tool/code-execution.ts +2 -2
  58. package/src/tool/enterprise-web-search.ts +9 -3
  59. package/src/tool/file-search.ts +5 -7
  60. package/src/tool/google-maps.ts +3 -2
  61. package/src/tool/google-search.ts +11 -12
  62. package/src/tool/url-context.ts +4 -2
  63. package/src/tool/vertex-rag-store.ts +9 -6
  64. package/dist/index.d.mts +0 -384
  65. package/dist/index.mjs +0 -2519
  66. package/dist/index.mjs.map +0 -1
  67. package/dist/internal/index.d.mts +0 -287
  68. package/dist/internal/index.mjs +0 -1708
  69. package/dist/internal/index.mjs.map +0 -1
  70. package/src/convert-to-google-generative-ai-messages.ts +0 -239
  71. package/src/google-generative-ai-prompt.ts +0 -47
@@ -0,0 +1,818 @@
1
+ import type {
2
+ JSONValue,
3
+ LanguageModelV4FinishReason,
4
+ LanguageModelV4Source,
5
+ LanguageModelV4StreamPart,
6
+ SharedV4ProviderMetadata,
7
+ SharedV4Warning,
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;
71
+ /**
72
+ * Accumulator for partial JSON arguments. Arguments stream as a
73
+ * sequence of `arguments_delta` substrings on `step.delta`; each one is
74
+ * appended verbatim and surfaced as a `tool-input-delta`. On
75
+ * `step.stop` the accumulated string is parsed to recover the full
76
+ * arguments object for the final `tool-call` event.
77
+ */
78
+ argumentsAccum: string;
79
+ signature?: string;
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
+ /**
101
+ * A `model_output` step whose inner content-block kind has not yet been
102
+ * disambiguated. `step.start` may arrive bare (`{type: 'model_output'}`,
103
+ * no content payload); the first `step.delta` reveals whether the block
104
+ * is text or image. The block opens in this transitional state and swaps
105
+ * to `text` / `image` on the first matching delta.
106
+ */
107
+ | { kind: 'pending_model_output'; id: string }
108
+ | { kind: 'unknown'; id: string };
109
+
110
+ /**
111
+ * Builds a `TransformStream<ParseResult<GoogleInteractionsEvent>, LanguageModelV4StreamPart>`
112
+ * over the Interactions API SSE event stream.
113
+ *
114
+ * Surfaces text + thought (reasoning), function_call, image, built-in tool
115
+ * call/result steps, and `text_annotation` -> `source` parts.
116
+ */
117
+ export function buildGoogleInteractionsStreamTransform({
118
+ warnings,
119
+ generateId,
120
+ includeRawChunks,
121
+ serviceTier: headerServiceTier,
122
+ }: {
123
+ warnings: Array<SharedV4Warning>;
124
+ generateId: () => string;
125
+ includeRawChunks?: boolean;
126
+ /**
127
+ * Defensive fallback for service tier read from the `x-gemini-service-tier`
128
+ * HTTP response header. The Interactions API surfaces the applied tier in
129
+ * the `interaction.completed` event body (see `service_tier` below); this
130
+ * parameter exists so we still surface a tier if the API later starts
131
+ * sending the header.
132
+ */
133
+ serviceTier?: string;
134
+ }): TransformStream<
135
+ ParseResult<GoogleInteractionsEvent>,
136
+ LanguageModelV4StreamPart
137
+ > {
138
+ let interactionId: string | undefined;
139
+ let usage: GoogleInteractionsUsage | undefined;
140
+ let serviceTier: string | undefined = headerServiceTier;
141
+ let finishStatus: GoogleInteractionsStatus | string | undefined;
142
+ let hasFunctionCall = false;
143
+
144
+ /*
145
+ * Per-index open step slots. The Interactions API frames concurrent steps
146
+ * (e.g. text alongside thought) by `index`; we track each open slot
147
+ * independently so a text delta at index N never collides with a thought
148
+ * delta at index M.
149
+ */
150
+ const openBlocks = new Map<number, OpenBlockState>();
151
+
152
+ /*
153
+ * De-duplicate sources across the whole stream. Citations often re-appear
154
+ * across multiple `text_annotation` deltas as the model's text grows;
155
+ * surface each unique URL once.
156
+ */
157
+ const emittedSourceKeys = new Set<string>();
158
+
159
+ function sourceKey(source: LanguageModelV4Source): string {
160
+ return source.sourceType === 'url'
161
+ ? `url:${source.url}`
162
+ : `doc:${source.filename ?? source.title}`;
163
+ }
164
+
165
+ return new TransformStream<
166
+ ParseResult<GoogleInteractionsEvent>,
167
+ LanguageModelV4StreamPart
168
+ >({
169
+ start(controller) {
170
+ controller.enqueue({ type: 'stream-start', warnings });
171
+ },
172
+
173
+ transform(chunk, controller) {
174
+ if (includeRawChunks) {
175
+ controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
176
+ }
177
+
178
+ if (!chunk.success) {
179
+ finishStatus = 'failed';
180
+ controller.enqueue({ type: 'error', error: chunk.error });
181
+ return;
182
+ }
183
+
184
+ const value = chunk.value;
185
+ const eventType = (value as { event_type?: string }).event_type;
186
+
187
+ switch (eventType) {
188
+ case 'interaction.created': {
189
+ const event = value as Extract<
190
+ GoogleInteractionsEvent,
191
+ { event_type: 'interaction.created' }
192
+ >;
193
+ const interaction = event.interaction;
194
+ /*
195
+ * The Interactions API returns `id: ""` (empty string) on streaming
196
+ * events when running with `store: false` — there is no server-side
197
+ * record. Treat empty string the same as missing so providerMetadata
198
+ * stays clean.
199
+ */
200
+ interactionId =
201
+ interaction?.id != null && interaction.id.length > 0
202
+ ? interaction.id
203
+ : undefined;
204
+
205
+ const created = (interaction as { created?: string } | undefined)
206
+ ?.created;
207
+ let timestamp: Date | undefined;
208
+ if (typeof created === 'string') {
209
+ const parsed = new Date(created);
210
+ if (!Number.isNaN(parsed.getTime())) {
211
+ timestamp = parsed;
212
+ }
213
+ }
214
+
215
+ controller.enqueue({
216
+ type: 'response-metadata',
217
+ ...(interactionId != null ? { id: interactionId } : {}),
218
+ modelId: (interaction as { model?: string } | undefined)?.model,
219
+ ...(timestamp ? { timestamp } : {}),
220
+ });
221
+ break;
222
+ }
223
+
224
+ case 'step.start': {
225
+ const event = value as Extract<
226
+ GoogleInteractionsEvent,
227
+ { event_type: 'step.start' }
228
+ >;
229
+ const step = event.step as
230
+ | {
231
+ type?: string;
232
+ id?: string;
233
+ call_id?: string;
234
+ name?: string;
235
+ arguments?: Record<string, unknown>;
236
+ signature?: string;
237
+ summary?: Array<{ type?: string; text?: string }>;
238
+ result?: unknown;
239
+ is_error?: boolean;
240
+ content?: Array<{
241
+ type?: string;
242
+ text?: string;
243
+ data?: string;
244
+ mime_type?: string;
245
+ uri?: string;
246
+ annotations?: Array<GoogleInteractionsAnnotation>;
247
+ }>;
248
+ }
249
+ | undefined;
250
+ const index = event.index;
251
+ const blockId = `${interactionId ?? 'interaction'}:${index}`;
252
+ const stepType = step?.type;
253
+
254
+ if (stepType === 'model_output') {
255
+ /*
256
+ * `step.start` for a `model_output` step often carries only the
257
+ * type discriminator — content/image payloads then arrive on
258
+ * subsequent `step.delta` events. Open in a transitional
259
+ * `pending_model_output` state; the first delta promotes it to
260
+ * either `text` (and emits `text-start`) or `image`.
261
+ *
262
+ * `step.content[0]` may also arrive populated as a hint; when
263
+ * present, promote eagerly.
264
+ */
265
+ const initial = step?.content?.[0] as
266
+ | {
267
+ type?: string;
268
+ text?: string;
269
+ data?: string;
270
+ mime_type?: string;
271
+ uri?: string;
272
+ annotations?: Array<GoogleInteractionsAnnotation>;
273
+ }
274
+ | undefined;
275
+ if (initial?.type === 'text') {
276
+ openBlocks.set(index, {
277
+ kind: 'text',
278
+ id: blockId,
279
+ emittedSourceKeys: new Set<string>(),
280
+ });
281
+ controller.enqueue({ type: 'text-start', id: blockId });
282
+
283
+ const initialSources = annotationsToSources({
284
+ annotations: initial.annotations,
285
+ generateId,
286
+ });
287
+ for (const source of initialSources) {
288
+ const key = sourceKey(source);
289
+ if (emittedSourceKeys.has(key)) continue;
290
+ emittedSourceKeys.add(key);
291
+ controller.enqueue(source);
292
+ }
293
+ } else if (initial?.type === 'image') {
294
+ openBlocks.set(index, {
295
+ kind: 'image',
296
+ id: blockId,
297
+ ...(initial.data != null ? { data: initial.data } : {}),
298
+ ...(initial.mime_type != null
299
+ ? { mimeType: initial.mime_type }
300
+ : {}),
301
+ ...(initial.uri != null ? { uri: initial.uri } : {}),
302
+ });
303
+ } else {
304
+ openBlocks.set(index, {
305
+ kind: 'pending_model_output',
306
+ id: blockId,
307
+ });
308
+ }
309
+ } else if (stepType === 'thought') {
310
+ const signature = step?.signature;
311
+ openBlocks.set(index, {
312
+ kind: 'reasoning',
313
+ id: blockId,
314
+ ...(signature != null ? { signature } : {}),
315
+ });
316
+ controller.enqueue({ type: 'reasoning-start', id: blockId });
317
+ /*
318
+ * A `thought` step's initial `summary[]` may already contain text
319
+ * items on `step.start` — emit those as reasoning deltas so the
320
+ * consumer's reasoning buffer is up to date before any delta
321
+ * arrives.
322
+ */
323
+ if (Array.isArray(step?.summary)) {
324
+ for (const item of step.summary) {
325
+ if (item?.type === 'text' && typeof item.text === 'string') {
326
+ controller.enqueue({
327
+ type: 'reasoning-delta',
328
+ id: blockId,
329
+ delta: item.text,
330
+ });
331
+ }
332
+ }
333
+ }
334
+ } else if (stepType === 'function_call') {
335
+ const toolCallId = step?.id ?? blockId;
336
+ const toolName = step?.name ?? 'unknown';
337
+ hasFunctionCall = true;
338
+ const state: Extract<OpenBlockState, { kind: 'function_call' }> = {
339
+ kind: 'function_call',
340
+ id: blockId,
341
+ toolCallId,
342
+ toolName,
343
+ argumentsAccum: '',
344
+ ...(step?.signature != null ? { signature: step.signature } : {}),
345
+ };
346
+ openBlocks.set(index, state);
347
+ controller.enqueue({
348
+ type: 'tool-input-start',
349
+ id: toolCallId,
350
+ toolName,
351
+ });
352
+ } else if (
353
+ stepType != null &&
354
+ BUILTIN_TOOL_CALL_TYPES.has(stepType)
355
+ ) {
356
+ const toolName =
357
+ stepType === 'mcp_server_tool_call'
358
+ ? (step?.name ?? 'mcp_server_tool')
359
+ : builtinToolNameFromCallType(stepType);
360
+ const toolCallId = step?.id ?? blockId;
361
+ const state: Extract<
362
+ OpenBlockState,
363
+ { kind: 'builtin_tool_call' }
364
+ > = {
365
+ kind: 'builtin_tool_call',
366
+ id: blockId,
367
+ blockType: stepType,
368
+ toolCallId,
369
+ toolName,
370
+ arguments: step?.arguments ?? {},
371
+ callEmitted: false,
372
+ };
373
+ openBlocks.set(index, state);
374
+ } else if (
375
+ stepType != null &&
376
+ BUILTIN_TOOL_RESULT_TYPES.has(stepType)
377
+ ) {
378
+ const toolName =
379
+ stepType === 'mcp_server_tool_result'
380
+ ? (step?.name ?? 'mcp_server_tool')
381
+ : builtinToolNameFromResultType(stepType);
382
+ const callId = step?.call_id ?? blockId;
383
+ const state: Extract<
384
+ OpenBlockState,
385
+ { kind: 'builtin_tool_result' }
386
+ > = {
387
+ kind: 'builtin_tool_result',
388
+ id: blockId,
389
+ blockType: stepType,
390
+ callId,
391
+ toolName,
392
+ result: step?.result ?? null,
393
+ ...(step?.is_error != null ? { isError: step.is_error } : {}),
394
+ resultEmitted: false,
395
+ };
396
+ openBlocks.set(index, state);
397
+ } else {
398
+ openBlocks.set(index, { kind: 'unknown', id: blockId });
399
+ }
400
+ break;
401
+ }
402
+
403
+ case 'step.delta': {
404
+ const event = value as Extract<
405
+ GoogleInteractionsEvent,
406
+ { event_type: 'step.delta' }
407
+ >;
408
+ let open = openBlocks.get(event.index);
409
+ if (open == null) break;
410
+
411
+ const dtype = (event.delta as { type?: string } | undefined)?.type;
412
+
413
+ /*
414
+ * Promote a pending model_output block to `text` on the first
415
+ * text-shaped delta. Image deltas are emitted inline below — a
416
+ * model_output step can interleave text and image deltas, so the
417
+ * text "open block" stays in place across image emissions instead
418
+ * of being swapped for an image state.
419
+ */
420
+ if (open.kind === 'pending_model_output') {
421
+ if (
422
+ dtype === 'text' ||
423
+ dtype === 'text_annotation' ||
424
+ dtype === 'text_annotation_delta'
425
+ ) {
426
+ const promoted: Extract<OpenBlockState, { kind: 'text' }> = {
427
+ kind: 'text',
428
+ id: open.id,
429
+ emittedSourceKeys: new Set<string>(),
430
+ };
431
+ openBlocks.set(event.index, promoted);
432
+ open = promoted;
433
+ controller.enqueue({ type: 'text-start', id: promoted.id });
434
+ }
435
+ }
436
+
437
+ /*
438
+ * Image deltas inside `model_output` carry the full payload in a
439
+ * single chunk (no per-byte streaming). Emit the `file` part as
440
+ * soon as the delta arrives so it surfaces regardless of whether
441
+ * a text block is currently open at the same index.
442
+ */
443
+ if (
444
+ dtype === 'image' &&
445
+ (open.kind === 'pending_model_output' ||
446
+ open.kind === 'text' ||
447
+ open.kind === 'image')
448
+ ) {
449
+ const imageDelta = event.delta as
450
+ | { data?: string; mime_type?: string; uri?: string }
451
+ | undefined;
452
+ const google: Record<string, string> = {};
453
+ if (interactionId != null) google.interactionId = interactionId;
454
+ const providerMetadata =
455
+ Object.keys(google).length > 0 ? { google } : undefined;
456
+ if (imageDelta?.data != null && imageDelta.data.length > 0) {
457
+ controller.enqueue({
458
+ type: 'file',
459
+ mediaType: imageDelta.mime_type ?? 'image/png',
460
+ data: { type: 'data', data: imageDelta.data },
461
+ ...(providerMetadata ? { providerMetadata } : {}),
462
+ });
463
+ } else if (imageDelta?.uri != null && imageDelta.uri.length > 0) {
464
+ controller.enqueue({
465
+ type: 'file',
466
+ mediaType: imageDelta.mime_type ?? 'image/png',
467
+ data: { type: 'url', url: new URL(imageDelta.uri) },
468
+ ...(providerMetadata ? { providerMetadata } : {}),
469
+ });
470
+ }
471
+ // The file part was emitted inline; clear any data on an
472
+ // eagerly-promoted image OpenBlockState so the `step.stop`
473
+ // handler does not emit a duplicate.
474
+ if (open.kind === 'image') {
475
+ open.data = undefined;
476
+ open.uri = undefined;
477
+ }
478
+ break;
479
+ }
480
+
481
+ const delta = event.delta as
482
+ | {
483
+ type?: string;
484
+ text?: string;
485
+ signature?: string;
486
+ content?: { type?: string; text?: string };
487
+ id?: string;
488
+ /*
489
+ * `arguments` carries different shapes per delta kind:
490
+ * - `type: 'arguments_delta'` → `string` (partial JSON)
491
+ * - `type: '<builtin>_tool_call'` → `Record<string, unknown>`
492
+ * The branch handler reads it with the matching type.
493
+ */
494
+ arguments?: Record<string, unknown> | string;
495
+ annotations?: Array<GoogleInteractionsAnnotation>;
496
+ call_id?: string;
497
+ result?: unknown;
498
+ is_error?: boolean;
499
+ data?: string;
500
+ mime_type?: string;
501
+ uri?: string;
502
+ name?: string;
503
+ }
504
+ | undefined;
505
+
506
+ if (open.kind === 'text' && delta?.type === 'text') {
507
+ const text = delta.text ?? '';
508
+ if (text.length > 0) {
509
+ controller.enqueue({
510
+ type: 'text-delta',
511
+ id: open.id,
512
+ delta: text,
513
+ });
514
+ }
515
+ } else if (
516
+ open.kind === 'text' &&
517
+ (delta?.type === 'text_annotation' ||
518
+ delta?.type === 'text_annotation_delta')
519
+ ) {
520
+ const sources = annotationsToSources({
521
+ annotations: delta.annotations,
522
+ generateId,
523
+ });
524
+ for (const source of sources) {
525
+ const key = sourceKey(source);
526
+ if (emittedSourceKeys.has(key)) continue;
527
+ emittedSourceKeys.add(key);
528
+ open.emittedSourceKeys.add(key);
529
+ controller.enqueue(source);
530
+ }
531
+ } else if (open.kind === 'image' && delta?.type === 'image') {
532
+ if (delta.data != null) open.data = delta.data;
533
+ if (delta.mime_type != null) open.mimeType = delta.mime_type;
534
+ if (delta.uri != null) open.uri = delta.uri;
535
+ } else if (open.kind === 'reasoning') {
536
+ if (delta?.type === 'thought_summary') {
537
+ const item = delta.content;
538
+ if (item?.type === 'text' && typeof item.text === 'string') {
539
+ controller.enqueue({
540
+ type: 'reasoning-delta',
541
+ id: open.id,
542
+ delta: item.text,
543
+ });
544
+ }
545
+ } else if (delta?.type === 'thought_signature') {
546
+ const signature = delta.signature;
547
+ if (signature != null) {
548
+ open.signature = signature;
549
+ }
550
+ }
551
+ } else if (
552
+ open.kind === 'function_call' &&
553
+ delta?.type === 'arguments_delta'
554
+ ) {
555
+ /*
556
+ * Partial JSON arguments arrive as `arguments_delta` events.
557
+ * The partial JSON string lives in `delta.arguments` (a string,
558
+ * not the parsed object — the `arguments_delta` name applies to
559
+ * the discriminator only). Append to the accumulator and surface
560
+ * each chunk as a `tool-input-delta`; the full arguments object
561
+ * is emitted at `step.stop`.
562
+ */
563
+ const slice =
564
+ typeof delta.arguments === 'string' ? delta.arguments : '';
565
+ if (slice.length > 0) {
566
+ open.argumentsAccum += slice;
567
+ controller.enqueue({
568
+ type: 'tool-input-delta',
569
+ id: open.toolCallId,
570
+ delta: slice,
571
+ });
572
+ }
573
+ if (delta.id != null) {
574
+ open.toolCallId = delta.id;
575
+ }
576
+ if (delta.signature != null) {
577
+ open.signature = delta.signature;
578
+ }
579
+ hasFunctionCall = true;
580
+ } else if (
581
+ open.kind === 'builtin_tool_call' &&
582
+ delta?.type === open.blockType
583
+ ) {
584
+ if (delta.id != null) open.toolCallId = delta.id;
585
+ if (
586
+ delta.arguments != null &&
587
+ typeof delta.arguments === 'object'
588
+ ) {
589
+ open.arguments = delta.arguments;
590
+ }
591
+ if (
592
+ delta.name != null &&
593
+ open.blockType === 'mcp_server_tool_call'
594
+ ) {
595
+ open.toolName = delta.name;
596
+ }
597
+ } else if (
598
+ open.kind === 'builtin_tool_result' &&
599
+ delta?.type === open.blockType
600
+ ) {
601
+ if (delta.call_id != null) open.callId = delta.call_id;
602
+ if (delta.result !== undefined) open.result = delta.result;
603
+ if (delta.is_error != null) open.isError = delta.is_error;
604
+ if (
605
+ delta.name != null &&
606
+ open.blockType === 'mcp_server_tool_result'
607
+ ) {
608
+ open.toolName = delta.name;
609
+ }
610
+ }
611
+ break;
612
+ }
613
+
614
+ case 'step.stop': {
615
+ const event = value as Extract<
616
+ GoogleInteractionsEvent,
617
+ { event_type: 'step.stop' }
618
+ >;
619
+ const open = openBlocks.get(event.index);
620
+ if (open == null) break;
621
+
622
+ if (open.kind === 'text') {
623
+ const textProviderMetadata =
624
+ interactionId != null ? { google: { interactionId } } : undefined;
625
+ controller.enqueue({
626
+ type: 'text-end',
627
+ id: open.id,
628
+ ...(textProviderMetadata
629
+ ? { providerMetadata: textProviderMetadata }
630
+ : {}),
631
+ });
632
+ } else if (open.kind === 'reasoning') {
633
+ const google: Record<string, string> = {};
634
+ if (open.signature != null) google.signature = open.signature;
635
+ if (interactionId != null) google.interactionId = interactionId;
636
+ const providerMetadata =
637
+ Object.keys(google).length > 0 ? { google } : undefined;
638
+ controller.enqueue({
639
+ type: 'reasoning-end',
640
+ id: open.id,
641
+ ...(providerMetadata ? { providerMetadata } : {}),
642
+ });
643
+ } else if (open.kind === 'image') {
644
+ const google: Record<string, string> = {};
645
+ if (interactionId != null) google.interactionId = interactionId;
646
+ const providerMetadata =
647
+ Object.keys(google).length > 0 ? { google } : undefined;
648
+ if (open.data != null && open.data.length > 0) {
649
+ controller.enqueue({
650
+ type: 'file',
651
+ mediaType: open.mimeType ?? 'image/png',
652
+ data: { type: 'data', data: open.data },
653
+ ...(providerMetadata ? { providerMetadata } : {}),
654
+ });
655
+ } else if (open.uri != null && open.uri.length > 0) {
656
+ controller.enqueue({
657
+ type: 'file',
658
+ mediaType: open.mimeType ?? 'image/png',
659
+ data: { type: 'url', url: new URL(open.uri) },
660
+ ...(providerMetadata ? { providerMetadata } : {}),
661
+ });
662
+ }
663
+ } else if (open.kind === 'function_call') {
664
+ const accumulated =
665
+ open.argumentsAccum.length > 0 ? open.argumentsAccum : '{}';
666
+ controller.enqueue({
667
+ type: 'tool-input-end',
668
+ id: open.toolCallId,
669
+ });
670
+ const google: Record<string, string> = {};
671
+ if (open.signature != null) google.signature = open.signature;
672
+ if (interactionId != null) google.interactionId = interactionId;
673
+ const providerMetadata =
674
+ Object.keys(google).length > 0 ? { google } : undefined;
675
+ controller.enqueue({
676
+ type: 'tool-call',
677
+ toolCallId: open.toolCallId,
678
+ toolName: open.toolName,
679
+ input: accumulated,
680
+ ...(providerMetadata ? { providerMetadata } : {}),
681
+ });
682
+ } else if (open.kind === 'builtin_tool_call' && !open.callEmitted) {
683
+ controller.enqueue({
684
+ type: 'tool-call',
685
+ toolCallId: open.toolCallId,
686
+ toolName: open.toolName,
687
+ input: JSON.stringify(open.arguments ?? {}),
688
+ providerExecuted: true,
689
+ });
690
+ open.callEmitted = true;
691
+ } else if (
692
+ open.kind === 'builtin_tool_result' &&
693
+ !open.resultEmitted
694
+ ) {
695
+ controller.enqueue({
696
+ type: 'tool-result',
697
+ toolCallId: open.callId,
698
+ toolName: open.toolName,
699
+ result: (open.result ?? null) as NonNullable<JSONValue>,
700
+ });
701
+ open.resultEmitted = true;
702
+
703
+ const sources = builtinToolResultToSources({
704
+ block: {
705
+ type: open.blockType,
706
+ call_id: open.callId,
707
+ result: open.result,
708
+ } as unknown as GoogleInteractionsBuiltinToolResultContent,
709
+ generateId,
710
+ });
711
+ for (const source of sources) {
712
+ const key = sourceKey(source);
713
+ if (emittedSourceKeys.has(key)) continue;
714
+ emittedSourceKeys.add(key);
715
+ controller.enqueue(source);
716
+ }
717
+ }
718
+ openBlocks.delete(event.index);
719
+ break;
720
+ }
721
+
722
+ case 'interaction.status_update':
723
+ case 'interaction.in_progress':
724
+ case 'interaction.requires_action': {
725
+ const event = value as Extract<
726
+ GoogleInteractionsEvent,
727
+ {
728
+ event_type:
729
+ | 'interaction.status_update'
730
+ | 'interaction.in_progress'
731
+ | 'interaction.requires_action';
732
+ }
733
+ >;
734
+ if (event.status != null) {
735
+ finishStatus = event.status;
736
+ } else if (eventType === 'interaction.requires_action') {
737
+ finishStatus = 'requires_action';
738
+ } else {
739
+ finishStatus = 'in_progress';
740
+ }
741
+ break;
742
+ }
743
+
744
+ case 'interaction.completed': {
745
+ const event = value as Extract<
746
+ GoogleInteractionsEvent,
747
+ { event_type: 'interaction.completed' }
748
+ >;
749
+ const interaction = event.interaction as {
750
+ id?: string;
751
+ status?: GoogleInteractionsStatus;
752
+ usage?: GoogleInteractionsUsage;
753
+ service_tier?: string;
754
+ };
755
+ if (interaction?.id != null && interaction.id.length > 0) {
756
+ interactionId = interaction.id;
757
+ }
758
+ if (interaction?.status != null) {
759
+ finishStatus = interaction.status;
760
+ }
761
+ if (interaction?.usage != null) {
762
+ usage = interaction.usage;
763
+ }
764
+ /*
765
+ * The Interactions API surfaces the applied service tier on
766
+ * `interaction.completed.interaction.service_tier` (NOT on the
767
+ * `x-gemini-service-tier` HTTP header that `:generateContent`
768
+ * uses). Body wins over header fallback.
769
+ */
770
+ if (interaction?.service_tier != null) {
771
+ serviceTier = interaction.service_tier;
772
+ }
773
+ break;
774
+ }
775
+
776
+ case 'error': {
777
+ const event = value as Extract<
778
+ GoogleInteractionsEvent,
779
+ { event_type: 'error' }
780
+ >;
781
+ finishStatus = 'failed';
782
+ const errorPayload = event.error ?? {
783
+ message: 'Unknown interaction error',
784
+ };
785
+ controller.enqueue({ type: 'error', error: errorPayload });
786
+ break;
787
+ }
788
+
789
+ default:
790
+ break;
791
+ }
792
+ },
793
+
794
+ flush(controller) {
795
+ const finishReason: LanguageModelV4FinishReason = {
796
+ unified: mapGoogleInteractionsFinishReason({
797
+ status: finishStatus,
798
+ hasFunctionCall,
799
+ }),
800
+ raw: finishStatus,
801
+ };
802
+
803
+ const providerMetadata: SharedV4ProviderMetadata = {
804
+ google: {
805
+ ...(interactionId != null ? { interactionId } : {}),
806
+ ...(serviceTier != null ? { serviceTier } : {}),
807
+ },
808
+ };
809
+
810
+ controller.enqueue({
811
+ type: 'finish',
812
+ finishReason,
813
+ usage: convertGoogleInteractionsUsage(usage),
814
+ providerMetadata,
815
+ });
816
+ },
817
+ });
818
+ }