@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/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
- <Tabs items={['pnpm', 'npm', 'yarn', 'bun']}>
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 | Object Generation | Tool Usage | Tool Streaming | Reasoning |
560
- | ----------------------------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- |
561
- | `grok-4.20-reasoning` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
562
- | `grok-4.20-non-reasoning` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> |
563
- | `grok-4-1-fast-reasoning` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
564
- | `grok-4-1-fast-non-reasoning` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> |
565
- | `grok-4-1` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> |
566
- | `grok-4-fast-reasoning` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
567
- | `grok-4-fast-non-reasoning` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> |
568
- | `grok-code-fast-1` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
569
- | `grok-3` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Cross size={18} /> |
570
- | `grok-3-mini` | <Cross size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
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 | Speed | Output Formats |
679
- | --------- | ------------------- | ------------------- | -------------------------- |
680
- | `default` | <Check size={18} /> | <Check size={18} /> | mp3, wav, pcm, mulaw, alaw |
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 | Streaming | Word Timestamps | Diarization | Multichannel |
786
- | --------- | ------------------- | ------------------- | ------------------- | ------------------- | ------------------- |
787
- | `default` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
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 size={18} /> |
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 size={18} /> |
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 | Editing | Extension | R2V |
1272
- | -------------------- | -------- | ------------------------------------------------- | -------------- | ------------------- | ------------------- | ------------------- | ------------------- |
1273
- | `grok-imagine-video` | 1–15s | `1:1`, `16:9`, `9:16`, `4:3`, `3:4`, `3:2`, `2:3` | `480p`, `720p` | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> | <Check size={18} /> |
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.9",
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.6",
33
- "@ai-sdk/provider": "4.0.2",
34
- "@ai-sdk/provider-utils": "5.0.6"
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
- "@vercel/ai-tsconfig": "0.0.0",
42
- "@ai-sdk/test-server": "2.0.0"
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"
@@ -4,6 +4,7 @@ export type XaiResponsesModelId =
4
4
  | 'grok-4.20-non-reasoning'
5
5
  | 'grok-4.20-reasoning'
6
6
  | 'grok-4.3'
7
+ | 'grok-4.5'
7
8
  | 'grok-latest'
8
9
  | (string & {});
9
10
 
@@ -5,6 +5,7 @@ export type XaiChatModelId =
5
5
  | 'grok-4.20-non-reasoning'
6
6
  | 'grok-4.20-reasoning'
7
7
  | 'grok-4.3'
8
+ | 'grok-4.5'
8
9
  | 'grok-latest'
9
10
  | (string & {});
10
11
 
@@ -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
- getWebSocketConstructor,
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
- abortSignal?.removeEventListener('abort', abort);
286
- void audioReader?.cancel().catch(() => {});
287
- try {
288
- ws.close(closeCode);
289
- } catch {}
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 abort = () => {
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
- ws.send(
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
- ws.send(JSON.stringify({ type: 'audio.done' }));
345
+ socket.send(JSON.stringify({ type: 'audio.done' }));
343
346
  }
344
347
  };
345
348
 
346
- ws.onmessage = event => {
347
- void readWebSocketMessageText(event.data)
348
- .then(async text => {
349
- const parsed = await safeParseJSON({ text });
350
- if (!parsed.success) return;
351
- const raw = parsed.value as XaiStreamingTranscriptionEvent;
352
-
353
- if (includeRawChunks) {
354
- controller.enqueue({ type: 'raw', rawValue: raw });
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
- switch (raw.type) {
358
- case 'transcript.created': {
359
- controller.enqueue({ type: 'stream-start', warnings });
360
- void sendAudio().catch(finishWithError);
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
- case 'transcript.partial': {
365
- const id = channelId(raw.channel_index);
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.is_final) {
368
- controller.enqueue({
369
- type: 'transcript-final',
370
- id,
371
- text: raw.text ?? '',
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
- break;
386
- }
387
-
388
- case 'transcript.done': {
389
- const channelIndex = raw.channel_index ?? 0;
390
- doneTexts.set(channelIndex, raw.text ?? '');
391
- doneDuration = raw.duration ?? doneDuration;
392
- maybeFinish();
393
- break;
394
- }
395
-
396
- case 'error': {
397
- // xAI STT errors are terminal: surface the server message
398
- // instead of letting the socket close mask it.
399
- finishWithError(new Error(raw.message ?? 'xAI STT error'));
400
- break;
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
- ws.onerror = () => {
408
- finishWithError(
409
- new Error(
410
- 'xAI streaming transcription error.' +
411
- (webSocket == null
412
- ? ' Note: the native WebSocket implementation in browsers,' +
413
- ' Node.js, Deno, and Bun cannot send the Authorization' +
414
- ' header required by xAI. Pass a header-capable WebSocket' +
415
- " implementation (e.g. the 'ws' package) via" +
416
- ' createXai({ webSocket }).'
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
- ws.onclose = () => {
423
- if (finished) return;
424
- finished = true;
425
- cleanup();
426
- controller.close();
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: () => {
@@ -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 ?? 'image/png'};base64,${base64Data}`;
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
- return options.inputReferences.map(reference => ({
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
- body.image = { url: fileToXaiImageUrl(startImage) };
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
- if (getLastFrameImage(options) != null) {
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 models do not support last_frame. Use ' +
291
- 'providerOptions.xai.mode "extend-video" to continue from a ' +
292
- "video's last frame. The last frame image was ignored.",
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(options, xaiOptions);
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,