@ai-sdk/xai 4.0.10 → 4.0.13
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 +39 -0
- package/dist/index.js +73 -50
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/xai-image-model.ts +3 -0
- package/src/xai-transcription-model.ts +122 -93
- package/src/xai-video-model.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# @ai-sdk/xai
|
|
2
2
|
|
|
3
|
+
## 4.0.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [31c7be8]
|
|
8
|
+
- @ai-sdk/provider-utils@5.0.10
|
|
9
|
+
- @ai-sdk/openai-compatible@3.0.10
|
|
10
|
+
|
|
11
|
+
## 4.0.12
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- 4be62c1: fix(provider-utils): validate provider-response URLs in `getFromApi`
|
|
16
|
+
|
|
17
|
+
`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.
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
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`.
|
|
24
|
+
|
|
25
|
+
- Updated dependencies [4be62c1]
|
|
26
|
+
- Updated dependencies [7805e4a]
|
|
27
|
+
- Updated dependencies [cd12954]
|
|
28
|
+
- @ai-sdk/provider-utils@5.0.9
|
|
29
|
+
- @ai-sdk/openai-compatible@3.0.9
|
|
30
|
+
|
|
31
|
+
## 4.0.11
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- 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.
|
|
36
|
+
- e193290: Strip undefined header values before the streaming transcription WebSocket constructor (header-capable implementations like `ws` throw on undefined values).
|
|
37
|
+
- 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).
|
|
38
|
+
- Updated dependencies [e193290]
|
|
39
|
+
- @ai-sdk/provider-utils@5.0.8
|
|
40
|
+
- @ai-sdk/openai-compatible@3.0.8
|
|
41
|
+
|
|
3
42
|
## 4.0.10
|
|
4
43
|
|
|
5
44
|
### Patch Changes
|
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.
|
|
3480
|
+
var VERSION = true ? "4.0.13" : "0.0.0-test";
|
|
3478
3481
|
|
|
3479
3482
|
// src/files/xai-files.ts
|
|
3480
3483
|
import {
|
|
@@ -3912,6 +3915,7 @@ var XaiVideoModel = class {
|
|
|
3912
3915
|
}
|
|
3913
3916
|
const { value: statusResponse, responseHeaders: pollHeaders } = await getFromApi2({
|
|
3914
3917
|
url: `${baseURL}/videos/${requestId}`,
|
|
3918
|
+
validateUrl: false,
|
|
3915
3919
|
headers: combineHeaders5(this.config.headers(), options.headers),
|
|
3916
3920
|
successfulResponseHandler: createJsonResponseHandler5(
|
|
3917
3921
|
xaiVideoStatusResponseSchema
|
|
@@ -4172,14 +4176,14 @@ import {
|
|
|
4172
4176
|
combineHeaders as combineHeaders7,
|
|
4173
4177
|
convertBase64ToUint8Array,
|
|
4174
4178
|
createJsonResponseHandler as createJsonResponseHandler6,
|
|
4175
|
-
|
|
4179
|
+
connectToWebSocket,
|
|
4176
4180
|
mediaTypeToExtension,
|
|
4177
4181
|
parseProviderOptions as parseProviderOptions9,
|
|
4178
4182
|
postFormDataToApi as postFormDataToApi2,
|
|
4179
|
-
readWebSocketMessageText,
|
|
4180
4183
|
safeParseJSON as safeParseJSON2,
|
|
4181
4184
|
serializeModelOptions as serializeModelOptions5,
|
|
4182
4185
|
toWebSocketUrl,
|
|
4186
|
+
waitForWebSocketBufferDrain,
|
|
4183
4187
|
WORKFLOW_DESERIALIZE as WORKFLOW_DESERIALIZE5,
|
|
4184
4188
|
WORKFLOW_SERIALIZE as WORKFLOW_SERIALIZE5
|
|
4185
4189
|
} from "@ai-sdk/provider-utils";
|
|
@@ -4430,19 +4434,21 @@ function createXaiStreamingTranscriptionStream({
|
|
|
4430
4434
|
};
|
|
4431
4435
|
return new ReadableStream({
|
|
4432
4436
|
start: (controller) => {
|
|
4433
|
-
const WebSocketConstructor = getWebSocketConstructor(webSocket);
|
|
4434
|
-
const ws = new WebSocketConstructor(url, void 0, { headers });
|
|
4435
4437
|
const doneTexts = /* @__PURE__ */ new Map();
|
|
4438
|
+
const finalizedTexts = /* @__PURE__ */ new Map();
|
|
4439
|
+
const pendingTexts = /* @__PURE__ */ new Map();
|
|
4436
4440
|
let doneDuration;
|
|
4437
4441
|
let audioReader;
|
|
4442
|
+
let connection;
|
|
4438
4443
|
cleanup = (closeCode) => {
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4444
|
+
if (audioReader != null) {
|
|
4445
|
+
void audioReader.cancel().catch(() => {
|
|
4446
|
+
});
|
|
4447
|
+
} else {
|
|
4448
|
+
void audio.cancel().catch(() => {
|
|
4449
|
+
});
|
|
4445
4450
|
}
|
|
4451
|
+
connection == null ? void 0 : connection.close(closeCode);
|
|
4446
4452
|
};
|
|
4447
4453
|
const finishWithError = (error) => {
|
|
4448
4454
|
if (finished) return;
|
|
@@ -4464,35 +4470,34 @@ function createXaiStreamingTranscriptionStream({
|
|
|
4464
4470
|
controller.close();
|
|
4465
4471
|
cleanup(1e3);
|
|
4466
4472
|
};
|
|
4467
|
-
const
|
|
4468
|
-
var _a;
|
|
4469
|
-
finishWithError((_a = abortSignal == null ? void 0 : abortSignal.reason) != null ? _a : new Error("Aborted"));
|
|
4470
|
-
};
|
|
4471
|
-
if (abortSignal == null ? void 0 : abortSignal.aborted) {
|
|
4472
|
-
abort();
|
|
4473
|
-
return;
|
|
4474
|
-
}
|
|
4475
|
-
abortSignal == null ? void 0 : abortSignal.addEventListener("abort", abort, { once: true });
|
|
4476
|
-
const sendAudio = async () => {
|
|
4473
|
+
const sendAudio = async (socket) => {
|
|
4477
4474
|
audioReader = audio.getReader();
|
|
4478
4475
|
try {
|
|
4479
4476
|
while (true) {
|
|
4480
4477
|
const { done, value } = await audioReader.read();
|
|
4481
4478
|
if (done || finished) break;
|
|
4482
|
-
|
|
4479
|
+
socket.send(
|
|
4483
4480
|
value instanceof Uint8Array ? value : convertBase64ToUint8Array(value)
|
|
4484
4481
|
);
|
|
4482
|
+
await waitForWebSocketBufferDrain(socket);
|
|
4485
4483
|
}
|
|
4486
4484
|
} finally {
|
|
4487
4485
|
audioReader.releaseLock();
|
|
4486
|
+
audioReader = void 0;
|
|
4488
4487
|
}
|
|
4489
4488
|
if (!finished) {
|
|
4490
|
-
|
|
4489
|
+
socket.send(JSON.stringify({ type: "audio.done" }));
|
|
4491
4490
|
}
|
|
4492
4491
|
};
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
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;
|
|
4496
4501
|
const parsed = await safeParseJSON2({ text });
|
|
4497
4502
|
if (!parsed.success) return;
|
|
4498
4503
|
const raw = parsed.value;
|
|
@@ -4502,25 +4507,39 @@ function createXaiStreamingTranscriptionStream({
|
|
|
4502
4507
|
switch (raw.type) {
|
|
4503
4508
|
case "transcript.created": {
|
|
4504
4509
|
controller.enqueue({ type: "stream-start", warnings });
|
|
4505
|
-
void
|
|
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);
|
|
4506
4516
|
break;
|
|
4507
4517
|
}
|
|
4508
4518
|
case "transcript.partial": {
|
|
4509
4519
|
const id = channelId(raw.channel_index);
|
|
4510
|
-
const
|
|
4511
|
-
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);
|
|
4512
4530
|
controller.enqueue({
|
|
4513
4531
|
type: "transcript-final",
|
|
4514
4532
|
id,
|
|
4515
|
-
text: (
|
|
4533
|
+
text: (_c = raw.text) != null ? _c : "",
|
|
4516
4534
|
...timing,
|
|
4517
4535
|
channelIndex: raw.channel_index
|
|
4518
4536
|
});
|
|
4519
4537
|
} else {
|
|
4538
|
+
pendingTexts.set(channelIndex, (_d = raw.text) != null ? _d : "");
|
|
4520
4539
|
controller.enqueue({
|
|
4521
4540
|
type: "transcript-partial",
|
|
4522
4541
|
id,
|
|
4523
|
-
text: (
|
|
4542
|
+
text: (_e = raw.text) != null ? _e : "",
|
|
4524
4543
|
startSecond: raw.start,
|
|
4525
4544
|
durationInSeconds: raw.duration,
|
|
4526
4545
|
channelIndex: raw.channel_index
|
|
@@ -4529,32 +4548,36 @@ function createXaiStreamingTranscriptionStream({
|
|
|
4529
4548
|
break;
|
|
4530
4549
|
}
|
|
4531
4550
|
case "transcript.done": {
|
|
4532
|
-
const channelIndex = (
|
|
4533
|
-
|
|
4534
|
-
|
|
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;
|
|
4535
4558
|
maybeFinish();
|
|
4536
4559
|
break;
|
|
4537
4560
|
}
|
|
4538
4561
|
case "error": {
|
|
4539
|
-
finishWithError(new Error((
|
|
4562
|
+
finishWithError(new Error((_i = raw.message) != null ? _i : "xAI STT error"));
|
|
4540
4563
|
break;
|
|
4541
4564
|
}
|
|
4542
4565
|
}
|
|
4543
|
-
}
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
)
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
};
|
|
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
|
+
});
|
|
4558
4581
|
},
|
|
4559
4582
|
cancel: () => {
|
|
4560
4583
|
if (finished) return;
|