@ai-sdk/xai 4.0.9 → 4.0.12
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 +42 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +107 -58
- package/dist/index.js.map +1 -1
- package/docs/01-xai.mdx +27 -40
- package/package.json +6 -6
- package/src/responses/xai-responses-language-model-options.ts +1 -0
- package/src/xai-chat-language-model-options.ts +1 -0
- package/src/xai-image-model.ts +3 -0
- package/src/xai-transcription-model.ts +122 -93
- package/src/xai-video-model.ts +52 -9
package/docs/01-xai.mdx
CHANGED
|
@@ -12,21 +12,7 @@ The [xAI Grok](https://x.ai) provider contains language model support for the [x
|
|
|
12
12
|
The xAI Grok provider is available via the `@ai-sdk/xai` module. You can
|
|
13
13
|
install it with
|
|
14
14
|
|
|
15
|
-
<
|
|
16
|
-
<Tab>
|
|
17
|
-
<Snippet text="pnpm add @ai-sdk/xai" dark />
|
|
18
|
-
</Tab>
|
|
19
|
-
<Tab>
|
|
20
|
-
<Snippet text="npm install @ai-sdk/xai" dark />
|
|
21
|
-
</Tab>
|
|
22
|
-
<Tab>
|
|
23
|
-
<Snippet text="yarn add @ai-sdk/xai" dark />
|
|
24
|
-
</Tab>
|
|
25
|
-
|
|
26
|
-
<Tab>
|
|
27
|
-
<Snippet text="bun add @ai-sdk/xai" dark />
|
|
28
|
-
</Tab>
|
|
29
|
-
</Tabs>
|
|
15
|
+
<InstallPackages packages="@ai-sdk/xai" />
|
|
30
16
|
|
|
31
17
|
## Provider Instance
|
|
32
18
|
|
|
@@ -556,18 +542,19 @@ The following provider options are available:
|
|
|
556
542
|
|
|
557
543
|
## Model Capabilities
|
|
558
544
|
|
|
559
|
-
| Model | Image Input
|
|
560
|
-
| ----------------------------- |
|
|
561
|
-
| `grok-4.
|
|
562
|
-
| `grok-4.20-
|
|
563
|
-
| `grok-4-
|
|
564
|
-
| `grok-4-1-fast-
|
|
565
|
-
| `grok-4-1`
|
|
566
|
-
| `grok-4-
|
|
567
|
-
| `grok-4-fast-
|
|
568
|
-
| `grok-
|
|
569
|
-
| `grok-
|
|
570
|
-
| `grok-3
|
|
545
|
+
| Model | Image Input | Object Generation | Tool Usage | Tool Streaming | Reasoning |
|
|
546
|
+
| ----------------------------- | ----------- | ----------------- | ---------- | -------------- | --------- |
|
|
547
|
+
| `grok-4.5` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
548
|
+
| `grok-4.20-reasoning` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
549
|
+
| `grok-4.20-non-reasoning` | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> |
|
|
550
|
+
| `grok-4-1-fast-reasoning` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
551
|
+
| `grok-4-1-fast-non-reasoning` | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> |
|
|
552
|
+
| `grok-4-1` | <Cross /> | <Check /> | <Check /> | <Check /> | <Cross /> |
|
|
553
|
+
| `grok-4-fast-reasoning` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
554
|
+
| `grok-4-fast-non-reasoning` | <Check /> | <Check /> | <Check /> | <Check /> | <Cross /> |
|
|
555
|
+
| `grok-code-fast-1` | <Cross /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
556
|
+
| `grok-3` | <Cross /> | <Check /> | <Check /> | <Check /> | <Cross /> |
|
|
557
|
+
| `grok-3-mini` | <Cross /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
571
558
|
|
|
572
559
|
<Note>
|
|
573
560
|
The table above lists popular models. Please see the [xAI
|
|
@@ -675,9 +662,9 @@ const result = await generateSpeech({
|
|
|
675
662
|
|
|
676
663
|
### Model Capabilities
|
|
677
664
|
|
|
678
|
-
| Model | Language
|
|
679
|
-
| --------- |
|
|
680
|
-
| `default` | <Check
|
|
665
|
+
| Model | Language | Speed | Output Formats |
|
|
666
|
+
| --------- | --------- | --------- | -------------------------- |
|
|
667
|
+
| `default` | <Check /> | <Check /> | mp3, wav, pcm, mulaw, alaw |
|
|
681
668
|
|
|
682
669
|
## Transcription Models
|
|
683
670
|
|
|
@@ -782,9 +769,9 @@ const result = await transcribe({
|
|
|
782
769
|
|
|
783
770
|
### Model Capabilities
|
|
784
771
|
|
|
785
|
-
| Model | Request/Response
|
|
786
|
-
| --------- |
|
|
787
|
-
| `default` | <Check
|
|
772
|
+
| Model | Request/Response | Streaming | Word Timestamps | Diarization | Multichannel |
|
|
773
|
+
| --------- | ---------------- | --------- | --------------- | ----------- | ------------ |
|
|
774
|
+
| `default` | <Check /> | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
788
775
|
|
|
789
776
|
<Note>
|
|
790
777
|
Word timestamps, diarization results, and segments are only returned on the
|
|
@@ -919,10 +906,10 @@ const { images } = await generateImage({
|
|
|
919
906
|
|
|
920
907
|
### Image Model Capabilities
|
|
921
908
|
|
|
922
|
-
| Model | Resolution | Aspect Ratios | Image Editing
|
|
923
|
-
| ------------------------ | ---------- | ----------------------------------------------------------------------------------------------------------- |
|
|
924
|
-
| `grok-imagine-image-pro` | `1k`, `2k` | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3`, `2:1`, `1:2`, `19.5:9`, `9:19.5`, `20:9`, `9:20`, `auto` | <Check
|
|
925
|
-
| `grok-imagine-image` | `1k` | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3`, `2:1`, `1:2`, `19.5:9`, `9:19.5`, `20:9`, `9:20`, `auto` | <Check
|
|
909
|
+
| Model | Resolution | Aspect Ratios | Image Editing |
|
|
910
|
+
| ------------------------ | ---------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
|
|
911
|
+
| `grok-imagine-image-pro` | `1k`, `2k` | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3`, `2:1`, `1:2`, `19.5:9`, `9:19.5`, `20:9`, `9:20`, `auto` | <Check /> |
|
|
912
|
+
| `grok-imagine-image` | `1k` | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3`, `2:1`, `1:2`, `19.5:9`, `9:19.5`, `20:9`, `9:20`, `auto` | <Check /> |
|
|
926
913
|
|
|
927
914
|
## Video Models
|
|
928
915
|
|
|
@@ -1268,9 +1255,9 @@ and `resolution` just like text-to-video.
|
|
|
1268
1255
|
|
|
1269
1256
|
### Video Model Capabilities
|
|
1270
1257
|
|
|
1271
|
-
| Model | Duration | Aspect Ratios | Resolution | Image-to-Video
|
|
1272
|
-
| -------------------- | -------- | ------------------------------------------------- | -------------- |
|
|
1273
|
-
| `grok-imagine-video` | 1–15s | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3` | `480p`, `720p` | <Check
|
|
1258
|
+
| Model | Duration | Aspect Ratios | Resolution | Image-to-Video | Editing | Extension | R2V |
|
|
1259
|
+
| -------------------- | -------- | ------------------------------------------------- | -------------- | -------------- | --------- | --------- | --------- |
|
|
1260
|
+
| `grok-imagine-video` | 1–15s | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3` | `480p`, `720p` | <Check /> | <Check /> | <Check /> | <Check /> |
|
|
1274
1261
|
|
|
1275
1262
|
<Note>
|
|
1276
1263
|
You can also pass any available provider model ID as a string if needed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/xai",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -29,17 +29,17 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@ai-sdk/openai-compatible": "3.0.
|
|
33
|
-
"@ai-sdk/provider": "4.0.
|
|
34
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
32
|
+
"@ai-sdk/openai-compatible": "3.0.9",
|
|
33
|
+
"@ai-sdk/provider": "4.0.3",
|
|
34
|
+
"@ai-sdk/provider-utils": "5.0.9"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "22.19.19",
|
|
38
38
|
"tsup": "^8.5.1",
|
|
39
39
|
"typescript": "5.8.3",
|
|
40
40
|
"zod": "3.25.76",
|
|
41
|
-
"@
|
|
42
|
-
"@ai-
|
|
41
|
+
"@ai-sdk/test-server": "2.0.0",
|
|
42
|
+
"@vercel/ai-tsconfig": "0.0.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"zod": "^3.25.76 || ^4.1.8"
|
package/src/xai-image-model.ts
CHANGED
|
@@ -201,6 +201,9 @@ export class XaiImageModel implements ImageModelV4 {
|
|
|
201
201
|
): Promise<Uint8Array> {
|
|
202
202
|
const { value } = await getFromApi({
|
|
203
203
|
url,
|
|
204
|
+
// url is a generated-image URL from the provider response; validate it.
|
|
205
|
+
validateUrl: true,
|
|
206
|
+
trustedOrigin: this.config.baseURL,
|
|
204
207
|
abortSignal,
|
|
205
208
|
failedResponseHandler: createStatusCodeErrorResponseHandler(),
|
|
206
209
|
successfulResponseHandler: createBinaryResponseHandler(),
|
|
@@ -8,18 +8,20 @@ import {
|
|
|
8
8
|
combineHeaders,
|
|
9
9
|
convertBase64ToUint8Array,
|
|
10
10
|
createJsonResponseHandler,
|
|
11
|
-
|
|
11
|
+
connectToWebSocket,
|
|
12
12
|
mediaTypeToExtension,
|
|
13
13
|
parseProviderOptions,
|
|
14
14
|
postFormDataToApi,
|
|
15
|
-
readWebSocketMessageText,
|
|
16
15
|
safeParseJSON,
|
|
17
16
|
serializeModelOptions,
|
|
18
17
|
toWebSocketUrl,
|
|
18
|
+
waitForWebSocketBufferDrain,
|
|
19
19
|
WORKFLOW_DESERIALIZE,
|
|
20
20
|
WORKFLOW_SERIALIZE,
|
|
21
21
|
type FetchFunction,
|
|
22
|
+
type WebSocketConnection,
|
|
22
23
|
type WebSocketConstructor,
|
|
24
|
+
type WebSocketLike,
|
|
23
25
|
} from '@ai-sdk/provider-utils';
|
|
24
26
|
import { z } from 'zod/v4';
|
|
25
27
|
import { xaiFailedResponseHandler } from './xai-error';
|
|
@@ -273,20 +275,26 @@ function createXaiStreamingTranscriptionStream({
|
|
|
273
275
|
|
|
274
276
|
return new ReadableStream({
|
|
275
277
|
start: controller => {
|
|
276
|
-
const WebSocketConstructor = getWebSocketConstructor(webSocket);
|
|
277
|
-
const ws = new WebSocketConstructor(url, undefined, { headers });
|
|
278
278
|
const doneTexts = new Map<number, string>();
|
|
279
|
+
// per-channel finalized utterances + latest revisable text, used to
|
|
280
|
+
// reconstruct the finish text (xAI's `transcript.done` has empty text):
|
|
281
|
+
const finalizedTexts = new Map<number, string[]>();
|
|
282
|
+
const pendingTexts = new Map<number, string>();
|
|
279
283
|
let doneDuration: number | undefined;
|
|
280
284
|
let audioReader:
|
|
281
285
|
| ReadableStreamDefaultReader<Uint8Array | string>
|
|
282
286
|
| undefined;
|
|
287
|
+
let connection: WebSocketConnection | undefined;
|
|
283
288
|
|
|
284
289
|
cleanup = (closeCode?: number) => {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
+
if (audioReader != null) {
|
|
291
|
+
void audioReader.cancel().catch(() => {});
|
|
292
|
+
} else {
|
|
293
|
+
// pre-open failure or abort: cancel the caller's audio stream so an
|
|
294
|
+
// upstream producer piping into it does not hang:
|
|
295
|
+
void audio.cancel().catch(() => {});
|
|
296
|
+
}
|
|
297
|
+
connection?.close(closeCode);
|
|
290
298
|
};
|
|
291
299
|
|
|
292
300
|
const finishWithError = (error: unknown) => {
|
|
@@ -314,117 +322,138 @@ function createXaiStreamingTranscriptionStream({
|
|
|
314
322
|
cleanup(1000);
|
|
315
323
|
};
|
|
316
324
|
|
|
317
|
-
const
|
|
318
|
-
finishWithError(abortSignal?.reason ?? new Error('Aborted'));
|
|
319
|
-
};
|
|
320
|
-
if (abortSignal?.aborted) {
|
|
321
|
-
abort();
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
abortSignal?.addEventListener('abort', abort, { once: true });
|
|
325
|
-
|
|
326
|
-
const sendAudio = async () => {
|
|
325
|
+
const sendAudio = async (socket: WebSocketLike) => {
|
|
327
326
|
audioReader = audio.getReader();
|
|
328
327
|
try {
|
|
329
328
|
while (true) {
|
|
330
329
|
const { done, value } = await audioReader.read();
|
|
331
330
|
if (done || finished) break;
|
|
332
|
-
|
|
331
|
+
socket.send(
|
|
333
332
|
value instanceof Uint8Array
|
|
334
333
|
? value
|
|
335
334
|
: convertBase64ToUint8Array(value),
|
|
336
335
|
);
|
|
336
|
+
// backpressure: pause reads while the socket buffer is full
|
|
337
|
+
await waitForWebSocketBufferDrain(socket);
|
|
337
338
|
}
|
|
338
339
|
} finally {
|
|
339
340
|
audioReader.releaseLock();
|
|
341
|
+
// unlocked again: cleanup must cancel `audio`, not the reader
|
|
342
|
+
audioReader = undefined;
|
|
340
343
|
}
|
|
341
344
|
if (!finished) {
|
|
342
|
-
|
|
345
|
+
socket.send(JSON.stringify({ type: 'audio.done' }));
|
|
343
346
|
}
|
|
344
347
|
};
|
|
345
348
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
349
|
+
connection = connectToWebSocket({
|
|
350
|
+
url,
|
|
351
|
+
headers,
|
|
352
|
+
webSocket,
|
|
353
|
+
abortSignal,
|
|
354
|
+
onAbort: finishWithError,
|
|
355
|
+
onProcessingError: finishWithError,
|
|
356
|
+
onMessageText: async text => {
|
|
357
|
+
const parsed = await safeParseJSON({ text });
|
|
358
|
+
if (!parsed.success) return;
|
|
359
|
+
const raw = parsed.value as XaiStreamingTranscriptionEvent;
|
|
360
|
+
|
|
361
|
+
if (includeRawChunks) {
|
|
362
|
+
controller.enqueue({ type: 'raw', rawValue: raw });
|
|
363
|
+
}
|
|
356
364
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
365
|
+
switch (raw.type) {
|
|
366
|
+
case 'transcript.created': {
|
|
367
|
+
controller.enqueue({ type: 'stream-start', warnings });
|
|
368
|
+
const socket = connection?.socket;
|
|
369
|
+
if (socket == null) {
|
|
370
|
+
finishWithError(new Error('WebSocket is not connected.'));
|
|
361
371
|
break;
|
|
362
372
|
}
|
|
373
|
+
void sendAudio(socket).catch(finishWithError);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
363
376
|
|
|
364
|
-
|
|
365
|
-
|
|
377
|
+
case 'transcript.partial': {
|
|
378
|
+
const id = channelId(raw.channel_index);
|
|
379
|
+
const channelIndex = raw.channel_index ?? 0;
|
|
380
|
+
// only `speech_final` completes an utterance; `is_final`
|
|
381
|
+
// fragments are re-sent/revised later, so they stay partials:
|
|
382
|
+
if (raw.is_final && raw.speech_final) {
|
|
366
383
|
const timing = timingFromXaiEvent(raw);
|
|
367
|
-
if (raw.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
...timing,
|
|
373
|
-
channelIndex: raw.channel_index,
|
|
374
|
-
});
|
|
375
|
-
} else {
|
|
376
|
-
controller.enqueue({
|
|
377
|
-
type: 'transcript-partial',
|
|
378
|
-
id,
|
|
379
|
-
text: raw.text ?? '',
|
|
380
|
-
startSecond: raw.start,
|
|
381
|
-
durationInSeconds: raw.duration,
|
|
382
|
-
channelIndex: raw.channel_index,
|
|
383
|
-
});
|
|
384
|
+
if (raw.text) {
|
|
385
|
+
finalizedTexts.set(channelIndex, [
|
|
386
|
+
...(finalizedTexts.get(channelIndex) ?? []),
|
|
387
|
+
raw.text,
|
|
388
|
+
]);
|
|
384
389
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
390
|
+
pendingTexts.delete(channelIndex);
|
|
391
|
+
controller.enqueue({
|
|
392
|
+
type: 'transcript-final',
|
|
393
|
+
id,
|
|
394
|
+
text: raw.text ?? '',
|
|
395
|
+
...timing,
|
|
396
|
+
channelIndex: raw.channel_index,
|
|
397
|
+
});
|
|
398
|
+
} else {
|
|
399
|
+
pendingTexts.set(channelIndex, raw.text ?? '');
|
|
400
|
+
controller.enqueue({
|
|
401
|
+
type: 'transcript-partial',
|
|
402
|
+
id,
|
|
403
|
+
text: raw.text ?? '',
|
|
404
|
+
startSecond: raw.start,
|
|
405
|
+
durationInSeconds: raw.duration,
|
|
406
|
+
channelIndex: raw.channel_index,
|
|
407
|
+
});
|
|
401
408
|
}
|
|
409
|
+
break;
|
|
402
410
|
}
|
|
403
|
-
})
|
|
404
|
-
.catch(finishWithError);
|
|
405
|
-
};
|
|
406
411
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
412
|
+
case 'transcript.done': {
|
|
413
|
+
const channelIndex = raw.channel_index ?? 0;
|
|
414
|
+
// `transcript.done` text is empty; fall back to the
|
|
415
|
+
// accumulated utterances plus any trailing unfinalized text:
|
|
416
|
+
const accumulated = [
|
|
417
|
+
...(finalizedTexts.get(channelIndex) ?? []),
|
|
418
|
+
...(pendingTexts.get(channelIndex)
|
|
419
|
+
? [pendingTexts.get(channelIndex) as string]
|
|
420
|
+
: []),
|
|
421
|
+
].join(' ');
|
|
422
|
+
doneTexts.set(channelIndex, raw.text || accumulated);
|
|
423
|
+
doneDuration = raw.duration ?? doneDuration;
|
|
424
|
+
maybeFinish();
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
421
427
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
+
case 'error': {
|
|
429
|
+
// xAI STT errors are terminal: surface the server message
|
|
430
|
+
// instead of letting the socket close mask it.
|
|
431
|
+
finishWithError(new Error(raw.message ?? 'xAI STT error'));
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
onSocketError: () => {
|
|
437
|
+
finishWithError(
|
|
438
|
+
new Error(
|
|
439
|
+
'xAI streaming transcription error.' +
|
|
440
|
+
(webSocket == null
|
|
441
|
+
? ' Note: the native WebSocket implementation in browsers,' +
|
|
442
|
+
' Node.js, Deno, and Bun cannot send the Authorization' +
|
|
443
|
+
' header required by xAI. Pass a header-capable WebSocket' +
|
|
444
|
+
" implementation (e.g. the 'ws' package) via" +
|
|
445
|
+
' createXai({ webSocket }).'
|
|
446
|
+
: ''),
|
|
447
|
+
),
|
|
448
|
+
);
|
|
449
|
+
},
|
|
450
|
+
onClose: () => {
|
|
451
|
+
if (finished) return;
|
|
452
|
+
finished = true;
|
|
453
|
+
cleanup();
|
|
454
|
+
controller.close();
|
|
455
|
+
},
|
|
456
|
+
});
|
|
428
457
|
},
|
|
429
458
|
|
|
430
459
|
cancel: () => {
|
package/src/xai-video-model.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
createJsonResponseHandler,
|
|
11
11
|
delay,
|
|
12
12
|
getFromApi,
|
|
13
|
+
getTopLevelMediaType,
|
|
13
14
|
parseProviderOptions,
|
|
14
15
|
postJsonToApi,
|
|
15
16
|
type FetchFunction,
|
|
@@ -62,6 +63,9 @@ function resolveStartImage(
|
|
|
62
63
|
return getFirstFrameImage(options) ?? options.image;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
const isVideoFile = (file: Experimental_VideoModelV4File): boolean =>
|
|
67
|
+
file.mediaType != null && getTopLevelMediaType(file.mediaType) === 'video';
|
|
68
|
+
|
|
65
69
|
function fileToXaiImageUrl(file: Experimental_VideoModelV4File): string {
|
|
66
70
|
if (file.type === 'url') {
|
|
67
71
|
return file.url;
|
|
@@ -71,17 +75,35 @@ function fileToXaiImageUrl(file: Experimental_VideoModelV4File): string {
|
|
|
71
75
|
typeof file.data === 'string'
|
|
72
76
|
? file.data
|
|
73
77
|
: convertUint8ArrayToBase64(file.data);
|
|
74
|
-
return `data:${file.mediaType
|
|
78
|
+
return `data:${file.mediaType};base64,${base64Data}`;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
// Resolves the reference images for R2V generation. First-class
|
|
78
82
|
// `inputReferences` win over the legacy `referenceImageUrls` provider option.
|
|
83
|
+
// Video references are not supported for reference-to-video and are skipped
|
|
84
|
+
// with a warning.
|
|
79
85
|
function resolveReferenceImages(
|
|
80
86
|
options: XaiVideoDoGenerateOptions,
|
|
81
87
|
xaiOptions: XaiParsedVideoModelOptions | undefined,
|
|
88
|
+
warnings: SharedV4Warning[],
|
|
82
89
|
): Array<{ url: string }> | undefined {
|
|
83
90
|
if (options.inputReferences != null && options.inputReferences.length > 0) {
|
|
84
|
-
|
|
91
|
+
const imageReferences = options.inputReferences.filter(reference => {
|
|
92
|
+
if (isVideoFile(reference)) {
|
|
93
|
+
warnings.push({
|
|
94
|
+
type: 'unsupported',
|
|
95
|
+
feature: 'inputReferences',
|
|
96
|
+
details:
|
|
97
|
+
'xAI reference-to-video accepts image references only. The video ' +
|
|
98
|
+
'reference was ignored. Use providerOptions.xai.mode ' +
|
|
99
|
+
'"extend-video" to continue from a video.',
|
|
100
|
+
});
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return imageReferences.map(reference => ({
|
|
85
107
|
url: fileToXaiImageUrl(reference),
|
|
86
108
|
}));
|
|
87
109
|
}
|
|
@@ -278,24 +300,44 @@ export class XaiVideoModel implements Experimental_VideoModelV4 {
|
|
|
278
300
|
// nested xAI request image object.
|
|
279
301
|
const startImage = resolveStartImage(options);
|
|
280
302
|
if (startImage != null) {
|
|
281
|
-
|
|
303
|
+
if (isVideoFile(startImage)) {
|
|
304
|
+
const fromFrameImages = getFirstFrameImage(options) != null;
|
|
305
|
+
warnings.push({
|
|
306
|
+
type: 'unsupported',
|
|
307
|
+
feature: fromFrameImages ? 'frameImages' : 'image',
|
|
308
|
+
details:
|
|
309
|
+
'xAI does not accept a video as a start/frame image. The video ' +
|
|
310
|
+
'was ignored. Use providerOptions.xai.mode "extend-video" to ' +
|
|
311
|
+
'continue from a video instead.',
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
body.image = { url: fileToXaiImageUrl(startImage) };
|
|
315
|
+
}
|
|
282
316
|
}
|
|
283
317
|
|
|
284
318
|
// xAI has no first-last-frame interpolation; warn and ignore last_frame.
|
|
285
|
-
|
|
319
|
+
const lastFrameImage = getLastFrameImage(options);
|
|
320
|
+
if (lastFrameImage != null) {
|
|
286
321
|
warnings.push({
|
|
287
322
|
type: 'unsupported',
|
|
288
323
|
feature: 'frameImages',
|
|
289
|
-
details:
|
|
290
|
-
'xAI video
|
|
291
|
-
|
|
292
|
-
|
|
324
|
+
details: isVideoFile(lastFrameImage)
|
|
325
|
+
? 'xAI does not accept a video as a start/frame image. The video ' +
|
|
326
|
+
'last frame was ignored. Use providerOptions.xai.mode ' +
|
|
327
|
+
'"extend-video" to continue from a video instead.'
|
|
328
|
+
: 'xAI video models do not support last_frame. Use ' +
|
|
329
|
+
'providerOptions.xai.mode "extend-video" to continue from a ' +
|
|
330
|
+
"video's last frame. The last frame image was ignored.",
|
|
293
331
|
});
|
|
294
332
|
}
|
|
295
333
|
|
|
296
334
|
// Reference images for R2V (reference-to-video) generation
|
|
297
335
|
if (hasReferenceImages) {
|
|
298
|
-
const referenceImages = resolveReferenceImages(
|
|
336
|
+
const referenceImages = resolveReferenceImages(
|
|
337
|
+
options,
|
|
338
|
+
xaiOptions,
|
|
339
|
+
warnings,
|
|
340
|
+
);
|
|
299
341
|
if (referenceImages != null) {
|
|
300
342
|
body.reference_images = referenceImages;
|
|
301
343
|
}
|
|
@@ -386,6 +428,7 @@ export class XaiVideoModel implements Experimental_VideoModelV4 {
|
|
|
386
428
|
const { value: statusResponse, responseHeaders: pollHeaders } =
|
|
387
429
|
await getFromApi({
|
|
388
430
|
url: `${baseURL}/videos/${requestId}`,
|
|
431
|
+
validateUrl: false,
|
|
389
432
|
headers: combineHeaders(this.config.headers(), options.headers),
|
|
390
433
|
successfulResponseHandler: createJsonResponseHandler(
|
|
391
434
|
xaiVideoStatusResponseSchema,
|