@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 CHANGED
@@ -1,5 +1,47 @@
1
1
  # @ai-sdk/xai
2
2
 
3
+ ## 4.0.12
4
+
5
+ ### Patch Changes
6
+
7
+ - 4be62c1: fix(provider-utils): validate provider-response URLs in `getFromApi`
8
+
9
+ `getFromApi` now has a `validateUrl` flag. It is optional so existing callers keep compiling (omitting it behaves like `false`, i.e. no validation), but all AI SDK provider packages set it explicitly at every call site so each one makes a visible trust decision. When `true`, the URL is routed through `fetchWithValidatedRedirects` — the same guard used by `downloadBlob` — which rejects private/loopback/link-local targets, re-validates every redirect hop, strips proxy/metadata/cookie request headers, and drops all caller headers except the user-agent on cross-origin redirects (custom API-key headers must not follow a redirect off-origin any more than `Authorization` may); blocked URLs throw `DownloadError`. It is enabled at the image/video/audio download and polling call sites where the URL comes from a provider response body; URLs built from developer-configured endpoints pass `validateUrl: false` and are unaffected.
10
+
11
+ A new optional `credentialedOrigin` withholds caller headers unless the URL is same-origin with it, so the API key is not sent to a response-supplied host on a different origin.
12
+
13
+ A new optional `trustedOrigin` exempts URLs (and redirect hops) that are same-origin with the developer-configured provider endpoint from target validation, so self-hosted and localhost deployments whose response URLs point back at the configured host keep working; all other hops are still validated.
14
+
15
+ Also closes range gaps in `validateDownloadUrl` (IPv4 `224.0.0.0/4` multicast and the TEST-NET documentation ranges `192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`; IPv6 documentation ranges `2001:db8::/32` and `3fff::/20`), and follows only the fetch-spec redirect status codes (301/302/303/307/308) — a `Location` header on any other status is not followed. This guard performs string/literal checks only and does not resolve DNS; hostnames that resolve to private addresses and DNS rebinding remain out of scope and must be constrained at the network layer (or by injecting a Node `fetch` that pins the resolved IP at connect time) for server deployments handling untrusted URLs. See `contributing/secure-url-handling.md`.
16
+
17
+ - Updated dependencies [4be62c1]
18
+ - Updated dependencies [7805e4a]
19
+ - Updated dependencies [cd12954]
20
+ - @ai-sdk/provider-utils@5.0.9
21
+ - @ai-sdk/openai-compatible@3.0.9
22
+
23
+ ## 4.0.11
24
+
25
+ ### Patch Changes
26
+
27
+ - e193290: Add `connectToWebSocket` to `@ai-sdk/provider-utils`: a shared WebSocket connect layer (constructor resolution, header hygiene, abort wiring, message decoding) analogous to `postToApi` for HTTP. The openai and xai streaming transcription models now use it instead of hand-rolled connects. For openai and xai this also means WebSocket constructor failures now surface as stream errors instead of throwing synchronously from `doStream`, an already-aborted signal no longer constructs a socket, and the caller's audio stream is cancelled on pre-open failures. Messages are processed in order with close handling deferred behind pending frames, audio send loops apply backpressure via the socket's bufferedAmount, and failed sends cancel the caller's audio stream.
28
+ - e193290: Strip undefined header values before the streaming transcription WebSocket constructor (header-capable implementations like `ws` throw on undefined values).
29
+ - e193290: Fix streaming transcription stream parts against the live xAI STT API: `transcript-final` was emitted for every `is_final: true` event, but xAI re-sends the finalized text with `speech_final: false` before the `speech_final: true` event (duplicating finals) and also finalizes _fragments_ whose text the eventual `speech_final` event merges and re-punctuates (later-revised finals). `transcript-final` is now only emitted on `speech_final: true`; finalized fragments surface as `transcript-partial`. Additionally, xAI's `transcript.done` event arrives with an empty `text`, which produced an empty `finish.text` and made `experimental_streamTranscribe` throw `NoTranscriptGeneratedError` despite a full transcript having streamed — the finish text now falls back to the accumulated finalized utterances (plus any trailing unfinalized text).
30
+ - Updated dependencies [e193290]
31
+ - @ai-sdk/provider-utils@5.0.8
32
+ - @ai-sdk/openai-compatible@3.0.8
33
+
34
+ ## 4.0.10
35
+
36
+ ### Patch Changes
37
+
38
+ - 0f93c57: feat (video): support video (not just image) reference inputs in `inputReferences` for reference-to-video generation
39
+ - d25a084: feat (provider/xai): add grok-4.5 model id
40
+ - Updated dependencies [0f93c57]
41
+ - @ai-sdk/provider@4.0.3
42
+ - @ai-sdk/openai-compatible@3.0.7
43
+ - @ai-sdk/provider-utils@5.0.7
44
+
3
45
  ## 4.0.9
4
46
 
5
47
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ import * as _ai_sdk_provider_utils from '@ai-sdk/provider-utils';
3
3
  import { InferSchema, FetchFunction, WebSocketConstructor } from '@ai-sdk/provider-utils';
4
4
  import { ProviderV4, LanguageModelV4, ImageModelV4, Experimental_VideoModelV4, Experimental_RealtimeFactoryV4, SpeechModelV4, TranscriptionModelV4, FilesV4, Experimental_RealtimeModelV4, Experimental_RealtimeModelV4ClientSecretOptions, Experimental_RealtimeModelV4ClientSecretResult, Experimental_RealtimeModelV4ServerEvent, Experimental_RealtimeModelV4ClientEvent, Experimental_RealtimeModelV4SessionConfig } from '@ai-sdk/provider';
5
5
 
6
- type XaiChatModelId = 'grok-4.20-non-reasoning' | 'grok-4.20-reasoning' | 'grok-4.3' | 'grok-latest' | (string & {});
6
+ type XaiChatModelId = 'grok-4.20-non-reasoning' | 'grok-4.20-reasoning' | 'grok-4.3' | 'grok-4.5' | 'grok-latest' | (string & {});
7
7
  declare const xaiLanguageModelChatOptions: z.ZodObject<{
8
8
  reasoningEffort: z.ZodOptional<z.ZodEnum<{
9
9
  none: "none";
@@ -72,7 +72,7 @@ declare const xaiFilePartProviderOptions: z.ZodObject<{
72
72
  }, z.core.$strip>;
73
73
  type XaiFilePartProviderOptions = z.infer<typeof xaiFilePartProviderOptions>;
74
74
 
75
- type XaiResponsesModelId = 'grok-4.20-non-reasoning' | 'grok-4.20-reasoning' | 'grok-4.3' | 'grok-latest' | (string & {});
75
+ type XaiResponsesModelId = 'grok-4.20-non-reasoning' | 'grok-4.20-reasoning' | 'grok-4.3' | 'grok-4.5' | 'grok-latest' | (string & {});
76
76
  /**
77
77
  * @see https://docs.x.ai/docs/api-reference#create-new-response
78
78
  */
package/dist/index.js CHANGED
@@ -1181,6 +1181,9 @@ var XaiImageModel = class _XaiImageModel {
1181
1181
  async downloadImage(url, abortSignal) {
1182
1182
  const { value } = await getFromApi({
1183
1183
  url,
1184
+ // url is a generated-image URL from the provider response; validate it.
1185
+ validateUrl: true,
1186
+ trustedOrigin: this.config.baseURL,
1184
1187
  abortSignal,
1185
1188
  failedResponseHandler: createStatusCodeErrorResponseHandler(),
1186
1189
  successfulResponseHandler: createBinaryResponseHandler(),
@@ -3474,7 +3477,7 @@ var xaiTools = {
3474
3477
  };
3475
3478
 
3476
3479
  // src/version.ts
3477
- var VERSION = true ? "4.0.9" : "0.0.0-test";
3480
+ var VERSION = true ? "4.0.12" : "0.0.0-test";
3478
3481
 
3479
3482
  // src/files/xai-files.ts
3480
3483
  import {
@@ -3587,6 +3590,7 @@ import {
3587
3590
  createJsonResponseHandler as createJsonResponseHandler5,
3588
3591
  delay,
3589
3592
  getFromApi as getFromApi2,
3593
+ getTopLevelMediaType as getTopLevelMediaType3,
3590
3594
  parseProviderOptions as parseProviderOptions7,
3591
3595
  postJsonToApi as postJsonToApi4
3592
3596
  } from "@ai-sdk/provider-utils";
@@ -3661,17 +3665,28 @@ function resolveStartImage(options) {
3661
3665
  var _a;
3662
3666
  return (_a = getFirstFrameImage(options)) != null ? _a : options.image;
3663
3667
  }
3668
+ var isVideoFile = (file) => file.mediaType != null && getTopLevelMediaType3(file.mediaType) === "video";
3664
3669
  function fileToXaiImageUrl(file) {
3665
- var _a;
3666
3670
  if (file.type === "url") {
3667
3671
  return file.url;
3668
3672
  }
3669
3673
  const base64Data = typeof file.data === "string" ? file.data : convertUint8ArrayToBase64(file.data);
3670
- return `data:${(_a = file.mediaType) != null ? _a : "image/png"};base64,${base64Data}`;
3674
+ return `data:${file.mediaType};base64,${base64Data}`;
3671
3675
  }
3672
- function resolveReferenceImages(options, xaiOptions) {
3676
+ function resolveReferenceImages(options, xaiOptions, warnings) {
3673
3677
  if (options.inputReferences != null && options.inputReferences.length > 0) {
3674
- return options.inputReferences.map((reference) => ({
3678
+ const imageReferences = options.inputReferences.filter((reference) => {
3679
+ if (isVideoFile(reference)) {
3680
+ warnings.push({
3681
+ type: "unsupported",
3682
+ feature: "inputReferences",
3683
+ details: 'xAI reference-to-video accepts image references only. The video reference was ignored. Use providerOptions.xai.mode "extend-video" to continue from a video.'
3684
+ });
3685
+ return false;
3686
+ }
3687
+ return true;
3688
+ });
3689
+ return imageReferences.map((reference) => ({
3675
3690
  url: fileToXaiImageUrl(reference)
3676
3691
  }));
3677
3692
  }
@@ -3809,17 +3824,31 @@ var XaiVideoModel = class {
3809
3824
  }
3810
3825
  const startImage = resolveStartImage(options);
3811
3826
  if (startImage != null) {
3812
- body.image = { url: fileToXaiImageUrl(startImage) };
3827
+ if (isVideoFile(startImage)) {
3828
+ const fromFrameImages = getFirstFrameImage(options) != null;
3829
+ warnings.push({
3830
+ type: "unsupported",
3831
+ feature: fromFrameImages ? "frameImages" : "image",
3832
+ details: 'xAI does not accept a video as a start/frame image. The video was ignored. Use providerOptions.xai.mode "extend-video" to continue from a video instead.'
3833
+ });
3834
+ } else {
3835
+ body.image = { url: fileToXaiImageUrl(startImage) };
3836
+ }
3813
3837
  }
3814
- if (getLastFrameImage(options) != null) {
3838
+ const lastFrameImage = getLastFrameImage(options);
3839
+ if (lastFrameImage != null) {
3815
3840
  warnings.push({
3816
3841
  type: "unsupported",
3817
3842
  feature: "frameImages",
3818
- details: `xAI video models do not support last_frame. Use providerOptions.xai.mode "extend-video" to continue from a video's last frame. The last frame image was ignored.`
3843
+ details: isVideoFile(lastFrameImage) ? 'xAI does not accept a video as a start/frame image. The video last frame was ignored. Use providerOptions.xai.mode "extend-video" to continue from a video instead.' : `xAI video models do not support last_frame. Use providerOptions.xai.mode "extend-video" to continue from a video's last frame. The last frame image was ignored.`
3819
3844
  });
3820
3845
  }
3821
3846
  if (hasReferenceImages) {
3822
- const referenceImages = resolveReferenceImages(options, xaiOptions);
3847
+ const referenceImages = resolveReferenceImages(
3848
+ options,
3849
+ xaiOptions,
3850
+ warnings
3851
+ );
3823
3852
  if (referenceImages != null) {
3824
3853
  body.reference_images = referenceImages;
3825
3854
  }
@@ -3886,6 +3915,7 @@ var XaiVideoModel = class {
3886
3915
  }
3887
3916
  const { value: statusResponse, responseHeaders: pollHeaders } = await getFromApi2({
3888
3917
  url: `${baseURL}/videos/${requestId}`,
3918
+ validateUrl: false,
3889
3919
  headers: combineHeaders5(this.config.headers(), options.headers),
3890
3920
  successfulResponseHandler: createJsonResponseHandler5(
3891
3921
  xaiVideoStatusResponseSchema
@@ -4146,14 +4176,14 @@ import {
4146
4176
  combineHeaders as combineHeaders7,
4147
4177
  convertBase64ToUint8Array,
4148
4178
  createJsonResponseHandler as createJsonResponseHandler6,
4149
- getWebSocketConstructor,
4179
+ connectToWebSocket,
4150
4180
  mediaTypeToExtension,
4151
4181
  parseProviderOptions as parseProviderOptions9,
4152
4182
  postFormDataToApi as postFormDataToApi2,
4153
- readWebSocketMessageText,
4154
4183
  safeParseJSON as safeParseJSON2,
4155
4184
  serializeModelOptions as serializeModelOptions5,
4156
4185
  toWebSocketUrl,
4186
+ waitForWebSocketBufferDrain,
4157
4187
  WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE5,
4158
4188
  WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE5
4159
4189
  } from "@ai-sdk/provider-utils";
@@ -4404,19 +4434,21 @@ function createXaiStreamingTranscriptionStream({
4404
4434
  };
4405
4435
  return new ReadableStream({
4406
4436
  start: (controller) => {
4407
- const WebSocketConstructor = getWebSocketConstructor(webSocket);
4408
- const ws = new WebSocketConstructor(url, void 0, { headers });
4409
4437
  const doneTexts = /* @__PURE__ */ new Map();
4438
+ const finalizedTexts = /* @__PURE__ */ new Map();
4439
+ const pendingTexts = /* @__PURE__ */ new Map();
4410
4440
  let doneDuration;
4411
4441
  let audioReader;
4442
+ let connection;
4412
4443
  cleanup = (closeCode) => {
4413
- abortSignal == null ? void 0 : abortSignal.removeEventListener("abort", abort);
4414
- void (audioReader == null ? void 0 : audioReader.cancel().catch(() => {
4415
- }));
4416
- try {
4417
- ws.close(closeCode);
4418
- } catch (e) {
4444
+ if (audioReader != null) {
4445
+ void audioReader.cancel().catch(() => {
4446
+ });
4447
+ } else {
4448
+ void audio.cancel().catch(() => {
4449
+ });
4419
4450
  }
4451
+ connection == null ? void 0 : connection.close(closeCode);
4420
4452
  };
4421
4453
  const finishWithError = (error) => {
4422
4454
  if (finished) return;
@@ -4438,35 +4470,34 @@ function createXaiStreamingTranscriptionStream({
4438
4470
  controller.close();
4439
4471
  cleanup(1e3);
4440
4472
  };
4441
- const abort = () => {
4442
- var _a;
4443
- finishWithError((_a = abortSignal == null ? void 0 : abortSignal.reason) != null ? _a : new Error("Aborted"));
4444
- };
4445
- if (abortSignal == null ? void 0 : abortSignal.aborted) {
4446
- abort();
4447
- return;
4448
- }
4449
- abortSignal == null ? void 0 : abortSignal.addEventListener("abort", abort, { once: true });
4450
- const sendAudio = async () => {
4473
+ const sendAudio = async (socket) => {
4451
4474
  audioReader = audio.getReader();
4452
4475
  try {
4453
4476
  while (true) {
4454
4477
  const { done, value } = await audioReader.read();
4455
4478
  if (done || finished) break;
4456
- ws.send(
4479
+ socket.send(
4457
4480
  value instanceof Uint8Array ? value : convertBase64ToUint8Array(value)
4458
4481
  );
4482
+ await waitForWebSocketBufferDrain(socket);
4459
4483
  }
4460
4484
  } finally {
4461
4485
  audioReader.releaseLock();
4486
+ audioReader = void 0;
4462
4487
  }
4463
4488
  if (!finished) {
4464
- ws.send(JSON.stringify({ type: "audio.done" }));
4489
+ socket.send(JSON.stringify({ type: "audio.done" }));
4465
4490
  }
4466
4491
  };
4467
- ws.onmessage = (event) => {
4468
- void readWebSocketMessageText(event.data).then(async (text) => {
4469
- var _a, _b, _c, _d, _e, _f;
4492
+ connection = connectToWebSocket({
4493
+ url,
4494
+ headers,
4495
+ webSocket,
4496
+ abortSignal,
4497
+ onAbort: finishWithError,
4498
+ onProcessingError: finishWithError,
4499
+ onMessageText: async (text) => {
4500
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
4470
4501
  const parsed = await safeParseJSON2({ text });
4471
4502
  if (!parsed.success) return;
4472
4503
  const raw = parsed.value;
@@ -4476,25 +4507,39 @@ function createXaiStreamingTranscriptionStream({
4476
4507
  switch (raw.type) {
4477
4508
  case "transcript.created": {
4478
4509
  controller.enqueue({ type: "stream-start", warnings });
4479
- void sendAudio().catch(finishWithError);
4510
+ const socket = connection == null ? void 0 : connection.socket;
4511
+ if (socket == null) {
4512
+ finishWithError(new Error("WebSocket is not connected."));
4513
+ break;
4514
+ }
4515
+ void sendAudio(socket).catch(finishWithError);
4480
4516
  break;
4481
4517
  }
4482
4518
  case "transcript.partial": {
4483
4519
  const id = channelId(raw.channel_index);
4484
- const timing = timingFromXaiEvent(raw);
4485
- if (raw.is_final) {
4520
+ const channelIndex = (_a = raw.channel_index) != null ? _a : 0;
4521
+ if (raw.is_final && raw.speech_final) {
4522
+ const timing = timingFromXaiEvent(raw);
4523
+ if (raw.text) {
4524
+ finalizedTexts.set(channelIndex, [
4525
+ ...(_b = finalizedTexts.get(channelIndex)) != null ? _b : [],
4526
+ raw.text
4527
+ ]);
4528
+ }
4529
+ pendingTexts.delete(channelIndex);
4486
4530
  controller.enqueue({
4487
4531
  type: "transcript-final",
4488
4532
  id,
4489
- text: (_a = raw.text) != null ? _a : "",
4533
+ text: (_c = raw.text) != null ? _c : "",
4490
4534
  ...timing,
4491
4535
  channelIndex: raw.channel_index
4492
4536
  });
4493
4537
  } else {
4538
+ pendingTexts.set(channelIndex, (_d = raw.text) != null ? _d : "");
4494
4539
  controller.enqueue({
4495
4540
  type: "transcript-partial",
4496
4541
  id,
4497
- text: (_b = raw.text) != null ? _b : "",
4542
+ text: (_e = raw.text) != null ? _e : "",
4498
4543
  startSecond: raw.start,
4499
4544
  durationInSeconds: raw.duration,
4500
4545
  channelIndex: raw.channel_index
@@ -4503,32 +4548,36 @@ function createXaiStreamingTranscriptionStream({
4503
4548
  break;
4504
4549
  }
4505
4550
  case "transcript.done": {
4506
- const channelIndex = (_c = raw.channel_index) != null ? _c : 0;
4507
- doneTexts.set(channelIndex, (_d = raw.text) != null ? _d : "");
4508
- doneDuration = (_e = raw.duration) != null ? _e : doneDuration;
4551
+ const channelIndex = (_f = raw.channel_index) != null ? _f : 0;
4552
+ const accumulated = [
4553
+ ...(_g = finalizedTexts.get(channelIndex)) != null ? _g : [],
4554
+ ...pendingTexts.get(channelIndex) ? [pendingTexts.get(channelIndex)] : []
4555
+ ].join(" ");
4556
+ doneTexts.set(channelIndex, raw.text || accumulated);
4557
+ doneDuration = (_h = raw.duration) != null ? _h : doneDuration;
4509
4558
  maybeFinish();
4510
4559
  break;
4511
4560
  }
4512
4561
  case "error": {
4513
- finishWithError(new Error((_f = raw.message) != null ? _f : "xAI STT error"));
4562
+ finishWithError(new Error((_i = raw.message) != null ? _i : "xAI STT error"));
4514
4563
  break;
4515
4564
  }
4516
4565
  }
4517
- }).catch(finishWithError);
4518
- };
4519
- ws.onerror = () => {
4520
- finishWithError(
4521
- new Error(
4522
- "xAI streaming transcription error." + (webSocket == null ? " Note: the native WebSocket implementation in browsers, Node.js, Deno, and Bun cannot send the Authorization header required by xAI. Pass a header-capable WebSocket implementation (e.g. the 'ws' package) via createXai({ webSocket })." : "")
4523
- )
4524
- );
4525
- };
4526
- ws.onclose = () => {
4527
- if (finished) return;
4528
- finished = true;
4529
- cleanup();
4530
- controller.close();
4531
- };
4566
+ },
4567
+ onSocketError: () => {
4568
+ finishWithError(
4569
+ new Error(
4570
+ "xAI streaming transcription error." + (webSocket == null ? " Note: the native WebSocket implementation in browsers, Node.js, Deno, and Bun cannot send the Authorization header required by xAI. Pass a header-capable WebSocket implementation (e.g. the 'ws' package) via createXai({ webSocket })." : "")
4571
+ )
4572
+ );
4573
+ },
4574
+ onClose: () => {
4575
+ if (finished) return;
4576
+ finished = true;
4577
+ cleanup();
4578
+ controller.close();
4579
+ }
4580
+ });
4532
4581
  },
4533
4582
  cancel: () => {
4534
4583
  if (finished) return;