@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.
- package/CHANGELOG.md +7 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +214 -59
- package/dist/index.js.map +1 -1
- package/docs/01-xai.mdx +93 -34
- package/package.json +1 -1
- 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 +179 -53
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
|
}
|
|
@@ -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
|
-
//
|
|
171
|
-
//
|
|
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
|
-
|
|
175
|
-
(
|
|
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
|
-
//
|
|
418
|
+
// Only grok-imagine-video-1.5 supports a pinned last frame.
|
|
356
419
|
const lastFrameImage = getLastFrameImage(options);
|
|
357
420
|
if (lastFrameImage != null) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
: '
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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:
|
|
434
|
-
|
|
435
|
-
'
|
|
436
|
-
|
|
437
|
-
'
|
|
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 (
|
|
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
|
-
|
|
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
|
|
636
|
-
...(
|
|
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(),
|