@ai-sdk/xai 4.0.44 → 4.0.48

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.
@@ -0,0 +1,558 @@
1
+ import {
2
+ InvalidArgumentError,
3
+ type Experimental_BatchLanguageModelV4 as BatchLanguageModelV4,
4
+ type Experimental_BatchV4Error as BatchV4Error,
5
+ type Experimental_BatchV4ItemResult as BatchV4ItemResult,
6
+ type Experimental_BatchV4OperationOptions as BatchV4OperationOptions,
7
+ type Experimental_BatchV4StartOptions as BatchV4StartOptions,
8
+ type Experimental_BatchV4StartResult as BatchV4StartResult,
9
+ type Experimental_BatchV4Status as BatchV4Status,
10
+ type LanguageModelV4Content,
11
+ type LanguageModelV4GenerateResult,
12
+ type SharedV4ProviderMetadata,
13
+ type SharedV4Warning,
14
+ } from '@ai-sdk/provider';
15
+ import {
16
+ combineHeaders,
17
+ convertAsyncIteratorToReadableStream,
18
+ createJsonResponseHandler,
19
+ createNullLanguageModelUsage,
20
+ getFromApi,
21
+ lazySchema,
22
+ normalizeBatchRequestCounts,
23
+ postFormDataToApi,
24
+ postJsonToApi,
25
+ safeValidateTypes,
26
+ WORKFLOW_DESERIALIZE,
27
+ WORKFLOW_SERIALIZE,
28
+ zodSchema,
29
+ type InferSchema,
30
+ } from '@ai-sdk/provider-utils';
31
+ import { z } from 'zod/v4';
32
+ import { convertXaiChatUsage } from '../convert-xai-chat-usage';
33
+ import { getResponseMetadata } from '../get-response-metadata';
34
+ import { mapXaiFinishReason } from '../map-xai-finish-reason';
35
+ import {
36
+ xaiChatResponseSchema,
37
+ type XaiChatResponse,
38
+ } from '../xai-chat-language-model';
39
+ import { xaiFailedResponseHandler } from '../xai-error';
40
+ import { xaiFilesResponseSchema } from '../files/xai-files-api';
41
+ import {
42
+ XaiResponsesLanguageModel,
43
+ type XaiResponsesConfig,
44
+ } from './xai-responses-language-model';
45
+ import type { XaiResponsesModelId } from './xai-responses-language-model-options';
46
+
47
+ const xaiBatchEndpoint = '/v1/responses';
48
+ const xaiBatchName = 'ai-sdk-text-batch';
49
+ const xaiBatchResultsPageSize = 1000;
50
+
51
+ type XaiBatchRequest = Parameters<
52
+ BatchLanguageModelV4['experimental_doStartBatch']
53
+ >[0]['requests'][number];
54
+
55
+ type XaiBatchPreparedRequest = {
56
+ body: unknown;
57
+ warnings: SharedV4Warning[];
58
+ };
59
+
60
+ type XaiBatchResponseConversion =
61
+ | { success: true; result: LanguageModelV4GenerateResult }
62
+ | { success: false; error: BatchV4Error };
63
+
64
+ const xaiBatchResponseSchema = lazySchema(() =>
65
+ zodSchema(
66
+ z.object({
67
+ batch_id: z.string(),
68
+ name: z.string().nullish(),
69
+ create_time: z.string().nullish(),
70
+ expire_time: z.string().nullish(),
71
+ cancel_time: z.string().nullish(),
72
+ cancel_by_xai_message: z.string().nullish(),
73
+ state: z
74
+ .object({
75
+ num_requests: z.number().nullish(),
76
+ num_pending: z.number().nullish(),
77
+ num_success: z.number().nullish(),
78
+ num_error: z.number().nullish(),
79
+ num_cancelled: z.number().nullish(),
80
+ })
81
+ .nullish(),
82
+ }),
83
+ ),
84
+ );
85
+
86
+ type XaiBatchResponse = InferSchema<typeof xaiBatchResponseSchema>;
87
+
88
+ const xaiBatchErrorSchema = z.object({
89
+ code: z.union([z.string(), z.number()]).nullish(),
90
+ message: z.string().nullish(),
91
+ });
92
+
93
+ const xaiBatchResultSchema = z.object({
94
+ batch_request_id: z.string(),
95
+ batch_result: z
96
+ .object({
97
+ response: z
98
+ .object({
99
+ chat_get_completion: z.unknown().nullish(),
100
+ })
101
+ .nullish(),
102
+ error: xaiBatchErrorSchema.nullish(),
103
+ })
104
+ .nullish(),
105
+ error_message: z.string().nullish(),
106
+ });
107
+
108
+ type XaiBatchResult = z.infer<typeof xaiBatchResultSchema>;
109
+
110
+ const xaiBatchResultsPageSchema = lazySchema(() =>
111
+ zodSchema(
112
+ z.object({
113
+ results: z.array(xaiBatchResultSchema),
114
+ pagination_token: z.string().nullish(),
115
+ }),
116
+ ),
117
+ );
118
+
119
+ class XaiResponsesBatch {
120
+ constructor(
121
+ private readonly options: {
122
+ config: XaiResponsesConfig;
123
+ prepareRequest: (
124
+ request: XaiBatchRequest,
125
+ ) => PromiseLike<XaiBatchPreparedRequest>;
126
+ },
127
+ ) {}
128
+
129
+ async startBatch(
130
+ options: BatchV4StartOptions<XaiBatchRequest>,
131
+ ): Promise<BatchV4StartResult> {
132
+ const fileParts: string[] = [];
133
+ const warnings: BatchV4StartResult['warnings'] =
134
+ options.webhookUrl == null
135
+ ? []
136
+ : [
137
+ {
138
+ warning: {
139
+ type: 'unsupported',
140
+ feature: 'webhookUrl',
141
+ details:
142
+ 'The xAI Batch API does not support per-batch webhook URLs.',
143
+ },
144
+ },
145
+ ];
146
+
147
+ for (const request of options.requests) {
148
+ const preparedRequest = await this.options.prepareRequest(request);
149
+
150
+ fileParts.push(
151
+ JSON.stringify({
152
+ custom_id: request.id,
153
+ method: 'POST',
154
+ url: xaiBatchEndpoint,
155
+ body: preparedRequest.body,
156
+ }),
157
+ '\n',
158
+ );
159
+
160
+ for (const warning of preparedRequest.warnings) {
161
+ warnings.push({ requestId: request.id, warning });
162
+ }
163
+ }
164
+
165
+ const filename = 'batch.jsonl';
166
+ const file = new Blob(fileParts, { type: 'application/jsonl' });
167
+ fileParts.length = 0;
168
+ const formData = new FormData();
169
+ formData.append('file', file, filename);
170
+
171
+ const headers = combineHeaders(
172
+ this.options.config.headers?.(),
173
+ options.headers,
174
+ );
175
+
176
+ const { value: uploadedFile } = await postFormDataToApi({
177
+ url: this.getUrl('/files'),
178
+ headers,
179
+ formData,
180
+ failedResponseHandler: xaiFailedResponseHandler,
181
+ successfulResponseHandler: createJsonResponseHandler(
182
+ xaiFilesResponseSchema,
183
+ ),
184
+ abortSignal: options.abortSignal,
185
+ fetch: this.options.config.fetch,
186
+ });
187
+
188
+ const { value: batch } = await postJsonToApi({
189
+ url: this.getUrl('/batches'),
190
+ headers,
191
+ body: {
192
+ name: xaiBatchName,
193
+ input_file_id: uploadedFile.id,
194
+ },
195
+ failedResponseHandler: xaiFailedResponseHandler,
196
+ successfulResponseHandler: createJsonResponseHandler(
197
+ xaiBatchResponseSchema,
198
+ ),
199
+ abortSignal: options.abortSignal,
200
+ fetch: this.options.config.fetch,
201
+ });
202
+
203
+ return {
204
+ batchId: batch.batch_id,
205
+ ...convertXaiBatchStatus(batch),
206
+ warnings,
207
+ };
208
+ }
209
+
210
+ async getBatchStatus(
211
+ options: BatchV4OperationOptions,
212
+ ): Promise<BatchV4Status> {
213
+ return convertXaiBatchStatus(await this.retrieveBatch(options));
214
+ }
215
+
216
+ async getBatchResults(
217
+ options: BatchV4OperationOptions,
218
+ ): Promise<ReadableStream<BatchV4ItemResult<LanguageModelV4GenerateResult>>> {
219
+ const batch = await this.retrieveBatch(options);
220
+ if (convertXaiBatchStatus(batch).status === 'pending') {
221
+ throw new InvalidArgumentError({
222
+ argument: 'batchId',
223
+ message: `xAI batch "${options.batchId}" is not complete.`,
224
+ });
225
+ }
226
+
227
+ return convertAsyncIteratorToReadableStream(
228
+ this.iterateBatchResults(options),
229
+ );
230
+ }
231
+
232
+ private async retrieveBatch(
233
+ options: BatchV4OperationOptions,
234
+ ): Promise<XaiBatchResponse> {
235
+ const { value: batch } = await getFromApi({
236
+ url: this.getUrl(`/batches/${encodeURIComponent(options.batchId)}`),
237
+ headers: combineHeaders(this.options.config.headers?.(), options.headers),
238
+ failedResponseHandler: xaiFailedResponseHandler,
239
+ successfulResponseHandler: createJsonResponseHandler(
240
+ xaiBatchResponseSchema,
241
+ ),
242
+ abortSignal: options.abortSignal,
243
+ fetch: this.options.config.fetch,
244
+ validateUrl: false,
245
+ });
246
+
247
+ return batch;
248
+ }
249
+
250
+ private async *iterateBatchResults(
251
+ options: BatchV4OperationOptions,
252
+ ): AsyncGenerator<BatchV4ItemResult<LanguageModelV4GenerateResult>> {
253
+ let paginationToken: string | undefined;
254
+
255
+ do {
256
+ const query = new URLSearchParams({
257
+ limit: String(xaiBatchResultsPageSize),
258
+ });
259
+ if (paginationToken != null) {
260
+ query.set('pagination_token', paginationToken);
261
+ }
262
+
263
+ const { value: page } = await getFromApi({
264
+ url: this.getUrl(
265
+ `/batches/${encodeURIComponent(options.batchId)}/results?${query}`,
266
+ ),
267
+ headers: combineHeaders(
268
+ this.options.config.headers?.(),
269
+ options.headers,
270
+ ),
271
+ failedResponseHandler: xaiFailedResponseHandler,
272
+ successfulResponseHandler: createJsonResponseHandler(
273
+ xaiBatchResultsPageSchema,
274
+ ),
275
+ abortSignal: options.abortSignal,
276
+ fetch: this.options.config.fetch,
277
+ validateUrl: false,
278
+ });
279
+
280
+ for (const result of page.results) {
281
+ yield await this.convertBatchResult(result);
282
+ }
283
+
284
+ paginationToken = page.pagination_token ?? undefined;
285
+ } while (paginationToken != null);
286
+ }
287
+
288
+ private async convertBatchResult(
289
+ result: XaiBatchResult,
290
+ ): Promise<BatchV4ItemResult<LanguageModelV4GenerateResult>> {
291
+ const error = result.batch_result?.error;
292
+ if (
293
+ (result.error_message?.length ?? 0) > 0 ||
294
+ (error?.code != null && error.code !== 0 && error.code !== '0') ||
295
+ (error?.code == null && (error?.message?.length ?? 0) > 0)
296
+ ) {
297
+ const convertedError = convertXaiBatchError(result);
298
+ return {
299
+ id: result.batch_request_id,
300
+ status: isXaiCancellationError(error?.code) ? 'cancelled' : 'failed',
301
+ error: convertedError,
302
+ };
303
+ }
304
+
305
+ // xAI returns text batch results in chat completion format, including for
306
+ // requests submitted to the Responses API endpoint.
307
+ const response = result.batch_result?.response;
308
+ if (response?.chat_get_completion != null) {
309
+ const validation = await safeValidateTypes({
310
+ value: response.chat_get_completion,
311
+ schema: xaiChatResponseSchema,
312
+ });
313
+ if (!validation.success) {
314
+ return invalidXaiBatchResult(result.batch_request_id);
315
+ }
316
+
317
+ const conversion = convertXaiChatBatchResponse(validation.value);
318
+ return conversion.success
319
+ ? {
320
+ id: result.batch_request_id,
321
+ status: 'succeeded',
322
+ result: conversion.result,
323
+ }
324
+ : {
325
+ id: result.batch_request_id,
326
+ status: 'failed',
327
+ error: conversion.error,
328
+ };
329
+ }
330
+
331
+ return invalidXaiBatchResult(result.batch_request_id);
332
+ }
333
+
334
+ private getUrl(path: string) {
335
+ return `${this.options.config.baseURL ?? 'https://api.x.ai/v1'}${path}`;
336
+ }
337
+ }
338
+
339
+ export class XaiResponsesBatchLanguageModel
340
+ extends XaiResponsesLanguageModel
341
+ implements BatchLanguageModelV4
342
+ {
343
+ private readonly batch: XaiResponsesBatch;
344
+
345
+ static [WORKFLOW_SERIALIZE](model: XaiResponsesLanguageModel) {
346
+ return XaiResponsesLanguageModel[WORKFLOW_SERIALIZE](model);
347
+ }
348
+
349
+ static [WORKFLOW_DESERIALIZE](options: {
350
+ modelId: XaiResponsesModelId;
351
+ config: XaiResponsesConfig;
352
+ }) {
353
+ return new XaiResponsesBatchLanguageModel(options.modelId, options.config);
354
+ }
355
+
356
+ constructor(modelId: XaiResponsesModelId, config: XaiResponsesConfig) {
357
+ super(modelId, config);
358
+ this.batch = new XaiResponsesBatch({
359
+ config,
360
+ prepareRequest: async request => {
361
+ const { args: body, warnings } = await this.getArgs(request.options);
362
+ return { body, warnings };
363
+ },
364
+ });
365
+ }
366
+
367
+ experimental_doStartBatch(
368
+ options: Parameters<BatchLanguageModelV4['experimental_doStartBatch']>[0],
369
+ ) {
370
+ return this.batch.startBatch(options);
371
+ }
372
+
373
+ experimental_doGetBatchStatus(options: BatchV4OperationOptions) {
374
+ return this.batch.getBatchStatus(options);
375
+ }
376
+
377
+ experimental_doGetBatchResults(options: BatchV4OperationOptions) {
378
+ return this.batch.getBatchResults(options);
379
+ }
380
+ }
381
+
382
+ function convertXaiBatchStatus(batch: XaiBatchResponse): BatchV4Status {
383
+ const requestCounts = normalizeBatchRequestCounts({
384
+ total: batch.state?.num_requests,
385
+ pending: batch.state?.num_pending,
386
+ completed: batch.state?.num_success,
387
+ failed:
388
+ batch.state?.num_error != null && batch.state?.num_cancelled != null
389
+ ? batch.state.num_error + batch.state.num_cancelled
390
+ : undefined,
391
+ });
392
+ const isCancelled =
393
+ batch.cancel_time != null || batch.cancel_by_xai_message != null;
394
+ const isExpired = isPastDate(batch.expire_time);
395
+ const status: BatchV4Status['status'] =
396
+ isCancelled || isExpired
397
+ ? 'failed'
398
+ : requestCounts != null &&
399
+ requestCounts.total > 0 &&
400
+ requestCounts.pending === 0
401
+ ? 'completed'
402
+ : 'pending';
403
+
404
+ return {
405
+ status,
406
+ ...(requestCounts != null ? { requestCounts } : {}),
407
+ ...(isCancelled
408
+ ? {
409
+ error: {
410
+ message:
411
+ batch.cancel_by_xai_message ??
412
+ `xAI batch "${batch.batch_id}" was cancelled.`,
413
+ code: 'batch_cancelled',
414
+ },
415
+ }
416
+ : isExpired
417
+ ? {
418
+ error: {
419
+ message: `xAI batch "${batch.batch_id}" expired.`,
420
+ code: 'batch_expired',
421
+ },
422
+ }
423
+ : {}),
424
+ ...(batch.create_time != null ? { createdAt: batch.create_time } : {}),
425
+ ...(batch.expire_time != null ? { expiresAt: batch.expire_time } : {}),
426
+ };
427
+ }
428
+
429
+ function isPastDate(value: string | null | undefined) {
430
+ if (value == null) {
431
+ return false;
432
+ }
433
+ const timestamp = Date.parse(value);
434
+ return !Number.isNaN(timestamp) && timestamp <= Date.now();
435
+ }
436
+
437
+ function convertXaiBatchError(result: XaiBatchResult): BatchV4Error {
438
+ const error = result.batch_result?.error;
439
+ return {
440
+ message:
441
+ result.error_message || error?.message || 'xAI batch request failed.',
442
+ ...(error?.code != null && error.code !== 0 && error.code !== '0'
443
+ ? { code: String(error.code) }
444
+ : {}),
445
+ };
446
+ }
447
+
448
+ function isXaiCancellationError(code: string | number | null | undefined) {
449
+ const normalizedCode = String(code).toLowerCase();
450
+ return (
451
+ code === 1 ||
452
+ normalizedCode === '1' ||
453
+ normalizedCode === 'cancelled' ||
454
+ normalizedCode === 'batch_cancelled'
455
+ );
456
+ }
457
+
458
+ function invalidXaiBatchResult(
459
+ id: string,
460
+ ): BatchV4ItemResult<LanguageModelV4GenerateResult> {
461
+ return {
462
+ id,
463
+ status: 'failed',
464
+ error: {
465
+ message: 'xAI returned an invalid Responses batch result.',
466
+ code: 'invalid_response',
467
+ },
468
+ };
469
+ }
470
+
471
+ function convertXaiChatBatchResponse(
472
+ response: XaiChatResponse,
473
+ ): XaiBatchResponseConversion {
474
+ if (response.error != null) {
475
+ return {
476
+ success: false,
477
+ error: {
478
+ message: response.error,
479
+ ...(response.code != null ? { code: response.code } : {}),
480
+ },
481
+ };
482
+ }
483
+
484
+ const choice = response.choices?.[0];
485
+ if (choice == null) {
486
+ return {
487
+ success: false,
488
+ error: {
489
+ message: 'xAI returned a batch response without any choices.',
490
+ code: 'invalid_response',
491
+ },
492
+ };
493
+ }
494
+
495
+ if (choice.message.tool_calls?.length) {
496
+ return unsupportedXaiBatchContent('tool_calls');
497
+ }
498
+
499
+ const content: LanguageModelV4Content[] = [];
500
+ if (choice.message.content) {
501
+ content.push({ type: 'text', text: choice.message.content });
502
+ }
503
+ if (choice.message.reasoning_content) {
504
+ content.push({
505
+ type: 'reasoning',
506
+ text: choice.message.reasoning_content,
507
+ });
508
+ }
509
+ for (const url of response.citations ?? []) {
510
+ content.push({
511
+ type: 'source',
512
+ sourceType: 'url',
513
+ id: url,
514
+ url,
515
+ });
516
+ }
517
+
518
+ return {
519
+ success: true,
520
+ result: {
521
+ content,
522
+ finishReason: {
523
+ unified: mapXaiFinishReason(choice.finish_reason),
524
+ raw: choice.finish_reason ?? undefined,
525
+ },
526
+ usage: response.usage
527
+ ? convertXaiChatUsage(response.usage)
528
+ : createNullLanguageModelUsage(),
529
+ response: getResponseMetadata(response),
530
+ warnings: [],
531
+ ...((response.usage?.cost_in_usd_ticks != null ||
532
+ response.service_tier != null) && {
533
+ providerMetadata: {
534
+ xai: {
535
+ ...(response.usage?.cost_in_usd_ticks != null
536
+ ? { costInUsdTicks: response.usage.cost_in_usd_ticks }
537
+ : {}),
538
+ ...(response.service_tier != null
539
+ ? { serviceTier: response.service_tier }
540
+ : {}),
541
+ },
542
+ } satisfies SharedV4ProviderMetadata,
543
+ }),
544
+ },
545
+ };
546
+ }
547
+
548
+ function unsupportedXaiBatchContent(type: string): XaiBatchResponseConversion {
549
+ return {
550
+ success: false,
551
+ error: {
552
+ message:
553
+ `xAI returned "${type}" content, but tool content is not supported ` +
554
+ 'in AI SDK text batches.',
555
+ code: 'unsupported_content',
556
+ },
557
+ };
558
+ }