@ai-sdk/google 3.0.19 → 3.0.21

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.
@@ -0,0 +1,374 @@
1
+ import {
2
+ AISDKError,
3
+ type Experimental_VideoModelV3,
4
+ type SharedV3Warning,
5
+ } from '@ai-sdk/provider';
6
+ import {
7
+ combineHeaders,
8
+ convertUint8ArrayToBase64,
9
+ createJsonResponseHandler,
10
+ delay,
11
+ type FetchFunction,
12
+ getFromApi,
13
+ lazySchema,
14
+ parseProviderOptions,
15
+ postJsonToApi,
16
+ type Resolvable,
17
+ resolve,
18
+ zodSchema,
19
+ } from '@ai-sdk/provider-utils';
20
+ import { z } from 'zod/v4';
21
+ import { googleFailedResponseHandler } from './google-error';
22
+ import type { GoogleGenerativeAIVideoModelId } from './google-generative-ai-video-settings';
23
+
24
+ export type GoogleGenerativeAIVideoProviderOptions = {
25
+ // Polling configuration
26
+ pollIntervalMs?: number | null;
27
+ pollTimeoutMs?: number | null;
28
+
29
+ // Video generation options
30
+ personGeneration?: 'dont_allow' | 'allow_adult' | 'allow_all' | null;
31
+ negativePrompt?: string | null;
32
+
33
+ // Reference images (for style/asset reference)
34
+ referenceImages?: Array<{
35
+ bytesBase64Encoded?: string;
36
+ gcsUri?: string;
37
+ }> | null;
38
+
39
+ [key: string]: unknown; // For passthrough
40
+ };
41
+
42
+ interface GoogleGenerativeAIVideoModelConfig {
43
+ provider: string;
44
+ baseURL: string;
45
+ headers?: Resolvable<Record<string, string | undefined>>;
46
+ fetch?: FetchFunction;
47
+ generateId?: () => string;
48
+ _internal?: {
49
+ currentDate?: () => Date;
50
+ };
51
+ }
52
+
53
+ export class GoogleGenerativeAIVideoModel implements Experimental_VideoModelV3 {
54
+ readonly specificationVersion = 'v3';
55
+
56
+ get provider(): string {
57
+ return this.config.provider;
58
+ }
59
+
60
+ get maxVideosPerCall(): number {
61
+ // Google supports multiple videos via sampleCount
62
+ return 4;
63
+ }
64
+
65
+ constructor(
66
+ readonly modelId: GoogleGenerativeAIVideoModelId,
67
+ private readonly config: GoogleGenerativeAIVideoModelConfig,
68
+ ) {}
69
+
70
+ async doGenerate(
71
+ options: Parameters<Experimental_VideoModelV3['doGenerate']>[0],
72
+ ): Promise<Awaited<ReturnType<Experimental_VideoModelV3['doGenerate']>>> {
73
+ const currentDate = this.config._internal?.currentDate?.() ?? new Date();
74
+ const warnings: SharedV3Warning[] = [];
75
+
76
+ const googleOptions = (await parseProviderOptions({
77
+ provider: 'google',
78
+ providerOptions: options.providerOptions,
79
+ schema: googleVideoProviderOptionsSchema,
80
+ })) as GoogleGenerativeAIVideoProviderOptions | undefined;
81
+
82
+ const instances: Array<Record<string, unknown>> = [{}];
83
+ const instance = instances[0];
84
+
85
+ if (options.prompt != null) {
86
+ instance.prompt = options.prompt;
87
+ }
88
+
89
+ // Handle image-to-video: convert image to base64
90
+ if (options.image != null) {
91
+ if (options.image.type === 'url') {
92
+ warnings.push({
93
+ type: 'unsupported',
94
+ feature: 'URL-based image input',
95
+ details:
96
+ 'Google Generative AI video models require base64-encoded images. URL will be ignored.',
97
+ });
98
+ } else {
99
+ const base64Data =
100
+ typeof options.image.data === 'string'
101
+ ? options.image.data
102
+ : convertUint8ArrayToBase64(options.image.data);
103
+
104
+ instance.image = {
105
+ inlineData: {
106
+ mimeType: options.image.mediaType || 'image/png',
107
+ data: base64Data,
108
+ },
109
+ };
110
+ }
111
+ }
112
+
113
+ if (googleOptions?.referenceImages != null) {
114
+ instance.referenceImages = googleOptions.referenceImages.map(refImg => {
115
+ if (refImg.bytesBase64Encoded) {
116
+ return {
117
+ inlineData: {
118
+ mimeType: 'image/png',
119
+ data: refImg.bytesBase64Encoded,
120
+ },
121
+ };
122
+ } else if (refImg.gcsUri) {
123
+ return {
124
+ gcsUri: refImg.gcsUri,
125
+ };
126
+ }
127
+ return refImg;
128
+ });
129
+ }
130
+
131
+ const parameters: Record<string, unknown> = {
132
+ sampleCount: options.n,
133
+ };
134
+
135
+ if (options.aspectRatio) {
136
+ parameters.aspectRatio = options.aspectRatio;
137
+ }
138
+
139
+ if (options.resolution) {
140
+ const resolutionMap: Record<string, string> = {
141
+ '1280x720': '720p',
142
+ '1920x1080': '1080p',
143
+ '3840x2160': '4k',
144
+ };
145
+ parameters.resolution =
146
+ resolutionMap[options.resolution] || options.resolution;
147
+ }
148
+
149
+ if (options.duration) {
150
+ parameters.durationSeconds = options.duration;
151
+ }
152
+
153
+ if (options.seed) {
154
+ parameters.seed = options.seed;
155
+ }
156
+
157
+ if (googleOptions != null) {
158
+ const opts = googleOptions as GoogleGenerativeAIVideoProviderOptions;
159
+
160
+ if (
161
+ opts.personGeneration !== undefined &&
162
+ opts.personGeneration !== null
163
+ ) {
164
+ parameters.personGeneration = opts.personGeneration;
165
+ }
166
+ if (opts.negativePrompt !== undefined && opts.negativePrompt !== null) {
167
+ parameters.negativePrompt = opts.negativePrompt;
168
+ }
169
+
170
+ for (const [key, value] of Object.entries(opts)) {
171
+ if (
172
+ ![
173
+ 'pollIntervalMs',
174
+ 'pollTimeoutMs',
175
+ 'personGeneration',
176
+ 'negativePrompt',
177
+ 'referenceImages',
178
+ ].includes(key)
179
+ ) {
180
+ parameters[key] = value;
181
+ }
182
+ }
183
+ }
184
+
185
+ const { value: operation } = await postJsonToApi({
186
+ url: `${this.config.baseURL}/models/${this.modelId}:predictLongRunning`,
187
+ headers: combineHeaders(
188
+ await resolve(this.config.headers),
189
+ options.headers,
190
+ ),
191
+ body: {
192
+ instances,
193
+ parameters,
194
+ },
195
+ successfulResponseHandler: createJsonResponseHandler(
196
+ googleOperationSchema,
197
+ ),
198
+ failedResponseHandler: googleFailedResponseHandler,
199
+ abortSignal: options.abortSignal,
200
+ fetch: this.config.fetch,
201
+ });
202
+
203
+ const operationName = operation.name;
204
+ if (!operationName) {
205
+ throw new AISDKError({
206
+ name: 'GOOGLE_VIDEO_GENERATION_ERROR',
207
+ message: 'No operation name returned from API',
208
+ });
209
+ }
210
+
211
+ const pollIntervalMs = googleOptions?.pollIntervalMs ?? 10000; // 10 seconds (per Google docs)
212
+ const pollTimeoutMs = googleOptions?.pollTimeoutMs ?? 600000; // 10 minutes
213
+
214
+ const startTime = Date.now();
215
+ let finalOperation = operation;
216
+ let responseHeaders: Record<string, string> | undefined;
217
+
218
+ while (!finalOperation.done) {
219
+ if (Date.now() - startTime > pollTimeoutMs) {
220
+ throw new AISDKError({
221
+ name: 'GOOGLE_VIDEO_GENERATION_TIMEOUT',
222
+ message: `Video generation timed out after ${pollTimeoutMs}ms`,
223
+ });
224
+ }
225
+
226
+ await delay(pollIntervalMs);
227
+
228
+ if (options.abortSignal?.aborted) {
229
+ throw new AISDKError({
230
+ name: 'GOOGLE_VIDEO_GENERATION_ABORTED',
231
+ message: 'Video generation request was aborted',
232
+ });
233
+ }
234
+
235
+ const { value: statusOperation, responseHeaders: pollHeaders } =
236
+ await getFromApi({
237
+ url: `${this.config.baseURL}/${operationName}`,
238
+ headers: combineHeaders(
239
+ await resolve(this.config.headers),
240
+ options.headers,
241
+ ),
242
+ successfulResponseHandler: createJsonResponseHandler(
243
+ googleOperationSchema,
244
+ ),
245
+ failedResponseHandler: googleFailedResponseHandler,
246
+ abortSignal: options.abortSignal,
247
+ fetch: this.config.fetch,
248
+ });
249
+
250
+ finalOperation = statusOperation;
251
+ responseHeaders = pollHeaders;
252
+ }
253
+
254
+ if (finalOperation.error) {
255
+ throw new AISDKError({
256
+ name: 'GOOGLE_VIDEO_GENERATION_FAILED',
257
+ message: `Video generation failed: ${finalOperation.error.message}`,
258
+ });
259
+ }
260
+
261
+ const response = finalOperation.response;
262
+ if (
263
+ !response?.generateVideoResponse?.generatedSamples ||
264
+ response.generateVideoResponse.generatedSamples.length === 0
265
+ ) {
266
+ throw new AISDKError({
267
+ name: 'GOOGLE_VIDEO_GENERATION_ERROR',
268
+ message: `No videos in response. Response: ${JSON.stringify(finalOperation)}`,
269
+ });
270
+ }
271
+
272
+ const videos: Array<{ type: 'url'; url: string; mediaType: string }> = [];
273
+ const videoMetadata: Array<{ uri: string }> = [];
274
+
275
+ // Get API key from headers to append to download URLs
276
+ const resolvedHeaders = await resolve(this.config.headers);
277
+ const apiKey = resolvedHeaders?.['x-goog-api-key'];
278
+
279
+ for (const generatedSample of response.generateVideoResponse
280
+ .generatedSamples) {
281
+ if (generatedSample.video?.uri) {
282
+ // Append API key to URL for authentication during download
283
+ const urlWithAuth = apiKey
284
+ ? `${generatedSample.video.uri}${generatedSample.video.uri.includes('?') ? '&' : '?'}key=${apiKey}`
285
+ : generatedSample.video.uri;
286
+
287
+ videos.push({
288
+ type: 'url',
289
+ url: urlWithAuth,
290
+ mediaType: 'video/mp4',
291
+ });
292
+ videoMetadata.push({
293
+ uri: generatedSample.video.uri,
294
+ });
295
+ }
296
+ }
297
+
298
+ if (videos.length === 0) {
299
+ throw new AISDKError({
300
+ name: 'GOOGLE_VIDEO_GENERATION_ERROR',
301
+ message: 'No valid videos in response',
302
+ });
303
+ }
304
+
305
+ return {
306
+ videos,
307
+ warnings,
308
+ response: {
309
+ timestamp: currentDate,
310
+ modelId: this.modelId,
311
+ headers: responseHeaders,
312
+ },
313
+ providerMetadata: {
314
+ google: {
315
+ videos: videoMetadata,
316
+ },
317
+ },
318
+ };
319
+ }
320
+ }
321
+
322
+ const googleOperationSchema = z.object({
323
+ name: z.string().nullish(),
324
+ done: z.boolean().nullish(),
325
+ error: z
326
+ .object({
327
+ code: z.number().nullish(),
328
+ message: z.string(),
329
+ status: z.string().nullish(),
330
+ })
331
+ .nullish(),
332
+ response: z
333
+ .object({
334
+ generateVideoResponse: z
335
+ .object({
336
+ generatedSamples: z
337
+ .array(
338
+ z.object({
339
+ video: z
340
+ .object({
341
+ uri: z.string().nullish(),
342
+ })
343
+ .nullish(),
344
+ }),
345
+ )
346
+ .nullish(),
347
+ })
348
+ .nullish(),
349
+ })
350
+ .nullish(),
351
+ });
352
+
353
+ const googleVideoProviderOptionsSchema = lazySchema(() =>
354
+ zodSchema(
355
+ z
356
+ .object({
357
+ pollIntervalMs: z.number().positive().nullish(),
358
+ pollTimeoutMs: z.number().positive().nullish(),
359
+ personGeneration: z
360
+ .enum(['dont_allow', 'allow_adult', 'allow_all'])
361
+ .nullish(),
362
+ negativePrompt: z.string().nullish(),
363
+ referenceImages: z
364
+ .array(
365
+ z.object({
366
+ bytesBase64Encoded: z.string().nullish(),
367
+ gcsUri: z.string().nullish(),
368
+ }),
369
+ )
370
+ .nullish(),
371
+ })
372
+ .passthrough(),
373
+ ),
374
+ );
@@ -0,0 +1,6 @@
1
+ export type GoogleGenerativeAIVideoModelId =
2
+ | 'veo-3.1-fast-generate-preview'
3
+ | 'veo-3.1-generate-preview'
4
+ | 'veo-3.1-generate'
5
+ | 'veo-2.0-generate-001'
6
+ | (string & {});
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  EmbeddingModelV3,
3
+ Experimental_VideoModelV3,
4
+ ImageModelV3,
3
5
  LanguageModelV3,
4
6
  ProviderV3,
5
- ImageModelV3,
6
7
  } from '@ai-sdk/provider';
7
8
  import {
8
9
  FetchFunction,
@@ -23,6 +24,8 @@ import {
23
24
  GoogleGenerativeAIImageModelId,
24
25
  } from './google-generative-ai-image-settings';
25
26
  import { GoogleGenerativeAIImageModel } from './google-generative-ai-image-model';
27
+ import { GoogleGenerativeAIVideoModel } from './google-generative-ai-video-model';
28
+ import { GoogleGenerativeAIVideoModelId } from './google-generative-ai-video-settings';
26
29
 
27
30
  export interface GoogleGenerativeAIProvider extends ProviderV3 {
28
31
  (modelId: GoogleGenerativeAIModelId): LanguageModelV3;
@@ -39,6 +42,11 @@ export interface GoogleGenerativeAIProvider extends ProviderV3 {
39
42
  settings?: GoogleGenerativeAIImageSettings,
40
43
  ): ImageModelV3;
41
44
 
45
+ /**
46
+ * Creates a model for video generation.
47
+ */
48
+ video(modelId: GoogleGenerativeAIVideoModelId): Experimental_VideoModelV3;
49
+
42
50
  /**
43
51
  * @deprecated Use `chat()` instead.
44
52
  */
@@ -170,6 +178,15 @@ export function createGoogleGenerativeAI(
170
178
  fetch: options.fetch,
171
179
  });
172
180
 
181
+ const createVideoModel = (modelId: GoogleGenerativeAIVideoModelId) =>
182
+ new GoogleGenerativeAIVideoModel(modelId, {
183
+ provider: providerName,
184
+ baseURL,
185
+ headers: getHeaders,
186
+ fetch: options.fetch,
187
+ generateId: options.generateId ?? generateId,
188
+ });
189
+
173
190
  const provider = function (modelId: GoogleGenerativeAIModelId) {
174
191
  if (new.target) {
175
192
  throw new Error(
@@ -190,6 +207,7 @@ export function createGoogleGenerativeAI(
190
207
  provider.textEmbeddingModel = createEmbeddingModel;
191
208
  provider.image = createImageModel;
192
209
  provider.imageModel = createImageModel;
210
+ provider.video = createVideoModel;
193
211
  provider.tools = googleTools;
194
212
 
195
213
  return provider as GoogleGenerativeAIProvider;
package/src/index.ts CHANGED
@@ -3,6 +3,8 @@ export type { GoogleGenerativeAIProviderOptions } from './google-generative-ai-o
3
3
  export type { GoogleGenerativeAIProviderMetadata } from './google-generative-ai-prompt';
4
4
  export type { GoogleGenerativeAIImageProviderOptions } from './google-generative-ai-image-model';
5
5
  export type { GoogleGenerativeAIEmbeddingProviderOptions } from './google-generative-ai-embedding-options';
6
+ export type { GoogleGenerativeAIVideoProviderOptions } from './google-generative-ai-video-model';
7
+ export type { GoogleGenerativeAIVideoModelId } from './google-generative-ai-video-settings';
6
8
  export { createGoogleGenerativeAI, google } from './google-provider';
7
9
  export type {
8
10
  GoogleGenerativeAIProvider,