@ai-sdk/xai 4.0.56 → 4.0.58

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/xai",
3
- "version": "4.0.56",
3
+ "version": "4.0.58",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -29,8 +29,8 @@
29
29
  }
30
30
  },
31
31
  "dependencies": {
32
- "@ai-sdk/provider": "4.0.12",
33
- "@ai-sdk/provider-utils": "5.0.38"
32
+ "@ai-sdk/provider": "4.0.14",
33
+ "@ai-sdk/provider-utils": "5.0.40"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@ai-sdk/test-server": "2.0.1",
package/src/xai-batch.ts CHANGED
@@ -1,23 +1,33 @@
1
1
  import {
2
2
  InvalidArgumentError,
3
+ UnsupportedFunctionalityError,
3
4
  type Experimental_BatchV4 as BatchV4,
5
+ type Experimental_BatchV4CancelResult as BatchV4CancelResult,
4
6
  type Experimental_BatchV4Error as BatchV4Error,
5
7
  type Experimental_BatchV4ItemResult as BatchV4ItemResult,
8
+ type Experimental_BatchV4ListOptions as BatchV4ListOptions,
9
+ type Experimental_BatchV4ListResult as BatchV4ListResult,
6
10
  type Experimental_BatchV4OperationOptions as BatchV4OperationOptions,
7
11
  type Experimental_BatchV4StartResult as BatchV4StartResult,
8
12
  type Experimental_BatchV4Status as BatchV4Status,
9
13
  type Experimental_TextBatchV4Request as TextBatchV4Request,
14
+ type Experimental_ImageBatchV4Request as ImageBatchV4Request,
15
+ type Experimental_ImageBatchV4ItemResult as ImageBatchV4ItemResult,
10
16
  type Experimental_TextBatchV4ItemResult as TextBatchV4ItemResult,
11
17
  type Experimental_BatchV4StartOptions as BatchV4StartOptions,
12
18
  type LanguageModelV4Content,
13
19
  type LanguageModelV4GenerateResult,
14
20
  type SharedV4ProviderMetadata,
15
21
  type SharedV4Warning,
22
+ type ImageModelV4Result,
16
23
  } from '@ai-sdk/provider';
17
24
  import {
18
25
  combineHeaders,
26
+ convertImageModelFileToDataUri,
27
+ convertBase64ToUint8Array,
19
28
  convertAsyncIteratorToReadableStream,
20
29
  createJsonResponseHandler,
30
+ createBinaryResponseHandler,
21
31
  createNullLanguageModelUsage,
22
32
  getFromApi,
23
33
  lazySchema,
@@ -45,6 +55,8 @@ import {
45
55
  type XaiResponsesConfig,
46
56
  } from './responses/xai-responses-language-model';
47
57
  import type { XaiResponsesModelId } from './responses/xai-responses-language-model-options';
58
+ import type { XaiImageModelId } from './xai-image-settings';
59
+ import { xaiImageModelOptions } from './xai-image-model-options';
48
60
 
49
61
  const xaiBatchEndpoint = '/v1/responses';
50
62
  const xaiBatchName = 'ai-sdk-text-batch';
@@ -68,38 +80,70 @@ const xaiBatchProviderOptionsSchema = lazySchema(() =>
68
80
  ),
69
81
  );
70
82
 
71
- type XaiBatchModelIds = { readonly text: XaiResponsesModelId };
83
+ type XaiBatchModelIds = {
84
+ readonly text: XaiResponsesModelId;
85
+ readonly image: XaiImageModelId;
86
+ };
72
87
  type XaiBatchRequest = TextBatchV4Request<XaiResponsesModelId>;
88
+ type XaiImageBatchRequest = ImageBatchV4Request<XaiImageModelId>;
89
+
90
+ function assertSupportedBatchRequests(
91
+ requests: BatchV4StartOptions<XaiBatchModelIds>['requests'],
92
+ ) {
93
+ for (const request of requests) {
94
+ const requestType = request.type;
95
+ if (requestType !== 'text' && requestType !== 'image') {
96
+ throw new UnsupportedFunctionalityError({
97
+ functionality: `batch request type: ${requestType}`,
98
+ message: `The xAI Batch API does not support batch requests with type "${requestType}".`,
99
+ });
100
+ }
101
+ }
102
+ }
73
103
 
74
104
  type XaiBatchPreparedRequest = {
105
+ endpoint: string;
75
106
  body: unknown;
76
107
  warnings: SharedV4Warning[];
77
108
  };
78
109
 
110
+ const xaiBatchImageResponseSchema = z.object({
111
+ data: z.array(
112
+ z.object({
113
+ url: z.string().nullish(),
114
+ b64_json: z.string().nullish(),
115
+ revised_prompt: z.string().nullish(),
116
+ respect_moderation: z.boolean().nullish(),
117
+ }),
118
+ ),
119
+ usage: z.object({ cost_in_usd_ticks: z.number().nullish() }).nullish(),
120
+ });
121
+
79
122
  type XaiBatchResponseConversion =
80
123
  | { success: true; result: LanguageModelV4GenerateResult }
81
124
  | { success: false; error: BatchV4Error };
82
125
 
126
+ const xaiBatchResponseZodSchema = () =>
127
+ z.object({
128
+ batch_id: z.string(),
129
+ name: z.string().nullish(),
130
+ create_time: z.string().nullish(),
131
+ expire_time: z.string().nullish(),
132
+ cancel_time: z.string().nullish(),
133
+ cancel_by_xai_message: z.string().nullish(),
134
+ state: z
135
+ .object({
136
+ num_requests: z.number().nullish(),
137
+ num_pending: z.number().nullish(),
138
+ num_success: z.number().nullish(),
139
+ num_error: z.number().nullish(),
140
+ num_cancelled: z.number().nullish(),
141
+ })
142
+ .nullish(),
143
+ });
144
+
83
145
  const xaiBatchResponseSchema = lazySchema(() =>
84
- zodSchema(
85
- z.object({
86
- batch_id: z.string(),
87
- name: z.string().nullish(),
88
- create_time: z.string().nullish(),
89
- expire_time: z.string().nullish(),
90
- cancel_time: z.string().nullish(),
91
- cancel_by_xai_message: z.string().nullish(),
92
- state: z
93
- .object({
94
- num_requests: z.number().nullish(),
95
- num_pending: z.number().nullish(),
96
- num_success: z.number().nullish(),
97
- num_error: z.number().nullish(),
98
- num_cancelled: z.number().nullish(),
99
- })
100
- .nullish(),
101
- }),
102
- ),
146
+ zodSchema(xaiBatchResponseZodSchema()),
103
147
  );
104
148
 
105
149
  type XaiBatchResponse = InferSchema<typeof xaiBatchResponseSchema>;
@@ -116,6 +160,7 @@ const xaiBatchResultSchema = z.object({
116
160
  response: z
117
161
  .object({
118
162
  chat_get_completion: z.unknown().nullish(),
163
+ image_generation: z.unknown().nullish(),
119
164
  })
120
165
  .nullish(),
121
166
  error: xaiBatchErrorSchema.nullish(),
@@ -135,6 +180,15 @@ const xaiBatchResultsPageSchema = lazySchema(() =>
135
180
  ),
136
181
  );
137
182
 
183
+ const xaiBatchListResponseSchema = lazySchema(() =>
184
+ zodSchema(
185
+ z.object({
186
+ batches: z.array(xaiBatchResponseZodSchema()),
187
+ pagination_token: z.string().nullish(),
188
+ }),
189
+ ),
190
+ );
191
+
138
192
  export class XaiBatch implements BatchV4<XaiBatchModelIds> {
139
193
  readonly specificationVersion = 'v4' as const;
140
194
  readonly provider: string;
@@ -152,6 +206,7 @@ export class XaiBatch implements BatchV4<XaiBatchModelIds> {
152
206
  async doStartBatch(
153
207
  options: BatchV4StartOptions<XaiBatchModelIds>,
154
208
  ): Promise<BatchV4StartResult> {
209
+ assertSupportedBatchRequests(options.requests);
155
210
  const fileParts: string[] = [];
156
211
  const warnings: BatchV4StartResult['warnings'] =
157
212
  options.webhookUrl == null
@@ -180,7 +235,7 @@ export class XaiBatch implements BatchV4<XaiBatchModelIds> {
180
235
  JSON.stringify({
181
236
  custom_id: request.id,
182
237
  method: 'POST',
183
- url: xaiBatchEndpoint,
238
+ url: preparedRequest.endpoint,
184
239
  body: preparedRequest.body,
185
240
  }),
186
241
  '\n',
@@ -262,6 +317,58 @@ export class XaiBatch implements BatchV4<XaiBatchModelIds> {
262
317
  return convertXaiBatchStatus(await this.retrieveBatch(options));
263
318
  }
264
319
 
320
+ async doCancelBatch(
321
+ options: BatchV4OperationOptions,
322
+ ): Promise<BatchV4CancelResult> {
323
+ await postJsonToApi({
324
+ url: this.getUrl(
325
+ `/batches/${encodeURIComponent(options.batchId)}:cancel`,
326
+ ),
327
+ headers: combineHeaders(this.options.config.headers?.(), options.headers),
328
+ body: {},
329
+ failedResponseHandler: xaiFailedResponseHandler,
330
+ successfulResponseHandler: createJsonResponseHandler(
331
+ xaiBatchResponseSchema,
332
+ ),
333
+ abortSignal: options.abortSignal,
334
+ fetch: this.options.config.fetch,
335
+ });
336
+
337
+ return {};
338
+ }
339
+
340
+ async doListBatches(options: BatchV4ListOptions): Promise<BatchV4ListResult> {
341
+ const url = new URL(this.getUrl('/batches'));
342
+ if (options.limit != null) {
343
+ url.searchParams.set('limit', String(options.limit));
344
+ }
345
+ if (options.cursor != null) {
346
+ url.searchParams.set('pagination_token', options.cursor);
347
+ }
348
+
349
+ const { value: page } = await getFromApi({
350
+ url: url.toString(),
351
+ headers: combineHeaders(this.options.config.headers?.(), options.headers),
352
+ failedResponseHandler: xaiFailedResponseHandler,
353
+ successfulResponseHandler: createJsonResponseHandler(
354
+ xaiBatchListResponseSchema,
355
+ ),
356
+ abortSignal: options.abortSignal,
357
+ fetch: this.options.config.fetch,
358
+ validateUrl: false,
359
+ });
360
+
361
+ return {
362
+ batches: page.batches.map(batch => ({
363
+ batchId: batch.batch_id,
364
+ ...convertXaiBatchStatus(batch),
365
+ })),
366
+ ...(page.pagination_token != null
367
+ ? { nextCursor: page.pagination_token }
368
+ : {}),
369
+ };
370
+ }
371
+
265
372
  async doGetBatchResults(
266
373
  options: BatchV4OperationOptions,
267
374
  ): Promise<ReadableStream<BatchV4ItemResult>> {
@@ -327,7 +434,7 @@ export class XaiBatch implements BatchV4<XaiBatchModelIds> {
327
434
  });
328
435
 
329
436
  for (const result of page.results) {
330
- yield await this.convertBatchResult(result);
437
+ yield await this.convertBatchResult(result, options.abortSignal);
331
438
  }
332
439
 
333
440
  paginationToken = page.pagination_token ?? undefined;
@@ -336,7 +443,8 @@ export class XaiBatch implements BatchV4<XaiBatchModelIds> {
336
443
 
337
444
  private async convertBatchResult(
338
445
  result: XaiBatchResult,
339
- ): Promise<TextBatchV4ItemResult> {
446
+ abortSignal: AbortSignal | undefined,
447
+ ): Promise<TextBatchV4ItemResult | ImageBatchV4ItemResult> {
340
448
  const error = result.batch_result?.error;
341
449
  if (
342
450
  (result.error_message?.length ?? 0) > 0 ||
@@ -380,18 +488,149 @@ export class XaiBatch implements BatchV4<XaiBatchModelIds> {
380
488
  };
381
489
  }
382
490
 
491
+ if (response?.image_generation != null) {
492
+ const validation = await safeValidateTypes({
493
+ value: response.image_generation,
494
+ schema: zodSchema(xaiBatchImageResponseSchema),
495
+ });
496
+ if (!validation.success) {
497
+ return invalidXaiImageBatchResult(result.batch_request_id);
498
+ }
499
+ if (
500
+ validation.value.data.some(image => image.respect_moderation === false)
501
+ ) {
502
+ return {
503
+ type: 'image',
504
+ id: result.batch_request_id,
505
+ status: 'failed',
506
+ error: {
507
+ message:
508
+ 'Image generation was blocked due to a content policy violation.',
509
+ },
510
+ };
511
+ }
512
+ const imageResult = await this.convertImageBatchResponse(
513
+ validation.value,
514
+ abortSignal,
515
+ );
516
+ return {
517
+ type: 'image',
518
+ id: result.batch_request_id,
519
+ status: 'succeeded',
520
+ result: imageResult,
521
+ };
522
+ }
523
+
383
524
  return invalidXaiBatchResult(result.batch_request_id);
384
525
  }
385
526
 
386
527
  private async prepareRequest(
387
- request: XaiBatchRequest,
528
+ request: XaiBatchRequest | XaiImageBatchRequest,
388
529
  ): Promise<XaiBatchPreparedRequest> {
389
- const { args: body, warnings } =
390
- await XaiResponsesLanguageModel.prepareRequest({
391
- modelId: request.modelId,
392
- options: request.options,
530
+ if (request.type === 'text') {
531
+ const { args: body, warnings } =
532
+ await XaiResponsesLanguageModel.prepareRequest({
533
+ modelId: request.modelId,
534
+ options: request.options,
535
+ });
536
+ return { endpoint: xaiBatchEndpoint, body, warnings };
537
+ }
538
+
539
+ const { prompt, n, size, aspectRatio, seed, files, mask, providerOptions } =
540
+ request.options;
541
+ const warnings: SharedV4Warning[] = [];
542
+ if (size != null) {
543
+ warnings.push({
544
+ type: 'unsupported',
545
+ feature: 'size',
546
+ details:
547
+ 'This model does not support the `size` option. Use `aspectRatio` instead.',
393
548
  });
394
- return { body, warnings };
549
+ }
550
+ if (seed != null) warnings.push({ type: 'unsupported', feature: 'seed' });
551
+ if (mask != null) warnings.push({ type: 'unsupported', feature: 'mask' });
552
+
553
+ const xaiOptions = await parseProviderOptions({
554
+ provider: 'xai',
555
+ providerOptions,
556
+ schema: xaiImageModelOptions,
557
+ });
558
+ const imageUrls = (files ?? []).map(convertImageModelFileToDataUri);
559
+ const body: Record<string, unknown> = {
560
+ model: request.modelId,
561
+ prompt,
562
+ n,
563
+ response_format: 'b64_json',
564
+ };
565
+ if (aspectRatio != null) body.aspect_ratio = aspectRatio;
566
+ if (xaiOptions?.output_format != null)
567
+ body.output_format = xaiOptions.output_format;
568
+ if (xaiOptions?.sync_mode != null) body.sync_mode = xaiOptions.sync_mode;
569
+ if (xaiOptions?.aspect_ratio != null && aspectRatio == null)
570
+ body.aspect_ratio = xaiOptions.aspect_ratio;
571
+ if (xaiOptions?.resolution != null) body.resolution = xaiOptions.resolution;
572
+ if (xaiOptions?.quality != null) body.quality = xaiOptions.quality;
573
+ if (xaiOptions?.user != null) body.user = xaiOptions.user;
574
+ if (imageUrls.length === 1) {
575
+ body.image = { url: imageUrls[0], type: 'image_url' };
576
+ } else if (imageUrls.length > 1) {
577
+ body.images = imageUrls.map(url => ({ url, type: 'image_url' }));
578
+ }
579
+
580
+ return {
581
+ endpoint: files?.length ? '/v1/images/edits' : '/v1/images/generations',
582
+ body,
583
+ warnings,
584
+ };
585
+ }
586
+
587
+ private async convertImageBatchResponse(
588
+ response: z.infer<typeof xaiBatchImageResponseSchema>,
589
+ abortSignal: AbortSignal | undefined,
590
+ ): Promise<ImageModelV4Result> {
591
+ const hasAllBase64 = response.data.every(image => image.b64_json != null);
592
+ const images = hasAllBase64
593
+ ? response.data.map(image => image.b64_json!)
594
+ : await Promise.all(
595
+ response.data.map(async image => {
596
+ if (image.b64_json != null) {
597
+ return convertBase64ToUint8Array(image.b64_json);
598
+ }
599
+ if (image.url == null) {
600
+ throw new InvalidArgumentError({
601
+ argument: 'batchResult',
602
+ message: 'xAI returned an image without data or a URL.',
603
+ });
604
+ }
605
+ const { value } = await getFromApi({
606
+ url: image.url,
607
+ validateUrl: true,
608
+ trustedOrigin: this.options.config.baseURL,
609
+ abortSignal,
610
+ failedResponseHandler: xaiFailedResponseHandler,
611
+ successfulResponseHandler: createBinaryResponseHandler(),
612
+ fetch: this.options.config.fetch,
613
+ });
614
+ return value;
615
+ }),
616
+ );
617
+ return {
618
+ images,
619
+ warnings: [],
620
+ response: { timestamp: new Date(), modelId: '', headers: undefined },
621
+ providerMetadata: {
622
+ xai: {
623
+ images: response.data.map(item =>
624
+ item.revised_prompt != null
625
+ ? { revisedPrompt: item.revised_prompt }
626
+ : {},
627
+ ),
628
+ ...(response.usage?.cost_in_usd_ticks != null
629
+ ? { costInUsdTicks: response.usage.cost_in_usd_ticks }
630
+ : {}),
631
+ },
632
+ },
633
+ };
395
634
  }
396
635
 
397
636
  private getUrl(path: string) {
@@ -487,6 +726,18 @@ function invalidXaiBatchResult(id: string): TextBatchV4ItemResult {
487
726
  };
488
727
  }
489
728
 
729
+ function invalidXaiImageBatchResult(id: string): ImageBatchV4ItemResult {
730
+ return {
731
+ type: 'image',
732
+ id,
733
+ status: 'failed',
734
+ error: {
735
+ message: 'xAI returned an invalid image batch result.',
736
+ code: 'invalid_response',
737
+ },
738
+ };
739
+ }
740
+
490
741
  function convertXaiChatBatchResponse(
491
742
  response: XaiChatResponse,
492
743
  ): XaiBatchResponseConversion {
@@ -590,7 +590,7 @@ export class XaiChatLanguageModel implements LanguageModelV4 {
590
590
  }
591
591
 
592
592
  // process tool calls
593
- if (delta.tool_calls != null) {
593
+ if (delta.tool_calls != null && delta.tool_calls.length > 0) {
594
594
  // end active reasoning block before tool calls start
595
595
  if (
596
596
  activeReasoningBlockId != null &&
@@ -56,7 +56,10 @@ export interface XaiProvider extends ProviderV4 {
56
56
  /**
57
57
  * Returns a BatchV4 interface for processing batches with xAI.
58
58
  */
59
- experimental_batch(): BatchV4<{ text: XaiResponsesModelId }>;
59
+ experimental_batch(): BatchV4<{
60
+ text: XaiResponsesModelId;
61
+ image: XaiImageModelId;
62
+ }>;
60
63
 
61
64
  /**
62
65
  * Creates an Xai image model for image generation.