@ai-sdk/google 3.0.74 → 3.0.75

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.
@@ -67,16 +67,16 @@ type OpenBlockState =
67
67
  kind: 'function_call';
68
68
  id: string;
69
69
  toolCallId: string;
70
- toolName: string | undefined;
71
- arguments: Record<string, unknown>;
72
- signature?: string;
70
+ toolName: string;
73
71
  /**
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`.
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.
78
77
  */
79
- startEmitted: boolean;
78
+ argumentsAccum: string;
79
+ signature?: string;
80
80
  }
81
81
  | {
82
82
  kind: 'builtin_tool_call';
@@ -97,14 +97,22 @@ type OpenBlockState =
97
97
  isError?: boolean;
98
98
  resultEmitted: boolean;
99
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 }
100
108
  | { kind: 'unknown'; id: string };
101
109
 
102
110
  /**
103
111
  * Builds a `TransformStream<ParseResult<GoogleInteractionsEvent>, LanguageModelV3StreamPart>`
104
- * over the seven Interactions SSE event types.
112
+ * over the Interactions API SSE event stream.
105
113
  *
106
114
  * Surfaces text + thought (reasoning), function_call, image, built-in tool
107
- * call/result blocks, and `text_annotation` -> `source` parts.
115
+ * call/result steps, and `text_annotation` -> `source` parts.
108
116
  */
109
117
  export function buildGoogleInteractionsStreamTransform({
110
118
  warnings,
@@ -118,10 +126,9 @@ export function buildGoogleInteractionsStreamTransform({
118
126
  /**
119
127
  * Defensive fallback for service tier read from the `x-gemini-service-tier`
120
128
  * HTTP response header. The Interactions API surfaces the applied tier in
121
- * the `interaction.complete` event body (see `service_tier` below); this
129
+ * the `interaction.completed` event body (see `service_tier` below); this
122
130
  * 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).
131
+ * sending the header.
125
132
  */
126
133
  serviceTier?: string;
127
134
  }): TransformStream<
@@ -135,10 +142,10 @@ export function buildGoogleInteractionsStreamTransform({
135
142
  let hasFunctionCall = false;
136
143
 
137
144
  /*
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.
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.
142
149
  */
143
150
  const openBlocks = new Map<number, OpenBlockState>();
144
151
 
@@ -178,17 +185,17 @@ export function buildGoogleInteractionsStreamTransform({
178
185
  const eventType = (value as { event_type?: string }).event_type;
179
186
 
180
187
  switch (eventType) {
181
- case 'interaction.start': {
188
+ case 'interaction.created': {
182
189
  const event = value as Extract<
183
190
  GoogleInteractionsEvent,
184
- { event_type: 'interaction.start' }
191
+ { event_type: 'interaction.created' }
185
192
  >;
186
193
  const interaction = event.interaction;
187
194
  /*
188
195
  * 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.
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.
192
199
  */
193
200
  interactionId =
194
201
  interaction?.id != null && interaction.id.length > 0
@@ -214,12 +221,12 @@ export function buildGoogleInteractionsStreamTransform({
214
221
  break;
215
222
  }
216
223
 
217
- case 'content.start': {
224
+ case 'step.start': {
218
225
  const event = value as Extract<
219
226
  GoogleInteractionsEvent,
220
- { event_type: 'content.start' }
227
+ { event_type: 'step.start' }
221
228
  >;
222
- const block = event.content as
229
+ const step = event.step as
223
230
  | {
224
231
  type?: string;
225
232
  id?: string;
@@ -227,118 +234,163 @@ export function buildGoogleInteractionsStreamTransform({
227
234
  name?: string;
228
235
  arguments?: Record<string, unknown>;
229
236
  signature?: string;
237
+ summary?: Array<{ type?: string; text?: string }>;
230
238
  result?: unknown;
231
239
  is_error?: boolean;
232
- annotations?: Array<GoogleInteractionsAnnotation>;
240
+ content?: Array<{
241
+ type?: string;
242
+ text?: string;
243
+ data?: string;
244
+ mime_type?: string;
245
+ uri?: string;
246
+ annotations?: Array<GoogleInteractionsAnnotation>;
247
+ }>;
233
248
  }
234
249
  | undefined;
235
250
  const index = event.index;
236
251
  const blockId = `${interactionId ?? 'interaction'}:${index}`;
252
+ const stepType = step?.type;
237
253
 
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 });
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 });
245
282
 
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);
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
+ });
256
308
  }
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;
309
+ } else if (stepType === 'thought') {
310
+ const signature = step?.signature;
272
311
  openBlocks.set(index, {
273
312
  kind: 'reasoning',
274
313
  id: blockId,
275
314
  ...(signature != null ? { signature } : {}),
276
315
  });
277
316
  controller.enqueue({ type: 'reasoning-start', id: blockId });
278
- } else if (block?.type === 'function_call') {
279
- const fc = block;
280
- const toolCallId = fc.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';
281
337
  hasFunctionCall = true;
282
338
  const state: Extract<OpenBlockState, { kind: 'function_call' }> = {
283
339
  kind: 'function_call',
284
340
  id: blockId,
285
341
  toolCallId,
286
- toolName: fc.name,
287
- arguments: fc.arguments ?? {},
288
- ...(fc.signature != null ? { signature: fc.signature } : {}),
289
- startEmitted: false,
342
+ toolName,
343
+ argumentsAccum: '',
344
+ ...(step?.signature != null ? { signature: step.signature } : {}),
290
345
  };
291
346
  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
- }
347
+ controller.enqueue({
348
+ type: 'tool-input-start',
349
+ id: toolCallId,
350
+ toolName,
351
+ });
300
352
  } else if (
301
- block?.type != null &&
302
- BUILTIN_TOOL_CALL_TYPES.has(block.type)
353
+ stepType != null &&
354
+ BUILTIN_TOOL_CALL_TYPES.has(stepType)
303
355
  ) {
304
356
  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;
357
+ stepType === 'mcp_server_tool_call'
358
+ ? (step?.name ?? 'mcp_server_tool')
359
+ : builtinToolNameFromCallType(stepType);
360
+ const toolCallId = step?.id ?? blockId;
309
361
  const state: Extract<
310
362
  OpenBlockState,
311
363
  { kind: 'builtin_tool_call' }
312
364
  > = {
313
365
  kind: 'builtin_tool_call',
314
366
  id: blockId,
315
- blockType: block.type,
367
+ blockType: stepType,
316
368
  toolCallId,
317
369
  toolName,
318
- arguments: block.arguments ?? {},
370
+ arguments: step?.arguments ?? {},
319
371
  callEmitted: false,
320
372
  };
321
373
  openBlocks.set(index, state);
322
374
  } else if (
323
- block?.type != null &&
324
- BUILTIN_TOOL_RESULT_TYPES.has(block.type)
375
+ stepType != null &&
376
+ BUILTIN_TOOL_RESULT_TYPES.has(stepType)
325
377
  ) {
326
378
  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;
379
+ stepType === 'mcp_server_tool_result'
380
+ ? (step?.name ?? 'mcp_server_tool')
381
+ : builtinToolNameFromResultType(stepType);
382
+ const callId = step?.call_id ?? blockId;
331
383
  const state: Extract<
332
384
  OpenBlockState,
333
385
  { kind: 'builtin_tool_result' }
334
386
  > = {
335
387
  kind: 'builtin_tool_result',
336
388
  id: blockId,
337
- blockType: block.type,
389
+ blockType: stepType,
338
390
  callId,
339
391
  toolName,
340
- result: block.result ?? null,
341
- ...(block.is_error != null ? { isError: block.is_error } : {}),
392
+ result: step?.result ?? null,
393
+ ...(step?.is_error != null ? { isError: step.is_error } : {}),
342
394
  resultEmitted: false,
343
395
  };
344
396
  openBlocks.set(index, state);
@@ -348,14 +400,96 @@ export function buildGoogleInteractionsStreamTransform({
348
400
  break;
349
401
  }
350
402
 
351
- case 'content.delta': {
403
+ case 'step.delta': {
352
404
  const event = value as Extract<
353
405
  GoogleInteractionsEvent,
354
- { event_type: 'content.delta' }
406
+ { event_type: 'step.delta' }
355
407
  >;
356
- const open = openBlocks.get(event.index);
408
+ let open = openBlocks.get(event.index);
357
409
  if (open == null) break;
358
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 img = 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 (img?.data != null && img.data.length > 0) {
457
+ controller.enqueue({
458
+ type: 'file',
459
+ mediaType: img.mime_type ?? 'image/png',
460
+ data: img.data,
461
+ ...(providerMetadata ? { providerMetadata } : {}),
462
+ });
463
+ } else if (img?.uri != null && img.uri.length > 0) {
464
+ /*
465
+ * V3 `LanguageModelV3File` only supports inline data (`string` /
466
+ * `Uint8Array`). URL-only image outputs cannot be represented as
467
+ * a file stream part on the v3 spec; surface the URI through
468
+ * provider metadata so callers can still recover it.
469
+ */
470
+ const uriProviderMetadata = {
471
+ google: {
472
+ ...(interactionId != null ? { interactionId } : {}),
473
+ imageUri: img.uri,
474
+ },
475
+ };
476
+ controller.enqueue({
477
+ type: 'file',
478
+ mediaType: img.mime_type ?? 'image/png',
479
+ data: '',
480
+ providerMetadata: uriProviderMetadata,
481
+ });
482
+ }
483
+ // The file part was emitted inline; clear any data on an
484
+ // eagerly-promoted image OpenBlockState so the `step.stop`
485
+ // handler does not emit a duplicate.
486
+ if (open.kind === 'image') {
487
+ open.data = undefined;
488
+ open.uri = undefined;
489
+ }
490
+ break;
491
+ }
492
+
359
493
  const delta = event.delta as
360
494
  | {
361
495
  type?: string;
@@ -363,8 +497,13 @@ export function buildGoogleInteractionsStreamTransform({
363
497
  signature?: string;
364
498
  content?: { type?: string; text?: string };
365
499
  id?: string;
366
- name?: string;
367
- arguments?: Record<string, unknown>;
500
+ /*
501
+ * `arguments` carries different shapes per delta kind:
502
+ * - `type: 'arguments_delta'` → `string` (partial JSON)
503
+ * - `type: '<builtin>_tool_call'` → `Record<string, unknown>`
504
+ * The branch handler reads it with the matching type.
505
+ */
506
+ arguments?: Record<string, unknown> | string;
368
507
  annotations?: Array<GoogleInteractionsAnnotation>;
369
508
  call_id?: string;
370
509
  result?: unknown;
@@ -372,6 +511,7 @@ export function buildGoogleInteractionsStreamTransform({
372
511
  data?: string;
373
512
  mime_type?: string;
374
513
  uri?: string;
514
+ name?: string;
375
515
  }
376
516
  | undefined;
377
517
 
@@ -386,7 +526,8 @@ export function buildGoogleInteractionsStreamTransform({
386
526
  }
387
527
  } else if (
388
528
  open.kind === 'text' &&
389
- delta?.type === 'text_annotation'
529
+ (delta?.type === 'text_annotation' ||
530
+ delta?.type === 'text_annotation_delta')
390
531
  ) {
391
532
  const sources = annotationsToSources({
392
533
  annotations: delta.annotations,
@@ -400,14 +541,6 @@ export function buildGoogleInteractionsStreamTransform({
400
541
  controller.enqueue(source);
401
542
  }
402
543
  } 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
544
  if (delta.data != null) open.data = delta.data;
412
545
  if (delta.mime_type != null) open.mimeType = delta.mime_type;
413
546
  if (delta.uri != null) open.uri = delta.uri;
@@ -429,47 +562,44 @@ export function buildGoogleInteractionsStreamTransform({
429
562
  }
430
563
  } else if (
431
564
  open.kind === 'function_call' &&
432
- delta?.type === 'function_call'
565
+ delta?.type === 'arguments_delta'
433
566
  ) {
434
567
  /*
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.
568
+ * Partial JSON arguments arrive as `arguments_delta` events.
569
+ * The partial JSON string lives in `delta.arguments` (a string,
570
+ * not the parsed object — the `arguments_delta` name applies to
571
+ * the discriminator only). Append to the accumulator and surface
572
+ * each chunk as a `tool-input-delta`; the full arguments object
573
+ * is emitted at `step.stop`.
445
574
  */
575
+ const slice =
576
+ typeof delta.arguments === 'string' ? delta.arguments : '';
577
+ if (slice.length > 0) {
578
+ open.argumentsAccum += slice;
579
+ controller.enqueue({
580
+ type: 'tool-input-delta',
581
+ id: open.toolCallId,
582
+ delta: slice,
583
+ });
584
+ }
446
585
  if (delta.id != null) {
447
586
  open.toolCallId = delta.id;
448
587
  }
449
- if (delta.name != null) {
450
- open.toolName = delta.name;
451
- }
452
- if (delta.arguments != null) {
453
- open.arguments = delta.arguments;
454
- }
455
588
  if (delta.signature != null) {
456
589
  open.signature = delta.signature;
457
590
  }
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
591
  hasFunctionCall = true;
467
592
  } else if (
468
593
  open.kind === 'builtin_tool_call' &&
469
594
  delta?.type === open.blockType
470
595
  ) {
471
596
  if (delta.id != null) open.toolCallId = delta.id;
472
- if (delta.arguments != null) open.arguments = delta.arguments;
597
+ if (
598
+ delta.arguments != null &&
599
+ typeof delta.arguments === 'object'
600
+ ) {
601
+ open.arguments = delta.arguments;
602
+ }
473
603
  if (
474
604
  delta.name != null &&
475
605
  open.blockType === 'mcp_server_tool_call'
@@ -493,10 +623,10 @@ export function buildGoogleInteractionsStreamTransform({
493
623
  break;
494
624
  }
495
625
 
496
- case 'content.stop': {
626
+ case 'step.stop': {
497
627
  const event = value as Extract<
498
628
  GoogleInteractionsEvent,
499
- { event_type: 'content.stop' }
629
+ { event_type: 'step.stop' }
500
630
  >;
501
631
  const open = openBlocks.get(event.index);
502
632
  if (open == null) break;
@@ -555,20 +685,8 @@ export function buildGoogleInteractionsStreamTransform({
555
685
  });
556
686
  }
557
687
  } 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
- });
688
+ const accumulated =
689
+ open.argumentsAccum.length > 0 ? open.argumentsAccum : '{}';
572
690
  controller.enqueue({
573
691
  type: 'tool-input-end',
574
692
  id: open.toolCallId,
@@ -581,8 +699,8 @@ export function buildGoogleInteractionsStreamTransform({
581
699
  controller.enqueue({
582
700
  type: 'tool-call',
583
701
  toolCallId: open.toolCallId,
584
- toolName,
585
- input: argsJson,
702
+ toolName: open.toolName,
703
+ input: accumulated,
586
704
  ...(providerMetadata ? { providerMetadata } : {}),
587
705
  });
588
706
  } else if (open.kind === 'builtin_tool_call' && !open.callEmitted) {
@@ -625,19 +743,32 @@ export function buildGoogleInteractionsStreamTransform({
625
743
  break;
626
744
  }
627
745
 
628
- case 'interaction.status_update': {
746
+ case 'interaction.status_update':
747
+ case 'interaction.in_progress':
748
+ case 'interaction.requires_action': {
629
749
  const event = value as Extract<
630
750
  GoogleInteractionsEvent,
631
- { event_type: 'interaction.status_update' }
751
+ {
752
+ event_type:
753
+ | 'interaction.status_update'
754
+ | 'interaction.in_progress'
755
+ | 'interaction.requires_action';
756
+ }
632
757
  >;
633
- finishStatus = event.status;
758
+ if (event.status != null) {
759
+ finishStatus = event.status;
760
+ } else if (eventType === 'interaction.requires_action') {
761
+ finishStatus = 'requires_action';
762
+ } else {
763
+ finishStatus = 'in_progress';
764
+ }
634
765
  break;
635
766
  }
636
767
 
637
- case 'interaction.complete': {
768
+ case 'interaction.completed': {
638
769
  const event = value as Extract<
639
770
  GoogleInteractionsEvent,
640
- { event_type: 'interaction.complete' }
771
+ { event_type: 'interaction.completed' }
641
772
  >;
642
773
  const interaction = event.interaction as {
643
774
  id?: string;
@@ -656,7 +787,7 @@ export function buildGoogleInteractionsStreamTransform({
656
787
  }
657
788
  /*
658
789
  * The Interactions API surfaces the applied service tier on
659
- * `interaction.complete.interaction.service_tier` (NOT on the
790
+ * `interaction.completed.interaction.service_tier` (NOT on the
660
791
  * `x-gemini-service-tier` HTTP header that `:generateContent`
661
792
  * uses). Body wins over header fallback.
662
793
  */