@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/xai",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@ai-sdk/openai-compatible": "3.0.
|
|
32
|
+
"@ai-sdk/openai-compatible": "3.0.10",
|
|
33
33
|
"@ai-sdk/provider": "4.0.3",
|
|
34
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
34
|
+
"@ai-sdk/provider-utils": "5.0.10"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "22.19.19",
|
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
|
@@ -428,6 +428,7 @@ export class XaiVideoModel implements Experimental_VideoModelV4 {
|
|
|
428
428
|
const { value: statusResponse, responseHeaders: pollHeaders } =
|
|
429
429
|
await getFromApi({
|
|
430
430
|
url: `${baseURL}/videos/${requestId}`,
|
|
431
|
+
validateUrl: false,
|
|
431
432
|
headers: combineHeaders(this.config.headers(), options.headers),
|
|
432
433
|
successfulResponseHandler: createJsonResponseHandler(
|
|
433
434
|
xaiVideoStatusResponseSchema,
|