@ai-sdk/google 4.0.3 → 4.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/google",
3
- "version": "4.0.3",
3
+ "version": "4.0.5",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@ai-sdk/provider": "4.0.1",
39
- "@ai-sdk/provider-utils": "5.0.2"
39
+ "@ai-sdk/provider-utils": "5.0.3"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "22.19.19",
@@ -11,7 +11,10 @@ import type {
11
11
  GoogleInteractionsEvent,
12
12
  GoogleInteractionsUsage,
13
13
  } from './google-interactions-api';
14
- import { convertGoogleInteractionsUsage } from './convert-google-interactions-usage';
14
+ import {
15
+ convertGoogleInteractionsUsage,
16
+ getGoogleInteractionsOutputTokensByModality,
17
+ } from './convert-google-interactions-usage';
15
18
  import {
16
19
  annotationsToSources,
17
20
  builtinToolResultToSources,
@@ -478,6 +481,41 @@ export function buildGoogleInteractionsStreamTransform({
478
481
  break;
479
482
  }
480
483
 
484
+ /*
485
+ * `video` deltas inside `model_output` carry the full payload in a
486
+ * single chunk (no per-byte streaming), mirroring `image`. Emit the
487
+ * `file` part as soon as the delta arrives so it surfaces regardless
488
+ * of whether a text block is currently open at the same index.
489
+ */
490
+ if (
491
+ dtype === 'video' &&
492
+ (open.kind === 'pending_model_output' || open.kind === 'text')
493
+ ) {
494
+ const videoDelta = event.delta as
495
+ | { data?: string; mime_type?: string; uri?: string }
496
+ | undefined;
497
+ const google: Record<string, string> = {};
498
+ if (interactionId != null) google.interactionId = interactionId;
499
+ const providerMetadata =
500
+ Object.keys(google).length > 0 ? { google } : undefined;
501
+ if (videoDelta?.data != null && videoDelta.data.length > 0) {
502
+ controller.enqueue({
503
+ type: 'file',
504
+ mediaType: videoDelta.mime_type ?? 'video/mp4',
505
+ data: { type: 'data', data: videoDelta.data },
506
+ ...(providerMetadata ? { providerMetadata } : {}),
507
+ });
508
+ } else if (videoDelta?.uri != null && videoDelta.uri.length > 0) {
509
+ controller.enqueue({
510
+ type: 'file',
511
+ mediaType: videoDelta.mime_type ?? 'video/mp4',
512
+ data: { type: 'url', url: new URL(videoDelta.uri) },
513
+ ...(providerMetadata ? { providerMetadata } : {}),
514
+ });
515
+ }
516
+ break;
517
+ }
518
+
481
519
  const delta = event.delta as
482
520
  | {
483
521
  type?: string;
@@ -800,10 +838,14 @@ export function buildGoogleInteractionsStreamTransform({
800
838
  raw: finishStatus,
801
839
  };
802
840
 
841
+ const outputTokensByModality =
842
+ getGoogleInteractionsOutputTokensByModality(usage);
843
+
803
844
  const providerMetadata: SharedV4ProviderMetadata = {
804
845
  google: {
805
846
  ...(interactionId != null ? { interactionId } : {}),
806
847
  ...(serviceTier != null ? { serviceTier } : {}),
848
+ ...(outputTokensByModality != null ? { outputTokensByModality } : {}),
807
849
  },
808
850
  };
809
851
 
@@ -45,3 +45,25 @@ export function convertGoogleInteractionsUsage(
45
45
  raw: usage as unknown as JSONObject,
46
46
  };
47
47
  }
48
+
49
+ /**
50
+ * Extracts the per-modality output token breakdown from an Interactions usage
51
+ * record (e.g. `{ video: 57920, text: 12 }`).
52
+ */
53
+ export function getGoogleInteractionsOutputTokensByModality(
54
+ usage: GoogleInteractionsUsage | undefined | null,
55
+ ): Record<string, number> | undefined {
56
+ const byModality = usage?.output_tokens_by_modality;
57
+ if (byModality == null) {
58
+ return undefined;
59
+ }
60
+
61
+ const result: Record<string, number> = {};
62
+ for (const entry of byModality) {
63
+ if (entry?.modality != null && entry.tokens != null) {
64
+ result[entry.modality] = entry.tokens;
65
+ }
66
+ }
67
+
68
+ return Object.keys(result).length > 0 ? result : undefined;
69
+ }
@@ -140,9 +140,19 @@ const contentBlockSchema = () => {
140
140
  })
141
141
  .loose();
142
142
 
143
+ const videoContent = z
144
+ .object({
145
+ type: z.literal('video'),
146
+ data: z.string().nullish(),
147
+ mime_type: z.string().nullish(),
148
+ uri: z.string().nullish(),
149
+ })
150
+ .loose();
151
+
143
152
  return z.union([
144
153
  textContent,
145
154
  imageContent,
155
+ videoContent,
146
156
  z.object({ type: z.string() }).loose(),
147
157
  ]);
148
158
  };
@@ -384,6 +394,19 @@ export const googleInteractionsEventSchema = lazySchema(() =>
384
394
  })
385
395
  .loose();
386
396
 
397
+ /*
398
+ * `video` deltas carry the entire payload per delta (`data` base64 +
399
+ * `mime_type`, or `uri`) — there is no per-byte streaming.
400
+ */
401
+ const stepDeltaVideo = z
402
+ .object({
403
+ type: z.literal('video'),
404
+ data: z.string().nullish(),
405
+ mime_type: z.string().nullish(),
406
+ uri: z.string().nullish(),
407
+ })
408
+ .loose();
409
+
387
410
  /*
388
411
  * Built-in tool call/result step deltas mirror the shape of their step
389
412
  * counterparts (full payload per delta — there is no per-token
@@ -419,6 +442,7 @@ export const googleInteractionsEventSchema = lazySchema(() =>
419
442
  const stepDeltaUnion = z.union([
420
443
  stepDeltaText,
421
444
  stepDeltaImage,
445
+ stepDeltaVideo,
422
446
  stepDeltaThoughtSummary,
423
447
  stepDeltaThoughtSignature,
424
448
  stepDeltaArgumentsDelta,
@@ -23,7 +23,10 @@ import {
23
23
  } from '@ai-sdk/provider-utils';
24
24
  import { googleFailedResponseHandler } from '../google-error';
25
25
  import { buildGoogleInteractionsStreamTransform } from './build-google-interactions-stream-transform';
26
- import { convertGoogleInteractionsUsage } from './convert-google-interactions-usage';
26
+ import {
27
+ convertGoogleInteractionsUsage,
28
+ getGoogleInteractionsOutputTokensByModality,
29
+ } from './convert-google-interactions-usage';
27
30
  import { convertToGoogleInteractionsInput } from './convert-to-google-interactions-input';
28
31
  import {
29
32
  googleInteractionsEventSchema,
@@ -546,10 +549,15 @@ export class GoogleInteractionsLanguageModel implements LanguageModelV4 {
546
549
  * `response.id` is omitted when `store: false` (fully stateless mode), so
547
550
  * `interactionId` is only surfaced when the API actually returned one.
548
551
  */
552
+ const outputTokensByModality = getGoogleInteractionsOutputTokensByModality(
553
+ response.usage,
554
+ );
555
+
549
556
  const providerMetadata: SharedV4ProviderMetadata = {
550
557
  google: {
551
558
  ...(interactionId != null ? { interactionId } : {}),
552
559
  ...(serviceTier != null ? { serviceTier } : {}),
560
+ ...(outputTokensByModality != null ? { outputTokensByModality } : {}),
553
561
  },
554
562
  };
555
563
 
@@ -15,6 +15,14 @@ export type GoogleInteractionsProviderMetadata = {
15
15
  */
16
16
  serviceTier?: string;
17
17
 
18
+ /**
19
+ * Output token counts keyed by modality (e.g. `{ video: 57920 }`), sourced
20
+ * from the Interactions API `output_tokens_by_modality`. Present only when
21
+ * the response reports a breakdown. Preview surface for per-modality billing;
22
+ * may be promoted to a first-class usage field later.
23
+ */
24
+ outputTokensByModality?: Record<string, number>;
25
+
18
26
  /**
19
27
  * Per-block signature hash for backend validation. Set by the SDK on output
20
28
  * reasoning / tool-call parts and round-tripped on input parts.
@@ -151,6 +151,27 @@ export function parseGoogleInteractionsOutputs({
151
151
  ...googleProviderMetadata({ interactionId }),
152
152
  });
153
153
  }
154
+ } else if (blockType === 'video') {
155
+ const video = block as {
156
+ data?: string;
157
+ mime_type?: string;
158
+ uri?: string;
159
+ };
160
+ if (video.data != null && video.data.length > 0) {
161
+ content.push({
162
+ type: 'file',
163
+ mediaType: video.mime_type ?? 'video/mp4',
164
+ data: { type: 'data', data: video.data },
165
+ ...googleProviderMetadata({ interactionId }),
166
+ });
167
+ } else if (video.uri != null && video.uri.length > 0) {
168
+ content.push({
169
+ type: 'file',
170
+ mediaType: video.mime_type ?? 'video/mp4',
171
+ data: { type: 'url', url: new URL(video.uri) },
172
+ ...googleProviderMetadata({ interactionId }),
173
+ });
174
+ }
154
175
  }
155
176
  }
156
177
  break;