@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.
- package/CHANGELOG.md +17 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +214 -61
- package/dist/index.js.map +1 -1
- package/docs/01-xai.mdx +93 -34
- package/package.json +2 -2
- package/src/responses/xai-responses-api.ts +8 -1
- package/src/responses/xai-responses-language-model-options.ts +27 -1
- package/src/responses/xai-responses-language-model.ts +38 -14
- package/src/xai-video-model-options.ts +27 -0
- package/src/xai-video-model.ts +178 -56
package/src/xai-video-model.ts
CHANGED
|
@@ -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
|
-
//
|
|
105
|
-
//
|
|
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:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
173
|
-
//
|
|
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
|
-
|
|
177
|
-
(
|
|
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
|
-
//
|
|
418
|
+
// Only grok-imagine-video-1.5 supports a pinned last frame.
|
|
360
419
|
const lastFrameImage = getLastFrameImage(options);
|
|
361
420
|
if (lastFrameImage != null) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
: '
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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:
|
|
438
|
-
|
|
439
|
-
'
|
|
440
|
-
|
|
441
|
-
'
|
|
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 (
|
|
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
|
-
|
|
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
|
|
640
|
-
...(
|
|
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(),
|