@ai-sdk/google 4.0.0-canary.63 → 4.0.0-canary.65

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>, LanguageModelV4StreamPart>`
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,84 @@ 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: { type: 'data', data: img.data },
461
+ ...(providerMetadata ? { providerMetadata } : {}),
462
+ });
463
+ } else if (img?.uri != null && img.uri.length > 0) {
464
+ controller.enqueue({
465
+ type: 'file',
466
+ mediaType: img.mime_type ?? 'image/png',
467
+ data: { type: 'url', url: new URL(img.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
+
359
481
  const delta = event.delta as
360
482
  | {
361
483
  type?: string;
@@ -363,8 +485,13 @@ export function buildGoogleInteractionsStreamTransform({
363
485
  signature?: string;
364
486
  content?: { type?: string; text?: string };
365
487
  id?: string;
366
- name?: string;
367
- arguments?: Record<string, unknown>;
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;
368
495
  annotations?: Array<GoogleInteractionsAnnotation>;
369
496
  call_id?: string;
370
497
  result?: unknown;
@@ -372,6 +499,7 @@ export function buildGoogleInteractionsStreamTransform({
372
499
  data?: string;
373
500
  mime_type?: string;
374
501
  uri?: string;
502
+ name?: string;
375
503
  }
376
504
  | undefined;
377
505
 
@@ -386,7 +514,8 @@ export function buildGoogleInteractionsStreamTransform({
386
514
  }
387
515
  } else if (
388
516
  open.kind === 'text' &&
389
- delta?.type === 'text_annotation'
517
+ (delta?.type === 'text_annotation' ||
518
+ delta?.type === 'text_annotation_delta')
390
519
  ) {
391
520
  const sources = annotationsToSources({
392
521
  annotations: delta.annotations,
@@ -400,14 +529,6 @@ export function buildGoogleInteractionsStreamTransform({
400
529
  controller.enqueue(source);
401
530
  }
402
531
  } 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
532
  if (delta.data != null) open.data = delta.data;
412
533
  if (delta.mime_type != null) open.mimeType = delta.mime_type;
413
534
  if (delta.uri != null) open.uri = delta.uri;
@@ -429,47 +550,44 @@ export function buildGoogleInteractionsStreamTransform({
429
550
  }
430
551
  } else if (
431
552
  open.kind === 'function_call' &&
432
- delta?.type === 'function_call'
553
+ delta?.type === 'arguments_delta'
433
554
  ) {
434
555
  /*
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.
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`.
445
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
+ }
446
573
  if (delta.id != null) {
447
574
  open.toolCallId = delta.id;
448
575
  }
449
- if (delta.name != null) {
450
- open.toolName = delta.name;
451
- }
452
- if (delta.arguments != null) {
453
- open.arguments = delta.arguments;
454
- }
455
576
  if (delta.signature != null) {
456
577
  open.signature = delta.signature;
457
578
  }
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
579
  hasFunctionCall = true;
467
580
  } else if (
468
581
  open.kind === 'builtin_tool_call' &&
469
582
  delta?.type === open.blockType
470
583
  ) {
471
584
  if (delta.id != null) open.toolCallId = delta.id;
472
- if (delta.arguments != null) open.arguments = delta.arguments;
585
+ if (
586
+ delta.arguments != null &&
587
+ typeof delta.arguments === 'object'
588
+ ) {
589
+ open.arguments = delta.arguments;
590
+ }
473
591
  if (
474
592
  delta.name != null &&
475
593
  open.blockType === 'mcp_server_tool_call'
@@ -493,10 +611,10 @@ export function buildGoogleInteractionsStreamTransform({
493
611
  break;
494
612
  }
495
613
 
496
- case 'content.stop': {
614
+ case 'step.stop': {
497
615
  const event = value as Extract<
498
616
  GoogleInteractionsEvent,
499
- { event_type: 'content.stop' }
617
+ { event_type: 'step.stop' }
500
618
  >;
501
619
  const open = openBlocks.get(event.index);
502
620
  if (open == null) break;
@@ -543,20 +661,8 @@ export function buildGoogleInteractionsStreamTransform({
543
661
  });
544
662
  }
545
663
  } else if (open.kind === 'function_call') {
546
- const toolName = open.toolName ?? 'unknown';
547
- const argsJson = JSON.stringify(open.arguments ?? {});
548
- if (!open.startEmitted) {
549
- controller.enqueue({
550
- type: 'tool-input-start',
551
- id: open.toolCallId,
552
- toolName,
553
- });
554
- }
555
- controller.enqueue({
556
- type: 'tool-input-delta',
557
- id: open.toolCallId,
558
- delta: argsJson,
559
- });
664
+ const accumulated =
665
+ open.argumentsAccum.length > 0 ? open.argumentsAccum : '{}';
560
666
  controller.enqueue({
561
667
  type: 'tool-input-end',
562
668
  id: open.toolCallId,
@@ -569,8 +675,8 @@ export function buildGoogleInteractionsStreamTransform({
569
675
  controller.enqueue({
570
676
  type: 'tool-call',
571
677
  toolCallId: open.toolCallId,
572
- toolName,
573
- input: argsJson,
678
+ toolName: open.toolName,
679
+ input: accumulated,
574
680
  ...(providerMetadata ? { providerMetadata } : {}),
575
681
  });
576
682
  } else if (open.kind === 'builtin_tool_call' && !open.callEmitted) {
@@ -613,19 +719,32 @@ export function buildGoogleInteractionsStreamTransform({
613
719
  break;
614
720
  }
615
721
 
616
- case 'interaction.status_update': {
722
+ case 'interaction.status_update':
723
+ case 'interaction.in_progress':
724
+ case 'interaction.requires_action': {
617
725
  const event = value as Extract<
618
726
  GoogleInteractionsEvent,
619
- { event_type: 'interaction.status_update' }
727
+ {
728
+ event_type:
729
+ | 'interaction.status_update'
730
+ | 'interaction.in_progress'
731
+ | 'interaction.requires_action';
732
+ }
620
733
  >;
621
- finishStatus = event.status;
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
+ }
622
741
  break;
623
742
  }
624
743
 
625
- case 'interaction.complete': {
744
+ case 'interaction.completed': {
626
745
  const event = value as Extract<
627
746
  GoogleInteractionsEvent,
628
- { event_type: 'interaction.complete' }
747
+ { event_type: 'interaction.completed' }
629
748
  >;
630
749
  const interaction = event.interaction as {
631
750
  id?: string;
@@ -644,7 +763,7 @@ export function buildGoogleInteractionsStreamTransform({
644
763
  }
645
764
  /*
646
765
  * The Interactions API surfaces the applied service tier on
647
- * `interaction.complete.interaction.service_tier` (NOT on the
766
+ * `interaction.completed.interaction.service_tier` (NOT on the
648
767
  * `x-gemini-service-tier` HTTP header that `:generateContent`
649
768
  * uses). Body wins over header fallback.
650
769
  */