ai 7.0.49 → 7.0.50

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.
@@ -2,12 +2,15 @@ import type {
2
2
  Experimental_VideoModelV4,
3
3
  Experimental_VideoModelV4CallOptions,
4
4
  Experimental_VideoModelV4File,
5
+ Experimental_VideoModelV4Result,
6
+ Experimental_VideoModelV4OperationWebhook,
5
7
  Experimental_VideoModelV4FrameImage,
6
8
  Experimental_VideoModelV4FrameType,
7
9
  SharedV4ProviderMetadata,
8
10
  } from '@ai-sdk/provider';
9
11
  import {
10
12
  convertBase64ToUint8Array,
13
+ delay as defaultDelay,
11
14
  withUserAgentSuffix,
12
15
  type DataContent,
13
16
  detectMediaType,
@@ -19,6 +22,7 @@ import {
19
22
  type GeneratedFile,
20
23
  } from '../generate-text/generated-file';
21
24
  import { logWarnings } from '../logger/log-warnings';
25
+ import { mergeAbortSignals } from '../util/merge-abort-signals';
22
26
  import { resolveVideoModel } from '../model/resolve-model';
23
27
  import type { VideoModel } from '../types/video-model';
24
28
  import type { VideoModelResponseMetadata } from '../types/video-model-response-metadata';
@@ -36,6 +40,52 @@ export type GenerateVideoPrompt =
36
40
  text?: string;
37
41
  };
38
42
 
43
+ /**
44
+ * Polling configuration for models that support the asynchronous
45
+ * start/status flow.
46
+ *
47
+ * When used with `webhook`, `timeoutMs` also limits how long the SDK waits for
48
+ * the webhook notification. If the model does not support webhooks, these
49
+ * options configure the automatic polling fallback.
50
+ */
51
+ export type GenerateVideoPollOptions = {
52
+ /**
53
+ * Interval between status checks in milliseconds.
54
+ *
55
+ * @default 5000
56
+ */
57
+ intervalMs?: number;
58
+
59
+ /**
60
+ * Maximum time to wait for completion in milliseconds.
61
+ *
62
+ * @default 600000 (10 minutes)
63
+ */
64
+ timeoutMs?: number;
65
+
66
+ /**
67
+ * Custom delay implementation for polling intervals and webhook timeouts.
68
+ * This can be used with durable workflow sleep functions.
69
+ *
70
+ * @default the built-in timer-based delay
71
+ */
72
+ delay?: (
73
+ delayInMs: number,
74
+ options?: { abortSignal?: AbortSignal },
75
+ ) => PromiseLike<void>;
76
+ };
77
+
78
+ /**
79
+ * Webhook factory for models that support the asynchronous start/status flow.
80
+ *
81
+ * The factory should return a URL for the provider to send notifications to,
82
+ * and a `received` promise that resolves when the notification arrives.
83
+ */
84
+ export type GenerateVideoWebhookFactory = () => PromiseLike<{
85
+ url: string;
86
+ received: PromiseLike<Experimental_VideoModelV4OperationWebhook>;
87
+ }>;
88
+
39
89
  /**
40
90
  * Generates videos using a video model.
41
91
  *
@@ -55,6 +105,8 @@ export type GenerateVideoPrompt =
55
105
  * @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2.
56
106
  * @param abortSignal - An optional abort signal that can be used to cancel the call.
57
107
  * @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
108
+ * @param poll - Polling configuration for models that support the start/status flow.
109
+ * @param webhook - Webhook factory for models that support the start/status flow.
58
110
  *
59
111
  * @returns A result object that contains the generated videos.
60
112
  */
@@ -78,6 +130,8 @@ export async function experimental_generateVideo({
78
130
  abortSignal,
79
131
  headers,
80
132
  download: downloadFn = defaultDownload,
133
+ poll,
134
+ webhook,
81
135
  }: {
82
136
  /**
83
137
  * The video model to use.
@@ -105,7 +159,7 @@ export async function experimental_generateVideo({
105
159
  aspectRatio?: `${number}:${number}`;
106
160
 
107
161
  /**
108
- * Resolution of the videos to generate. Must have the format `{width}x{height}`.
162
+ * Resolution of the videos to generate. Must have the format `{width}x${height}`.
109
163
  */
110
164
  resolution?: `${number}x${number}`;
111
165
 
@@ -200,6 +254,29 @@ export async function experimental_generateVideo({
200
254
  url: URL;
201
255
  abortSignal?: AbortSignal;
202
256
  }) => Promise<{ data: Uint8Array; mediaType: string | undefined }>;
257
+
258
+ /**
259
+ * Polling configuration for models that support the asynchronous
260
+ * start/status flow. When provided and the model implements `doStart`
261
+ * and `doStatus`, the SDK will orchestrate polling automatically.
262
+ *
263
+ * This option can be combined with `webhook`: `timeoutMs` limits the webhook
264
+ * wait, and the polling settings apply if the model does not support
265
+ * webhooks.
266
+ */
267
+ poll?: GenerateVideoPollOptions;
268
+
269
+ /**
270
+ * Webhook factory for models that support the asynchronous
271
+ * start/status flow. When provided and the model implements `doStart`
272
+ * and `doStatus`, the SDK will use webhooks instead of polling.
273
+ *
274
+ * The factory should return a URL for the provider to send notifications to,
275
+ * and a `received` promise that resolves when the notification arrives.
276
+ * `poll` can also be provided to configure the webhook timeout and polling
277
+ * fallback.
278
+ */
279
+ webhook?: GenerateVideoWebhookFactory;
203
280
  }): Promise<GenerateVideoResult> {
204
281
  const model = resolveVideoModel(modelArg);
205
282
 
@@ -270,6 +347,34 @@ export async function experimental_generateVideo({
270
347
  const maxVideosPerCallWithDefault =
271
348
  maxVideosPerCall ?? (await invokeModelMaxVideosPerCall(model)) ?? 1;
272
349
 
350
+ // Determine whether to use the start/status flow:
351
+ const hasStartStatus = model.doStart != null && model.doStatus != null;
352
+ const useStartStatus =
353
+ hasStartStatus &&
354
+ (poll != null || webhook != null || model.doGenerate == null);
355
+
356
+ // Validate model capabilities
357
+ if (model.doGenerate == null && !hasStartStatus) {
358
+ throw new Error(
359
+ `Video model ${model.modelId} does not implement doGenerate or doStart/doStatus.`,
360
+ );
361
+ }
362
+
363
+ // Warn if poll/webhook provided but model doesn't support start/status
364
+ if ((poll != null || webhook != null) && !hasStartStatus) {
365
+ logWarnings({
366
+ warnings: [
367
+ {
368
+ type: 'other',
369
+ message:
370
+ 'poll/webhook options were provided but the model does not support doStart/doStatus. Falling back to doGenerate.',
371
+ },
372
+ ],
373
+ provider: model.provider,
374
+ model: model.modelId,
375
+ });
376
+ }
377
+
273
378
  // parallelize calls to the model:
274
379
  const callCount = Math.ceil(n / maxVideosPerCallWithDefault);
275
380
  const callVideoCounts = Array.from({ length: callCount }, (_, index) => {
@@ -278,27 +383,36 @@ export async function experimental_generateVideo({
278
383
  });
279
384
 
280
385
  const results = await Promise.all(
281
- callVideoCounts.map(
282
- async callVideoCount =>
283
- await retry(() =>
284
- model.doGenerate({
285
- prompt,
286
- n: callVideoCount,
287
- aspectRatio,
288
- resolution,
289
- duration,
290
- fps,
291
- seed,
292
- image: resolvedImage,
293
- frameImages: normalizedFrameImages,
294
- inputReferences: effectiveInputReferences,
295
- generateAudio,
296
- providerOptions: providerOptions ?? {},
297
- headers: headersWithUserAgent,
298
- abortSignal,
299
- } satisfies Experimental_VideoModelV4CallOptions),
300
- ),
301
- ),
386
+ callVideoCounts.map(async callVideoCount => {
387
+ const callOptions: Experimental_VideoModelV4CallOptions = {
388
+ prompt,
389
+ n: callVideoCount,
390
+ aspectRatio,
391
+ resolution,
392
+ duration,
393
+ fps,
394
+ seed,
395
+ image: resolvedImage,
396
+ frameImages: normalizedFrameImages,
397
+ inputReferences: effectiveInputReferences,
398
+ generateAudio,
399
+ providerOptions: providerOptions ?? {},
400
+ headers: headersWithUserAgent,
401
+ abortSignal,
402
+ };
403
+
404
+ if (useStartStatus) {
405
+ return executeStartStatusFlow({
406
+ model,
407
+ callOptions,
408
+ poll,
409
+ webhook,
410
+ retry,
411
+ });
412
+ }
413
+
414
+ return retry(() => model.doGenerate!(callOptions));
415
+ }),
302
416
  );
303
417
 
304
418
  // collect result videos, warnings, and response metadata
@@ -377,32 +491,7 @@ export async function experimental_generateVideo({
377
491
  });
378
492
 
379
493
  if (result.providerMetadata != null) {
380
- for (const [providerName, metadata] of Object.entries(
381
- result.providerMetadata,
382
- )) {
383
- const existingMetadata = providerMetadata[providerName];
384
- if (existingMetadata != null && typeof existingMetadata === 'object') {
385
- providerMetadata[providerName] = {
386
- ...existingMetadata,
387
- ...metadata,
388
- };
389
-
390
- // Merge videos arrays if both exist
391
- if (
392
- 'videos' in existingMetadata &&
393
- Array.isArray(existingMetadata.videos) &&
394
- 'videos' in metadata &&
395
- Array.isArray(metadata.videos)
396
- ) {
397
- (providerMetadata[providerName] as { videos: unknown[] }).videos = [
398
- ...existingMetadata.videos,
399
- ...metadata.videos,
400
- ];
401
- }
402
- } else {
403
- providerMetadata[providerName] = metadata;
404
- }
405
- }
494
+ mergeProviderMetadata(providerMetadata, result.providerMetadata);
406
495
  }
407
496
  }
408
497
 
@@ -427,6 +516,197 @@ export async function experimental_generateVideo({
427
516
  };
428
517
  }
429
518
 
519
+ async function executeStartStatusFlow({
520
+ model,
521
+ callOptions,
522
+ poll: pollConfig,
523
+ webhook: webhookFactory,
524
+ retry,
525
+ }: {
526
+ model: Experimental_VideoModelV4;
527
+ callOptions: Experimental_VideoModelV4CallOptions;
528
+ poll?: GenerateVideoPollOptions;
529
+ webhook?: GenerateVideoWebhookFactory;
530
+ retry: <OUTPUT>(fn: () => PromiseLike<OUTPUT>) => PromiseLike<OUTPUT>;
531
+ }): Promise<Experimental_VideoModelV4Result> {
532
+ // 1. If webhook and provider supports it, set up the webhook
533
+ const earlyWarnings: Experimental_VideoModelV4Result['warnings'] = [];
534
+ let webhookUrl: string | undefined;
535
+ let webhookReceived:
536
+ | PromiseLike<Experimental_VideoModelV4OperationWebhook>
537
+ | undefined;
538
+
539
+ if (webhookFactory != null) {
540
+ if (model.handleWebhookOption != null) {
541
+ const result = await model.handleWebhookOption({
542
+ webhook: webhookFactory,
543
+ });
544
+ webhookUrl = result.webhookUrl;
545
+ webhookReceived = result.received;
546
+ } else {
547
+ earlyWarnings.push({
548
+ type: 'unsupported',
549
+ feature: 'webhook',
550
+ details:
551
+ 'This model does not support webhooks. Falling back to polling.',
552
+ });
553
+ }
554
+ }
555
+
556
+ // 2. Start the generation
557
+ const startResult = await retry(() =>
558
+ model.doStart!({
559
+ ...callOptions,
560
+ webhookUrl,
561
+ }),
562
+ );
563
+
564
+ const allWarnings = [...earlyWarnings, ...startResult.warnings];
565
+ let operationProviderMetadata =
566
+ startResult.providerMetadata == null
567
+ ? undefined
568
+ : { ...startResult.providerMetadata };
569
+ const intervalMs = pollConfig?.intervalMs ?? 5000;
570
+ const timeoutMs = pollConfig?.timeoutMs ?? 600_000;
571
+ const delay = pollConfig?.delay ?? defaultDelay;
572
+ const startTime = Date.now();
573
+
574
+ if (webhookReceived != null) {
575
+ // 3a. Webhook flow: wait for webhook, then get final status
576
+ await waitForWebhook({
577
+ received: webhookReceived,
578
+ timeoutMs,
579
+ abortSignal: callOptions.abortSignal,
580
+ delay,
581
+ });
582
+ }
583
+
584
+ while (true) {
585
+ if (webhookReceived == null) {
586
+ // 3b. Polling flow (also used when webhooks are not supported)
587
+ const elapsedMs = Date.now() - startTime;
588
+ if (elapsedMs >= timeoutMs) {
589
+ throw new Error(`Video generation timed out after ${timeoutMs}ms.`);
590
+ }
591
+ await delay(Math.min(intervalMs, timeoutMs - elapsedMs), {
592
+ abortSignal: callOptions.abortSignal,
593
+ });
594
+ if (Date.now() - startTime >= timeoutMs) {
595
+ throw new Error(`Video generation timed out after ${timeoutMs}ms.`);
596
+ }
597
+ }
598
+
599
+ const statusResult = await retry(() =>
600
+ model.doStatus!({
601
+ operation: startResult.operation,
602
+ abortSignal: callOptions.abortSignal,
603
+ headers: callOptions.headers,
604
+ }),
605
+ );
606
+
607
+ if (statusResult.status === 'error') {
608
+ throw new Error(statusResult.error);
609
+ }
610
+
611
+ if (statusResult.warnings != null) {
612
+ allWarnings.push(...statusResult.warnings);
613
+ }
614
+ if (statusResult.providerMetadata != null) {
615
+ operationProviderMetadata ??= {};
616
+ mergeProviderMetadata(
617
+ operationProviderMetadata,
618
+ statusResult.providerMetadata,
619
+ );
620
+ }
621
+
622
+ if (statusResult.status === 'completed') {
623
+ return {
624
+ videos: statusResult.videos,
625
+ warnings: allWarnings,
626
+ providerMetadata: operationProviderMetadata,
627
+ response: statusResult.response,
628
+ };
629
+ }
630
+
631
+ if (webhookReceived != null) {
632
+ throw new Error(
633
+ 'Video generation did not complete after webhook notification.',
634
+ );
635
+ }
636
+ }
637
+ }
638
+
639
+ async function waitForWebhook({
640
+ received,
641
+ timeoutMs,
642
+ abortSignal,
643
+ delay,
644
+ }: {
645
+ received: PromiseLike<Experimental_VideoModelV4OperationWebhook>;
646
+ timeoutMs: number;
647
+ abortSignal?: AbortSignal;
648
+ delay: (
649
+ delayInMs: number,
650
+ options?: { abortSignal?: AbortSignal },
651
+ ) => PromiseLike<void>;
652
+ }) {
653
+ // Cancel the timeout delay once the webhook arrives (or we abort/time out),
654
+ // so its timer does not keep the event loop alive on the success path.
655
+ const timeoutController =
656
+ typeof globalThis.AbortController === 'function'
657
+ ? new globalThis.AbortController()
658
+ : undefined;
659
+ try {
660
+ await Promise.race([
661
+ received,
662
+ delay(timeoutMs, {
663
+ abortSignal:
664
+ timeoutController == null
665
+ ? abortSignal
666
+ : mergeAbortSignals(abortSignal, timeoutController.signal),
667
+ }).then(() => {
668
+ throw new Error(`Video generation timed out after ${timeoutMs}ms.`);
669
+ }),
670
+ ]);
671
+ } finally {
672
+ timeoutController?.abort();
673
+ }
674
+ }
675
+
676
+ function mergeProviderMetadata(
677
+ target: SharedV4ProviderMetadata,
678
+ source: SharedV4ProviderMetadata,
679
+ ): void {
680
+ for (const [providerName, metadataValue] of Object.entries(source)) {
681
+ const existingMetadata = target[providerName];
682
+ if (
683
+ existingMetadata != null &&
684
+ typeof existingMetadata === 'object' &&
685
+ metadataValue != null &&
686
+ typeof metadataValue === 'object'
687
+ ) {
688
+ target[providerName] = {
689
+ ...existingMetadata,
690
+ ...metadataValue,
691
+ };
692
+
693
+ if (
694
+ 'videos' in existingMetadata &&
695
+ Array.isArray(existingMetadata.videos) &&
696
+ 'videos' in metadataValue &&
697
+ Array.isArray(metadataValue.videos)
698
+ ) {
699
+ (target[providerName] as { videos: unknown[] }).videos = [
700
+ ...existingMetadata.videos,
701
+ ...metadataValue.videos,
702
+ ];
703
+ }
704
+ } else {
705
+ target[providerName] = metadataValue;
706
+ }
707
+ }
708
+ }
709
+
430
710
  function normalizePrompt(promptArg: GenerateVideoPrompt): {
431
711
  prompt: string | undefined;
432
712
  image: Experimental_VideoModelV4File | undefined;
@@ -8,21 +8,28 @@ export class MockVideoModelV4 implements Experimental_VideoModelV4 {
8
8
  readonly maxVideosPerCall: Experimental_VideoModelV4['maxVideosPerCall'];
9
9
 
10
10
  doGenerate: Experimental_VideoModelV4['doGenerate'];
11
+ handleWebhookOption: Experimental_VideoModelV4['handleWebhookOption'];
12
+ doStart: Experimental_VideoModelV4['doStart'];
13
+ doStatus: Experimental_VideoModelV4['doStatus'];
11
14
 
12
- constructor({
13
- provider = 'mock-provider',
14
- modelId = 'mock-model-id',
15
- maxVideosPerCall = 1,
16
- doGenerate = notImplemented,
17
- }: {
18
- provider?: Experimental_VideoModelV4['provider'];
19
- modelId?: Experimental_VideoModelV4['modelId'];
20
- maxVideosPerCall?: Experimental_VideoModelV4['maxVideosPerCall'];
21
- doGenerate?: Experimental_VideoModelV4['doGenerate'];
22
- } = {}) {
23
- this.provider = provider;
24
- this.modelId = modelId;
25
- this.maxVideosPerCall = maxVideosPerCall;
26
- this.doGenerate = doGenerate;
15
+ constructor(
16
+ options: {
17
+ provider?: Experimental_VideoModelV4['provider'];
18
+ modelId?: Experimental_VideoModelV4['modelId'];
19
+ maxVideosPerCall?: Experimental_VideoModelV4['maxVideosPerCall'];
20
+ doGenerate?: Experimental_VideoModelV4['doGenerate'];
21
+ handleWebhookOption?: Experimental_VideoModelV4['handleWebhookOption'];
22
+ doStart?: Experimental_VideoModelV4['doStart'];
23
+ doStatus?: Experimental_VideoModelV4['doStatus'];
24
+ } = {},
25
+ ) {
26
+ this.provider = options.provider ?? 'mock-provider';
27
+ this.modelId = options.modelId ?? 'mock-model-id';
28
+ this.maxVideosPerCall = options.maxVideosPerCall ?? 1;
29
+ this.doGenerate =
30
+ 'doGenerate' in options ? options.doGenerate : notImplemented;
31
+ this.handleWebhookOption = options.handleWebhookOption;
32
+ this.doStart = options.doStart;
33
+ this.doStatus = options.doStatus;
27
34
  }
28
35
  }