@ai-sdk/xai 5.0.7 → 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
  }
@@ -142,11 +147,25 @@ function resolveReferences(
142
147
  }
143
148
  }
144
149
 
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;
158
+ }
159
+
145
160
  // True when at least one reference would survive as an image.
146
161
  function hasImageInputReference(options: XaiVideoCallOptions): boolean {
147
162
  return options.inputReferences?.some(isImageReference) ?? false;
148
163
  }
149
164
 
165
+ function hasAudioInputReference(options: XaiVideoCallOptions): boolean {
166
+ return options.inputReferences?.some(isAudioReference) ?? false;
167
+ }
168
+
150
169
  function resolveVideoMode(
151
170
  options: XaiVideoCallOptions,
152
171
  xaiOptions: XaiParsedVideoModelOptions | undefined,
@@ -159,20 +178,16 @@ function resolveVideoMode(
159
178
  return 'edit-video';
160
179
  }
161
180
 
162
- // frameImages (first/last frame) take precedence over reference images, so
163
- // only auto-select reference-to-video when no frame images are provided.
164
- const hasFrameImages =
165
- options.frameImages != null && options.frameImages.length > 0;
166
181
  const hasLegacyReferenceUrls =
167
182
  xaiOptions?.referenceImageUrls != null &&
168
183
  xaiOptions.referenceImageUrls.length > 0;
169
184
 
170
- // Reference-to-video needs at least one image reference. An audio-only (or
171
- // video-only) `inputReferences` array must not flip a text- or
172
- // 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.
173
187
  if (
174
- !hasFrameImages &&
175
- (hasImageInputReference(options) || hasLegacyReferenceUrls)
188
+ hasImageInputReference(options) ||
189
+ hasAudioInputReference(options) ||
190
+ hasLegacyReferenceUrls
176
191
  ) {
177
192
  return 'reference-to-video';
178
193
  }
@@ -323,6 +338,54 @@ export class XaiVideoModel implements VideoModelV4 {
323
338
  }
324
339
  }
325
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
+
326
389
  // Video editing: pass source video URL (nested object)
327
390
  if (isEdit) {
328
391
  body.video = { url: xaiOptions!.videoUrl };
@@ -352,29 +415,36 @@ export class XaiVideoModel implements VideoModelV4 {
352
415
  }
353
416
  }
354
417
 
355
- // xAI has no first-last-frame interpolation; warn and ignore last_frame.
418
+ // Only grok-imagine-video-1.5 supports a pinned last frame.
356
419
  const lastFrameImage = getLastFrameImage(options);
357
420
  if (lastFrameImage != null) {
358
- warnings.push({
359
- type: 'unsupported',
360
- feature: 'frameImages',
361
- details: isVideoFile(lastFrameImage)
362
- ? 'xAI does not accept a video as a start/frame image. The video ' +
363
- 'last frame was ignored. Use providerOptions.xai.mode ' +
364
- '"extend-video" to continue from a video instead.'
365
- : 'xAI video models do not support last_frame. Use ' +
366
- 'providerOptions.xai.mode "extend-video" to continue from a ' +
367
- "video's last frame. The last frame image was ignored.",
368
- });
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
+ }
369
438
  }
370
439
 
371
440
  // Reference images for R2V (reference-to-video) generation
372
441
  if (hasReferenceImages) {
373
442
  const referenceImages = resolveReferences(options, xaiOptions, warnings);
443
+ const referenceAudios = resolveReferenceAudios(options);
374
444
 
375
445
  if (referenceImages != null) {
376
446
  body.reference_images = referenceImages;
377
- } else {
447
+ } else if (referenceAudios == null) {
378
448
  // Explicit R2V with no usable image references would silently send
379
449
  // a plain generations request; tell the user it is no longer R2V.
380
450
  warnings.push({
@@ -387,10 +457,23 @@ export class XaiVideoModel implements VideoModelV4 {
387
457
  }
388
458
 
389
459
  const referenceVoiceIds = xaiOptions?.referenceVoiceIds;
390
- if (referenceVoiceIds != null && referenceVoiceIds.length > 0) {
391
- body.reference_audios = referenceVoiceIds.map(voiceId => ({
392
- voice_id: voiceId,
393
- }));
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);
394
477
  }
395
478
 
396
479
  // Reference-to-video is capped at 720p; downgrade a 1080p request.
@@ -430,11 +513,12 @@ export class XaiVideoModel implements VideoModelV4 {
430
513
  warnings.push({
431
514
  type: 'unsupported',
432
515
  feature: 'inputReferences',
433
- details: hasImageInputReference(options)
434
- ? 'xAI only supports inputReferences for reference-to-video ' +
435
- 'generation. The reference images were ignored.'
436
- : 'xAI reference-to-video requires at least one image reference. ' +
437
- '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.',
438
522
  });
439
523
  }
440
524
 
@@ -468,6 +552,8 @@ export class XaiVideoModel implements VideoModelV4 {
468
552
  'videoUrl',
469
553
  'referenceImageUrls',
470
554
  'referenceVoiceIds',
555
+ 'keyframes',
556
+ 'storageOptions',
471
557
  'user',
472
558
  ].includes(key)
473
559
  ) {
@@ -588,8 +674,10 @@ export class XaiVideoModel implements VideoModelV4 {
588
674
  statusResponse.status === 'done' ||
589
675
  (statusResponse.status == null && statusResponse.video?.url)
590
676
  ) {
677
+ const video = statusResponse.video;
678
+
591
679
  // Terminal outcomes, so they are reported the same way as an upstream `failed`
592
- if (statusResponse.video?.respect_moderation === false) {
680
+ if (video?.respect_moderation === false) {
593
681
  return {
594
682
  status: 'error' as const,
595
683
  error:
@@ -602,7 +690,10 @@ export class XaiVideoModel implements VideoModelV4 {
602
690
  };
603
691
  }
604
692
 
605
- if (!statusResponse.video?.url) {
693
+ const videoUrl =
694
+ video?.url ?? video?.file_output?.public_url ?? undefined;
695
+
696
+ if (!videoUrl) {
606
697
  return {
607
698
  status: 'error' as const,
608
699
  error: 'Video generation completed but no video URL was returned.',
@@ -616,13 +707,7 @@ export class XaiVideoModel implements VideoModelV4 {
616
707
 
617
708
  return {
618
709
  status: 'completed',
619
- videos: [
620
- {
621
- type: 'url',
622
- url: statusResponse.video.url,
623
- mediaType: 'video/mp4',
624
- },
625
- ],
710
+ videos: [{ type: 'url', url: videoUrl, mediaType: 'video/mp4' }],
626
711
  warnings: [],
627
712
  response: {
628
713
  timestamp: currentDate,
@@ -632,16 +717,46 @@ export class XaiVideoModel implements VideoModelV4 {
632
717
  providerMetadata: {
633
718
  xai: {
634
719
  requestId,
635
- videoUrl: statusResponse.video.url,
636
- ...(statusResponse.video.duration != null
637
- ? { duration: statusResponse.video.duration }
638
- : {}),
720
+ videoUrl,
721
+ ...(video?.duration != null ? { duration: video.duration } : {}),
639
722
  ...(statusResponse.usage?.cost_in_usd_ticks != null
640
723
  ? { costInUsdTicks: statusResponse.usage.cost_in_usd_ticks }
641
724
  : {}),
642
725
  ...(statusResponse.progress != null
643
726
  ? { progress: statusResponse.progress }
644
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
+ : {}),
645
760
  },
646
761
  },
647
762
  };
@@ -667,9 +782,20 @@ const xaiVideoStatusResponseSchema = z.object({
667
782
  status: z.string().nullish(),
668
783
  video: z
669
784
  .object({
670
- url: z.string(),
785
+ url: z.string().nullish(),
671
786
  duration: z.number().nullish(),
672
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(),
673
799
  })
674
800
  .nullish(),
675
801
  model: z.string().nullish(),