@ai-sdk/google 4.0.2 → 4.0.4

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.2",
3
+ "version": "4.0.4",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -35,16 +35,16 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@ai-sdk/provider": "4.0.0",
39
- "@ai-sdk/provider-utils": "5.0.1"
38
+ "@ai-sdk/provider": "4.0.1",
39
+ "@ai-sdk/provider-utils": "5.0.2"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "22.19.19",
43
43
  "tsup": "^8.5.1",
44
44
  "typescript": "5.8.3",
45
45
  "zod": "3.25.76",
46
- "@vercel/ai-tsconfig": "0.0.0",
47
- "@ai-sdk/test-server": "2.0.0"
46
+ "@ai-sdk/test-server": "2.0.0",
47
+ "@vercel/ai-tsconfig": "0.0.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "zod": "^3.25.76 || ^4.1.8"
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AISDKError,
3
3
  type Experimental_VideoModelV4,
4
+ type Experimental_VideoModelV4File,
4
5
  type SharedV4Warning,
5
6
  } from '@ai-sdk/provider';
6
7
  import {
@@ -35,6 +36,103 @@ interface GoogleVideoModelConfig {
35
36
  };
36
37
  }
37
38
 
39
+ function getFirstFrameImage(
40
+ options: Parameters<Experimental_VideoModelV4['doGenerate']>[0],
41
+ ): Experimental_VideoModelV4File | undefined {
42
+ return options.frameImages?.find(frame => frame.frameType === 'first_frame')
43
+ ?.image;
44
+ }
45
+
46
+ function resolveStartImage(
47
+ options: Parameters<Experimental_VideoModelV4['doGenerate']>[0],
48
+ ): Experimental_VideoModelV4File | undefined {
49
+ return getFirstFrameImage(options) ?? options.image;
50
+ }
51
+
52
+ function getLastFrameImage(
53
+ options: Parameters<Experimental_VideoModelV4['doGenerate']>[0],
54
+ ): Experimental_VideoModelV4File | undefined {
55
+ return options.frameImages?.find(frame => frame.frameType === 'last_frame')
56
+ ?.image;
57
+ }
58
+
59
+ function getInputReferences(
60
+ options: Parameters<Experimental_VideoModelV4['doGenerate']>[0],
61
+ ): Array<Experimental_VideoModelV4File> | undefined {
62
+ if (options.frameImages != null && options.frameImages.length > 0) {
63
+ return undefined;
64
+ }
65
+
66
+ return options.inputReferences != null && options.inputReferences.length > 0
67
+ ? options.inputReferences
68
+ : undefined;
69
+ }
70
+
71
+ function convertFileToGoogleImage(
72
+ file: Experimental_VideoModelV4File,
73
+ warnings: SharedV4Warning[],
74
+ ): Record<string, unknown> | undefined {
75
+ if (file.type === 'url') {
76
+ if (file.url.startsWith('gs://')) {
77
+ return {
78
+ gcsUri: file.url,
79
+ mimeType: 'image/png',
80
+ };
81
+ }
82
+
83
+ warnings.push({
84
+ type: 'unsupported',
85
+ feature: 'URL-based image input',
86
+ details:
87
+ 'Google Generative AI video models require base64-encoded images or GCS URIs. URL will be ignored.',
88
+ });
89
+ return undefined;
90
+ }
91
+
92
+ const base64Data =
93
+ typeof file.data === 'string'
94
+ ? file.data
95
+ : convertUint8ArrayToBase64(file.data);
96
+
97
+ // The Gemini Developer API (generativelanguage.googleapis.com) requires
98
+ // inline image bytes wrapped as `inlineData`.
99
+ return {
100
+ inlineData: {
101
+ mimeType: file.mediaType || 'image/png',
102
+ data: base64Data,
103
+ },
104
+ };
105
+ }
106
+
107
+ function convertProviderReferenceImage(
108
+ refImg: NonNullable<GoogleVideoModelOptions['referenceImages']>[number],
109
+ ): Record<string, unknown> {
110
+ if (refImg.bytesBase64Encoded) {
111
+ return {
112
+ inlineData: {
113
+ mimeType: 'image/png',
114
+ data: refImg.bytesBase64Encoded,
115
+ },
116
+ };
117
+ }
118
+
119
+ if (refImg.gcsUri) {
120
+ return {
121
+ gcsUri: refImg.gcsUri,
122
+ };
123
+ }
124
+
125
+ return refImg;
126
+ }
127
+
128
+ function convertInputReferenceImage(
129
+ file: Experimental_VideoModelV4File,
130
+ warnings: SharedV4Warning[],
131
+ ): Record<string, unknown> | undefined {
132
+ const image = convertFileToGoogleImage(file, warnings);
133
+ return image != null ? { image, referenceType: 'asset' } : undefined;
134
+ }
135
+
38
136
  export class GoogleVideoModel implements Experimental_VideoModelV4 {
39
137
  readonly specificationVersion = 'v4';
40
138
 
@@ -71,46 +169,32 @@ export class GoogleVideoModel implements Experimental_VideoModelV4 {
71
169
  instance.prompt = options.prompt;
72
170
  }
73
171
 
74
- // Handle image-to-video: convert image to base64
75
- if (options.image != null) {
76
- if (options.image.type === 'url') {
77
- warnings.push({
78
- type: 'unsupported',
79
- feature: 'URL-based image input',
80
- details:
81
- 'Google Generative AI video models require base64-encoded images. URL will be ignored.',
82
- });
83
- } else {
84
- const base64Data =
85
- typeof options.image.data === 'string'
86
- ? options.image.data
87
- : convertUint8ArrayToBase64(options.image.data);
88
-
89
- instance.image = {
90
- inlineData: {
91
- mimeType: options.image.mediaType || 'image/png',
92
- data: base64Data,
93
- },
94
- };
172
+ const startImage = resolveStartImage(options);
173
+ if (startImage != null) {
174
+ const image = convertFileToGoogleImage(startImage, warnings);
175
+ if (image != null) {
176
+ instance.image = image;
95
177
  }
96
178
  }
97
179
 
98
- if (googleOptions?.referenceImages != null) {
99
- instance.referenceImages = googleOptions.referenceImages.map(refImg => {
100
- if (refImg.bytesBase64Encoded) {
101
- return {
102
- inlineData: {
103
- mimeType: 'image/png',
104
- data: refImg.bytesBase64Encoded,
105
- },
106
- };
107
- } else if (refImg.gcsUri) {
108
- return {
109
- gcsUri: refImg.gcsUri,
110
- };
111
- }
112
- return refImg;
180
+ const lastFrameImage = getLastFrameImage(options);
181
+ if (lastFrameImage != null) {
182
+ const lastFrame = convertFileToGoogleImage(lastFrameImage, warnings);
183
+ if (lastFrame != null) {
184
+ instance.lastFrame = lastFrame;
185
+ }
186
+ }
187
+
188
+ const inputReferences = getInputReferences(options);
189
+ if (inputReferences != null) {
190
+ instance.referenceImages = inputReferences.flatMap(reference => {
191
+ const converted = convertInputReferenceImage(reference, warnings);
192
+ return converted != null ? [converted] : [];
113
193
  });
194
+ } else if (googleOptions?.referenceImages != null) {
195
+ instance.referenceImages = googleOptions.referenceImages.map(refImg =>
196
+ convertProviderReferenceImage(refImg),
197
+ );
114
198
  }
115
199
 
116
200
  const parameters: Record<string, unknown> = {
@@ -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;