@ai-sdk/xai 5.0.6 → 5.0.8

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.
@@ -87,6 +87,9 @@ const isVideoFile = (file: VideoModelV4File): boolean =>
87
87
  const isImageReference = (file: VideoModelV4File): boolean =>
88
88
  file.mediaType == null || getTopLevelMediaType(file.mediaType) === 'image';
89
89
 
90
+ const isAudioReference = (file: VideoModelV4File): boolean =>
91
+ file.mediaType != null && getTopLevelMediaType(file.mediaType) === 'audio';
92
+
90
93
  function fileToXaiUrl(file: VideoModelV4File): string {
91
94
  if (file.type === 'url') {
92
95
  return file.url;
@@ -101,8 +104,8 @@ function fileToXaiUrl(file: VideoModelV4File): string {
101
104
 
102
105
  // Resolves the reference images for R2V generation. First-class
103
106
  // `inputReferences` win over the legacy `referenceImageUrls` provider option.
104
- // Non-image references (video or audio) are not supported for
105
- // reference-to-video and are skipped with a warning.
107
+ // Video references are not supported for reference-to-video and are skipped
108
+ // with a warning. Audio references are handled separately below.
106
109
  function resolveReferences(
107
110
  options: XaiVideoCallOptions,
108
111
  xaiOptions: XaiParsedVideoModelOptions | undefined,
@@ -112,16 +115,18 @@ function resolveReferences(
112
115
  const imageFiles: VideoModelV4File[] = [];
113
116
 
114
117
  for (const reference of options.inputReferences) {
118
+ if (isAudioReference(reference)) {
119
+ continue;
120
+ }
121
+
115
122
  if (!isImageReference(reference)) {
116
123
  warnings.push({
117
124
  type: 'unsupported',
118
125
  feature: 'inputReferences',
119
- details: isVideoFile(reference)
120
- ? 'xAI reference-to-video accepts image references only. The ' +
121
- 'video reference was ignored. Use providerOptions.xai.mode ' +
122
- '"extend-video" to continue from a video.'
123
- : 'xAI reference-to-video accepts image references only. The ' +
124
- 'non-image reference was ignored.',
126
+ details:
127
+ 'xAI reference-to-video does not accept video references. The ' +
128
+ 'video reference was ignored. Use providerOptions.xai.mode ' +
129
+ '"extend-video" to continue from a video.',
125
130
  });
126
131
  continue;
127
132
  }
@@ -140,8 +145,16 @@ function resolveReferences(
140
145
  ) {
141
146
  return xaiOptions.referenceImageUrls.map(url => ({ url }));
142
147
  }
148
+ }
143
149
 
144
- return undefined;
150
+ function resolveReferenceAudios(
151
+ options: XaiVideoCallOptions,
152
+ ): Array<{ url: string }> | undefined {
153
+ const audioReferences = options.inputReferences?.filter(isAudioReference);
154
+
155
+ return audioReferences != null && audioReferences.length > 0
156
+ ? audioReferences.map(reference => ({ url: fileToXaiUrl(reference) }))
157
+ : undefined;
145
158
  }
146
159
 
147
160
  // True when at least one reference would survive as an image.
@@ -149,6 +162,10 @@ function hasImageInputReference(options: XaiVideoCallOptions): boolean {
149
162
  return options.inputReferences?.some(isImageReference) ?? false;
150
163
  }
151
164
 
165
+ function hasAudioInputReference(options: XaiVideoCallOptions): boolean {
166
+ return options.inputReferences?.some(isAudioReference) ?? false;
167
+ }
168
+
152
169
  function resolveVideoMode(
153
170
  options: XaiVideoCallOptions,
154
171
  xaiOptions: XaiParsedVideoModelOptions | undefined,
@@ -161,25 +178,19 @@ function resolveVideoMode(
161
178
  return 'edit-video';
162
179
  }
163
180
 
164
- // frameImages (first/last frame) take precedence over reference images, so
165
- // only auto-select reference-to-video when no frame images are provided.
166
- const hasFrameImages =
167
- options.frameImages != null && options.frameImages.length > 0;
168
181
  const hasLegacyReferenceUrls =
169
182
  xaiOptions?.referenceImageUrls != null &&
170
183
  xaiOptions.referenceImageUrls.length > 0;
171
184
 
172
- // Reference-to-video needs at least one image reference. An audio-only (or
173
- // video-only) `inputReferences` array must not flip a text- or
174
- // image-to-video request into R2V.
185
+ // xAI supports image references, audio references, or both. Video-only
186
+ // references must not flip a standard generation request into R2V.
175
187
  if (
176
- !hasFrameImages &&
177
- (hasImageInputReference(options) || hasLegacyReferenceUrls)
188
+ hasImageInputReference(options) ||
189
+ hasAudioInputReference(options) ||
190
+ hasLegacyReferenceUrls
178
191
  ) {
179
192
  return 'reference-to-video';
180
193
  }
181
-
182
- return undefined;
183
194
  }
184
195
 
185
196
  export class XaiVideoModel implements VideoModelV4 {
@@ -327,6 +338,54 @@ export class XaiVideoModel implements VideoModelV4 {
327
338
  }
328
339
  }
329
340
 
341
+ if (options.generateAudio != null && !isEdit && !isExtension) {
342
+ body.generate_audio = options.generateAudio;
343
+ } else if (options.generateAudio != null) {
344
+ warnings.push({
345
+ type: 'unsupported',
346
+ feature: 'generateAudio',
347
+ details: `xAI ${isEdit ? 'video editing' : 'video extension'} does not support generateAudio.`,
348
+ });
349
+ }
350
+
351
+ if (xaiOptions?.storageOptions != null) {
352
+ const { filename, expiresAfter, publicUrl } = xaiOptions.storageOptions;
353
+ body.storage_options = {
354
+ filename,
355
+ ...(expiresAfter != null ? { expires_after: expiresAfter } : {}),
356
+ ...(publicUrl != null
357
+ ? {
358
+ public_url:
359
+ typeof publicUrl === 'boolean'
360
+ ? publicUrl
361
+ : {
362
+ ...(publicUrl.expiresAfter != null
363
+ ? { expires_after: publicUrl.expiresAfter }
364
+ : {}),
365
+ },
366
+ }
367
+ : {}),
368
+ };
369
+ }
370
+
371
+ if (xaiOptions?.keyframes != null && xaiOptions.keyframes.length > 0) {
372
+ if (this.modelId !== 'grok-imagine-video-1.5' || isEdit || isExtension) {
373
+ warnings.push({
374
+ type: 'unsupported',
375
+ feature: 'keyframes',
376
+ details:
377
+ this.modelId !== 'grok-imagine-video-1.5'
378
+ ? 'xAI only supports keyframes with "grok-imagine-video-1.5".'
379
+ : `xAI ${isEdit ? 'video editing' : 'video extension'} does not support keyframes.`,
380
+ });
381
+ } else {
382
+ body.keyframes = xaiOptions.keyframes.map(keyframe => ({
383
+ image: { url: keyframe.imageUrl },
384
+ timestamp_s: keyframe.timestampSeconds,
385
+ }));
386
+ }
387
+ }
388
+
330
389
  // Video editing: pass source video URL (nested object)
331
390
  if (isEdit) {
332
391
  body.video = { url: xaiOptions!.videoUrl };
@@ -356,29 +415,36 @@ export class XaiVideoModel implements VideoModelV4 {
356
415
  }
357
416
  }
358
417
 
359
- // xAI has no first-last-frame interpolation; warn and ignore last_frame.
418
+ // Only grok-imagine-video-1.5 supports a pinned last frame.
360
419
  const lastFrameImage = getLastFrameImage(options);
361
420
  if (lastFrameImage != null) {
362
- warnings.push({
363
- type: 'unsupported',
364
- feature: 'frameImages',
365
- details: isVideoFile(lastFrameImage)
366
- ? 'xAI does not accept a video as a start/frame image. The video ' +
367
- 'last frame was ignored. Use providerOptions.xai.mode ' +
368
- '"extend-video" to continue from a video instead.'
369
- : 'xAI video models do not support last_frame. Use ' +
370
- 'providerOptions.xai.mode "extend-video" to continue from a ' +
371
- "video's last frame. The last frame image was ignored.",
372
- });
421
+ if (
422
+ this.modelId !== 'grok-imagine-video-1.5' ||
423
+ isEdit ||
424
+ isExtension ||
425
+ isVideoFile(lastFrameImage)
426
+ ) {
427
+ warnings.push({
428
+ type: 'unsupported',
429
+ feature: 'frameImages',
430
+ details:
431
+ this.modelId !== 'grok-imagine-video-1.5'
432
+ ? 'xAI only supports last_frame with "grok-imagine-video-1.5". The last frame was ignored.'
433
+ : 'xAI only accepts an image last_frame for video generation. The last frame was ignored.',
434
+ });
435
+ } else {
436
+ body.last_frame = { url: fileToXaiUrl(lastFrameImage) };
437
+ }
373
438
  }
374
439
 
375
440
  // Reference images for R2V (reference-to-video) generation
376
441
  if (hasReferenceImages) {
377
442
  const referenceImages = resolveReferences(options, xaiOptions, warnings);
443
+ const referenceAudios = resolveReferenceAudios(options);
378
444
 
379
445
  if (referenceImages != null) {
380
446
  body.reference_images = referenceImages;
381
- } else {
447
+ } else if (referenceAudios == null) {
382
448
  // Explicit R2V with no usable image references would silently send
383
449
  // a plain generations request; tell the user it is no longer R2V.
384
450
  warnings.push({
@@ -391,10 +457,23 @@ export class XaiVideoModel implements VideoModelV4 {
391
457
  }
392
458
 
393
459
  const referenceVoiceIds = xaiOptions?.referenceVoiceIds;
394
- if (referenceVoiceIds != null && referenceVoiceIds.length > 0) {
395
- body.reference_audios = referenceVoiceIds.map(voiceId => ({
396
- voice_id: voiceId,
397
- }));
460
+ const referenceVoices = referenceVoiceIds?.map(voiceId => ({
461
+ voice_id: voiceId,
462
+ }));
463
+ const referenceAudioInputs = [
464
+ ...(referenceAudios ?? []),
465
+ ...(referenceVoices ?? []),
466
+ ];
467
+ if (referenceAudioInputs.length > 0) {
468
+ if (referenceAudioInputs.length > 3) {
469
+ warnings.push({
470
+ type: 'unsupported',
471
+ feature: 'inputReferences',
472
+ details:
473
+ 'xAI reference-to-video supports at most 3 audio references. Only the first 3 were used.',
474
+ });
475
+ }
476
+ body.reference_audios = referenceAudioInputs.slice(0, 3);
398
477
  }
399
478
 
400
479
  // Reference-to-video is capped at 720p; downgrade a 1080p request.
@@ -434,11 +513,12 @@ export class XaiVideoModel implements VideoModelV4 {
434
513
  warnings.push({
435
514
  type: 'unsupported',
436
515
  feature: 'inputReferences',
437
- details: hasImageInputReference(options)
438
- ? 'xAI only supports inputReferences for reference-to-video ' +
439
- 'generation. The reference images were ignored.'
440
- : 'xAI reference-to-video requires at least one image reference. ' +
441
- 'The references were ignored.',
516
+ details:
517
+ hasImageInputReference(options) || hasAudioInputReference(options)
518
+ ? 'xAI only supports inputReferences for reference-to-video ' +
519
+ 'generation. The references were ignored.'
520
+ : 'xAI reference-to-video requires at least one image or audio reference. ' +
521
+ 'The references were ignored.',
442
522
  });
443
523
  }
444
524
 
@@ -472,6 +552,8 @@ export class XaiVideoModel implements VideoModelV4 {
472
552
  'videoUrl',
473
553
  'referenceImageUrls',
474
554
  'referenceVoiceIds',
555
+ 'keyframes',
556
+ 'storageOptions',
475
557
  'user',
476
558
  ].includes(key)
477
559
  ) {
@@ -592,8 +674,10 @@ export class XaiVideoModel implements VideoModelV4 {
592
674
  statusResponse.status === 'done' ||
593
675
  (statusResponse.status == null && statusResponse.video?.url)
594
676
  ) {
677
+ const video = statusResponse.video;
678
+
595
679
  // Terminal outcomes, so they are reported the same way as an upstream `failed`
596
- if (statusResponse.video?.respect_moderation === false) {
680
+ if (video?.respect_moderation === false) {
597
681
  return {
598
682
  status: 'error' as const,
599
683
  error:
@@ -606,7 +690,10 @@ export class XaiVideoModel implements VideoModelV4 {
606
690
  };
607
691
  }
608
692
 
609
- if (!statusResponse.video?.url) {
693
+ const videoUrl =
694
+ video?.url ?? video?.file_output?.public_url ?? undefined;
695
+
696
+ if (!videoUrl) {
610
697
  return {
611
698
  status: 'error' as const,
612
699
  error: 'Video generation completed but no video URL was returned.',
@@ -620,13 +707,7 @@ export class XaiVideoModel implements VideoModelV4 {
620
707
 
621
708
  return {
622
709
  status: 'completed',
623
- videos: [
624
- {
625
- type: 'url',
626
- url: statusResponse.video.url,
627
- mediaType: 'video/mp4',
628
- },
629
- ],
710
+ videos: [{ type: 'url', url: videoUrl, mediaType: 'video/mp4' }],
630
711
  warnings: [],
631
712
  response: {
632
713
  timestamp: currentDate,
@@ -636,16 +717,46 @@ export class XaiVideoModel implements VideoModelV4 {
636
717
  providerMetadata: {
637
718
  xai: {
638
719
  requestId,
639
- videoUrl: statusResponse.video.url,
640
- ...(statusResponse.video.duration != null
641
- ? { duration: statusResponse.video.duration }
642
- : {}),
720
+ videoUrl,
721
+ ...(video?.duration != null ? { duration: video.duration } : {}),
643
722
  ...(statusResponse.usage?.cost_in_usd_ticks != null
644
723
  ? { costInUsdTicks: statusResponse.usage.cost_in_usd_ticks }
645
724
  : {}),
646
725
  ...(statusResponse.progress != null
647
726
  ? { progress: statusResponse.progress }
648
727
  : {}),
728
+ ...(video?.file_output != null
729
+ ? {
730
+ fileOutput: {
731
+ fileId: video.file_output.file_id,
732
+ filename: video.file_output.filename,
733
+ ...(video.file_output.expires_at != null
734
+ ? {
735
+ expiresAt: video.file_output.expires_at,
736
+ }
737
+ : {}),
738
+ ...(video.file_output.public_url != null
739
+ ? {
740
+ publicUrl: video.file_output.public_url,
741
+ }
742
+ : {}),
743
+ ...(video.file_output.public_url_error != null
744
+ ? {
745
+ publicUrlError: video.file_output.public_url_error,
746
+ }
747
+ : {}),
748
+ ...(video.file_output.public_url_expires_at != null
749
+ ? {
750
+ publicUrlExpiresAt:
751
+ video.file_output.public_url_expires_at,
752
+ }
753
+ : {}),
754
+ },
755
+ }
756
+ : {}),
757
+ ...(video?.storage_error != null
758
+ ? { storageError: video.storage_error }
759
+ : {}),
649
760
  },
650
761
  },
651
762
  };
@@ -671,9 +782,20 @@ const xaiVideoStatusResponseSchema = z.object({
671
782
  status: z.string().nullish(),
672
783
  video: z
673
784
  .object({
674
- url: z.string(),
785
+ url: z.string().nullish(),
675
786
  duration: z.number().nullish(),
676
787
  respect_moderation: z.boolean().nullish(),
788
+ file_output: z
789
+ .object({
790
+ file_id: z.string(),
791
+ filename: z.string(),
792
+ expires_at: z.number().nullish(),
793
+ public_url: z.string().nullish(),
794
+ public_url_error: z.string().nullish(),
795
+ public_url_expires_at: z.number().nullish(),
796
+ })
797
+ .nullish(),
798
+ storage_error: z.string().nullish(),
677
799
  })
678
800
  .nullish(),
679
801
  model: z.string().nullish(),