@ai-sdk/xai 4.0.59 → 5.0.1

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.
@@ -1,778 +0,0 @@
1
- import {
2
- APICallError,
3
- type LanguageModelV4,
4
- type LanguageModelV4CallOptions,
5
- type LanguageModelV4Content,
6
- type LanguageModelV4FinishReason,
7
- type LanguageModelV4GenerateResult,
8
- type LanguageModelV4StreamPart,
9
- type LanguageModelV4StreamResult,
10
- type LanguageModelV4Usage,
11
- type SharedV4Warning,
12
- } from '@ai-sdk/provider';
13
- import {
14
- combineHeaders,
15
- createEventSourceResponseHandler,
16
- createJsonResponseHandler,
17
- extractResponseHeaders,
18
- isCustomReasoning,
19
- mapReasoningToProviderEffort,
20
- parseProviderOptions,
21
- postJsonToApi,
22
- safeParseJSON,
23
- serializeModelOptions,
24
- WORKFLOW_SERIALIZE,
25
- WORKFLOW_DESERIALIZE,
26
- type FetchFunction,
27
- type ParseResult,
28
- } from '@ai-sdk/provider-utils';
29
- import { z } from 'zod/v4';
30
- import { convertToXaiChatMessages } from './convert-to-xai-chat-messages';
31
- import { convertXaiChatUsage } from './convert-xai-chat-usage';
32
- import { getResponseMetadata } from './get-response-metadata';
33
- import { mapXaiFinishReason } from './map-xai-finish-reason';
34
- import { supportsReasoningEffort } from './supports-reasoning-effort';
35
- import {
36
- xaiLanguageModelChatOptions,
37
- type XaiChatModelId,
38
- } from './xai-chat-language-model-options';
39
- import { xaiFailedResponseHandler } from './xai-error';
40
- import { prepareTools } from './xai-prepare-tools';
41
-
42
- type XaiChatConfig = {
43
- provider: string;
44
- baseURL: string | undefined;
45
- headers?: () => Record<string, string | undefined>;
46
- generateId: () => string;
47
- fetch?: FetchFunction;
48
- };
49
-
50
- export class XaiChatLanguageModel implements LanguageModelV4 {
51
- readonly specificationVersion = 'v4';
52
-
53
- readonly modelId: XaiChatModelId;
54
-
55
- private readonly config: XaiChatConfig;
56
-
57
- static [WORKFLOW_SERIALIZE](model: XaiChatLanguageModel) {
58
- return serializeModelOptions({
59
- modelId: model.modelId,
60
- config: model.config,
61
- });
62
- }
63
-
64
- static [WORKFLOW_DESERIALIZE](options: {
65
- modelId: XaiChatModelId;
66
- config: XaiChatConfig;
67
- }) {
68
- return new XaiChatLanguageModel(options.modelId, options.config);
69
- }
70
-
71
- constructor(modelId: XaiChatModelId, config: XaiChatConfig) {
72
- this.modelId = modelId;
73
- this.config = config;
74
- }
75
-
76
- get provider(): string {
77
- return this.config.provider;
78
- }
79
-
80
- readonly supportedUrls: Record<string, RegExp[]> = {
81
- 'image/*': [/^https?:\/\/.*$/],
82
- };
83
-
84
- private async getArgs({
85
- prompt,
86
- maxOutputTokens,
87
- temperature,
88
- topP,
89
- topK,
90
- frequencyPenalty,
91
- presencePenalty,
92
- stopSequences,
93
- seed,
94
- reasoning,
95
- responseFormat,
96
- providerOptions,
97
- tools,
98
- toolChoice,
99
- }: LanguageModelV4CallOptions) {
100
- const warnings: SharedV4Warning[] = [];
101
-
102
- // parse xai-specific provider options
103
- const options =
104
- (await parseProviderOptions({
105
- provider: 'xai',
106
- providerOptions,
107
- schema: xaiLanguageModelChatOptions,
108
- })) ?? {};
109
-
110
- // check for unsupported parameters
111
- if (topK != null) {
112
- warnings.push({ type: 'unsupported', feature: 'topK' });
113
- }
114
-
115
- if (frequencyPenalty != null) {
116
- warnings.push({ type: 'unsupported', feature: 'frequencyPenalty' });
117
- }
118
-
119
- if (presencePenalty != null) {
120
- warnings.push({ type: 'unsupported', feature: 'presencePenalty' });
121
- }
122
-
123
- if (stopSequences != null) {
124
- warnings.push({ type: 'unsupported', feature: 'stopSequences' });
125
- }
126
-
127
- // convert ai sdk messages to xai format
128
- const { messages, warnings: messageWarnings } =
129
- await convertToXaiChatMessages(prompt);
130
- warnings.push(...messageWarnings);
131
-
132
- // prepare tools for xai
133
- const {
134
- tools: xaiTools,
135
- toolChoice: xaiToolChoice,
136
- toolWarnings,
137
- } = prepareTools({
138
- tools,
139
- toolChoice,
140
- });
141
- warnings.push(...toolWarnings);
142
-
143
- let reasoningEffort = options.reasoningEffort;
144
- if (reasoningEffort == null && isCustomReasoning(reasoning)) {
145
- if (!supportsReasoningEffort(this.modelId)) {
146
- warnings.push({
147
- type: 'unsupported',
148
- feature: 'reasoning',
149
- details: `reasoning "${reasoning}" is not supported by this model.`,
150
- });
151
- } else if (reasoning === 'none') {
152
- reasoningEffort = 'none';
153
- } else {
154
- reasoningEffort = mapReasoningToProviderEffort({
155
- reasoning,
156
- effortMap: {
157
- minimal: 'low',
158
- low: 'low',
159
- medium: 'medium',
160
- high: 'high',
161
- xhigh: this.modelId === 'grok-4.6' ? 'xhigh' : 'high',
162
- },
163
- warnings,
164
- });
165
- }
166
- }
167
-
168
- const baseArgs = {
169
- // model id
170
- model: this.modelId,
171
-
172
- // standard generation settings
173
- logprobs:
174
- options.logprobs === true || options.topLogprobs != null
175
- ? true
176
- : undefined,
177
- top_logprobs: options.topLogprobs,
178
- max_completion_tokens: maxOutputTokens,
179
- temperature,
180
- top_p: topP,
181
- seed,
182
- reasoning_effort: reasoningEffort,
183
-
184
- // scheduling priority
185
- service_tier: options.serviceTier,
186
-
187
- // parallel function calling
188
- parallel_function_calling: options.parallel_function_calling,
189
-
190
- // response format
191
- response_format:
192
- responseFormat?.type === 'json'
193
- ? responseFormat.schema != null
194
- ? {
195
- type: 'json_schema',
196
- json_schema: {
197
- name: responseFormat.name ?? 'response',
198
- schema: responseFormat.schema,
199
- strict: true,
200
- },
201
- }
202
- : { type: 'json_object' }
203
- : undefined,
204
-
205
- // search parameters
206
- search_parameters: options.searchParameters
207
- ? {
208
- mode: options.searchParameters.mode,
209
- return_citations: options.searchParameters.returnCitations,
210
- from_date: options.searchParameters.fromDate,
211
- to_date: options.searchParameters.toDate,
212
- max_search_results: options.searchParameters.maxSearchResults,
213
- sources: options.searchParameters.sources?.map(source => ({
214
- type: source.type,
215
- ...(source.type === 'web' && {
216
- country: source.country,
217
- excluded_websites: source.excludedWebsites,
218
- allowed_websites: source.allowedWebsites,
219
- safe_search: source.safeSearch,
220
- }),
221
- ...(source.type === 'x' && {
222
- excluded_x_handles: source.excludedXHandles,
223
- included_x_handles: source.includedXHandles ?? source.xHandles,
224
- post_favorite_count: source.postFavoriteCount,
225
- post_view_count: source.postViewCount,
226
- }),
227
- ...(source.type === 'news' && {
228
- country: source.country,
229
- excluded_websites: source.excludedWebsites,
230
- safe_search: source.safeSearch,
231
- }),
232
- ...(source.type === 'rss' && {
233
- links: source.links,
234
- }),
235
- })),
236
- }
237
- : undefined,
238
-
239
- // messages in xai format
240
- messages,
241
-
242
- // tools in xai format
243
- tools: xaiTools,
244
- tool_choice: xaiToolChoice,
245
- };
246
-
247
- return {
248
- args: baseArgs,
249
- warnings,
250
- };
251
- }
252
-
253
- async doGenerate(
254
- options: LanguageModelV4CallOptions,
255
- ): Promise<LanguageModelV4GenerateResult> {
256
- const { args: body, warnings } = await this.getArgs(options);
257
-
258
- const url = `${this.config.baseURL ?? 'https://api.x.ai/v1'}/chat/completions`;
259
-
260
- const {
261
- responseHeaders,
262
- value: response,
263
- rawValue: rawResponse,
264
- } = await postJsonToApi({
265
- url,
266
- headers: combineHeaders(this.config.headers?.(), options.headers),
267
- body,
268
- failedResponseHandler: xaiFailedResponseHandler,
269
- successfulResponseHandler: createJsonResponseHandler(
270
- xaiChatResponseSchema,
271
- ),
272
- abortSignal: options.abortSignal,
273
- fetch: this.config.fetch,
274
- });
275
-
276
- if (response.error != null) {
277
- throw new APICallError({
278
- message: response.error,
279
- url,
280
- requestBodyValues: body,
281
- statusCode: 200,
282
- responseHeaders,
283
- responseBody: JSON.stringify(rawResponse),
284
- isRetryable: response.code === 'The service is currently unavailable',
285
- });
286
- }
287
-
288
- const choice = response.choices![0];
289
- const content: Array<LanguageModelV4Content> = [];
290
-
291
- // extract text content
292
- if (choice.message.content != null && choice.message.content.length > 0) {
293
- let text = choice.message.content;
294
-
295
- // skip if this content duplicates the last assistant message
296
- const lastMessage = body.messages[body.messages.length - 1];
297
- if (lastMessage?.role === 'assistant' && text === lastMessage.content) {
298
- text = '';
299
- }
300
-
301
- if (text.length > 0) {
302
- content.push({ type: 'text', text });
303
- }
304
- }
305
-
306
- // extract reasoning content
307
- if (
308
- choice.message.reasoning_content != null &&
309
- choice.message.reasoning_content.length > 0
310
- ) {
311
- content.push({
312
- type: 'reasoning',
313
- text: choice.message.reasoning_content,
314
- });
315
- }
316
-
317
- // extract tool calls
318
- if (choice.message.tool_calls != null) {
319
- for (const toolCall of choice.message.tool_calls) {
320
- content.push({
321
- type: 'tool-call',
322
- toolCallId: toolCall.id,
323
- toolName: toolCall.function.name,
324
- input: toolCall.function.arguments,
325
- });
326
- }
327
- }
328
-
329
- // extract citations
330
- if (response.citations != null) {
331
- for (const url of response.citations) {
332
- content.push({
333
- type: 'source',
334
- sourceType: 'url',
335
- id: this.config.generateId(),
336
- url,
337
- });
338
- }
339
- }
340
-
341
- return {
342
- content,
343
- finishReason: {
344
- unified: mapXaiFinishReason(choice.finish_reason),
345
- raw: choice.finish_reason ?? undefined,
346
- },
347
- usage: response.usage
348
- ? convertXaiChatUsage(response.usage)
349
- : {
350
- inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
351
- outputTokens: { total: 0, text: 0, reasoning: 0 },
352
- },
353
- ...(response.service_tier != null && {
354
- providerMetadata: {
355
- xai: { serviceTier: response.service_tier },
356
- },
357
- }),
358
- request: { body },
359
- response: {
360
- ...getResponseMetadata(response),
361
- headers: responseHeaders,
362
- body: rawResponse,
363
- },
364
- warnings,
365
- };
366
- }
367
-
368
- async doStream(
369
- options: LanguageModelV4CallOptions,
370
- ): Promise<LanguageModelV4StreamResult> {
371
- const { args, warnings } = await this.getArgs(options);
372
- const body = {
373
- ...args,
374
- stream: true,
375
- stream_options: {
376
- include_usage: true,
377
- },
378
- };
379
-
380
- const url = `${this.config.baseURL ?? 'https://api.x.ai/v1'}/chat/completions`;
381
-
382
- const { responseHeaders, value: response } = await postJsonToApi({
383
- url,
384
- headers: combineHeaders(this.config.headers?.(), options.headers),
385
- body,
386
- failedResponseHandler: xaiFailedResponseHandler,
387
- successfulResponseHandler: async ({ response }) => {
388
- const responseHeaders = extractResponseHeaders(response);
389
- const contentType = response.headers.get('content-type');
390
-
391
- if (contentType?.includes('application/json')) {
392
- const responseBody = await response.text();
393
- const parsedError = await safeParseJSON({
394
- text: responseBody,
395
- schema: xaiStreamErrorSchema,
396
- });
397
-
398
- if (parsedError.success) {
399
- throw new APICallError({
400
- message: parsedError.value.error,
401
- url,
402
- requestBodyValues: body,
403
- statusCode: 200,
404
- responseHeaders,
405
- responseBody,
406
- isRetryable:
407
- parsedError.value.code ===
408
- 'The service is currently unavailable',
409
- });
410
- }
411
-
412
- throw new APICallError({
413
- message: 'Invalid JSON response',
414
- url,
415
- requestBodyValues: body,
416
- statusCode: 200,
417
- responseHeaders,
418
- responseBody,
419
- });
420
- }
421
-
422
- return createEventSourceResponseHandler(xaiChatChunkSchema)({
423
- response,
424
- url,
425
- requestBodyValues: body,
426
- });
427
- },
428
- abortSignal: options.abortSignal,
429
- fetch: this.config.fetch,
430
- });
431
-
432
- let finishReason: LanguageModelV4FinishReason = {
433
- unified: 'other',
434
- raw: undefined,
435
- };
436
- let usage: LanguageModelV4Usage | undefined = undefined;
437
- let serviceTier: string | undefined = undefined;
438
- let isFirstChunk = true;
439
- const contentBlocks: Record<
440
- string,
441
- { type: 'text' | 'reasoning'; ended: boolean }
442
- > = {};
443
- const lastReasoningDeltas: Record<string, string> = {};
444
- let activeReasoningBlockId: string | undefined = undefined;
445
-
446
- const self = this;
447
-
448
- return {
449
- stream: response.pipeThrough(
450
- new TransformStream<
451
- ParseResult<z.infer<typeof xaiChatChunkSchema>>,
452
- LanguageModelV4StreamPart
453
- >({
454
- start(controller) {
455
- controller.enqueue({ type: 'stream-start', warnings });
456
- },
457
-
458
- transform(chunk, controller) {
459
- // Emit raw chunk if requested (before anything else)
460
- if (options.includeRawChunks) {
461
- controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
462
- }
463
-
464
- if (!chunk.success) {
465
- controller.enqueue({ type: 'error', error: chunk.error });
466
- return;
467
- }
468
-
469
- const value = chunk.value;
470
-
471
- // emit response metadata on first chunk
472
- if (isFirstChunk) {
473
- controller.enqueue({
474
- type: 'response-metadata',
475
- ...getResponseMetadata(value),
476
- });
477
- isFirstChunk = false;
478
- }
479
-
480
- // emit citations if present (they come in the last chunk according to docs)
481
- if (value.citations != null) {
482
- for (const url of value.citations) {
483
- controller.enqueue({
484
- type: 'source',
485
- sourceType: 'url',
486
- id: self.config.generateId(),
487
- url,
488
- });
489
- }
490
- }
491
-
492
- // update usage if present
493
- if (value.usage != null) {
494
- usage = convertXaiChatUsage(value.usage);
495
- }
496
-
497
- // the applied tier is repeated on every chunk; keep the latest
498
- if (value.service_tier != null) {
499
- serviceTier = value.service_tier;
500
- }
501
-
502
- const choice = value.choices[0];
503
-
504
- // update finish reason if present
505
- if (choice?.finish_reason != null) {
506
- finishReason = {
507
- unified: mapXaiFinishReason(choice.finish_reason),
508
- raw: choice.finish_reason,
509
- };
510
- }
511
-
512
- // exit if no delta to process
513
- if (choice?.delta == null) {
514
- return;
515
- }
516
-
517
- const delta = choice.delta;
518
- const choiceIndex = choice.index;
519
-
520
- // process text content
521
- if (delta.content != null && delta.content.length > 0) {
522
- const textContent = delta.content;
523
-
524
- // end active reasoning block when text content arrives
525
- if (
526
- activeReasoningBlockId != null &&
527
- !contentBlocks[activeReasoningBlockId].ended
528
- ) {
529
- controller.enqueue({
530
- type: 'reasoning-end',
531
- id: activeReasoningBlockId,
532
- });
533
- contentBlocks[activeReasoningBlockId].ended = true;
534
- activeReasoningBlockId = undefined;
535
- }
536
-
537
- // skip if this content duplicates the last assistant message
538
- const lastMessage = body.messages[body.messages.length - 1];
539
- if (
540
- lastMessage?.role === 'assistant' &&
541
- textContent === lastMessage.content
542
- ) {
543
- return;
544
- }
545
-
546
- const blockId = `text-${value.id || choiceIndex}`;
547
-
548
- if (contentBlocks[blockId] == null) {
549
- contentBlocks[blockId] = { type: 'text', ended: false };
550
- controller.enqueue({
551
- type: 'text-start',
552
- id: blockId,
553
- });
554
- }
555
-
556
- controller.enqueue({
557
- type: 'text-delta',
558
- id: blockId,
559
- delta: textContent,
560
- });
561
- }
562
-
563
- // process reasoning content
564
- if (
565
- delta.reasoning_content != null &&
566
- delta.reasoning_content.length > 0
567
- ) {
568
- const blockId = `reasoning-${value.id || choiceIndex}`;
569
-
570
- // skip if this reasoning content duplicates the last delta
571
- if (lastReasoningDeltas[blockId] === delta.reasoning_content) {
572
- return;
573
- }
574
- lastReasoningDeltas[blockId] = delta.reasoning_content;
575
-
576
- if (contentBlocks[blockId] == null) {
577
- contentBlocks[blockId] = { type: 'reasoning', ended: false };
578
- activeReasoningBlockId = blockId;
579
- controller.enqueue({
580
- type: 'reasoning-start',
581
- id: blockId,
582
- });
583
- }
584
-
585
- controller.enqueue({
586
- type: 'reasoning-delta',
587
- id: blockId,
588
- delta: delta.reasoning_content,
589
- });
590
- }
591
-
592
- // process tool calls
593
- if (delta.tool_calls != null && delta.tool_calls.length > 0) {
594
- // end active reasoning block before tool calls start
595
- if (
596
- activeReasoningBlockId != null &&
597
- !contentBlocks[activeReasoningBlockId].ended
598
- ) {
599
- controller.enqueue({
600
- type: 'reasoning-end',
601
- id: activeReasoningBlockId,
602
- });
603
- contentBlocks[activeReasoningBlockId].ended = true;
604
- activeReasoningBlockId = undefined;
605
- }
606
-
607
- for (const toolCall of delta.tool_calls) {
608
- // xai tool calls come in one piece (like mistral)
609
- const toolCallId = toolCall.id;
610
-
611
- controller.enqueue({
612
- type: 'tool-input-start',
613
- id: toolCallId,
614
- toolName: toolCall.function.name,
615
- });
616
-
617
- controller.enqueue({
618
- type: 'tool-input-delta',
619
- id: toolCallId,
620
- delta: toolCall.function.arguments,
621
- });
622
-
623
- controller.enqueue({
624
- type: 'tool-input-end',
625
- id: toolCallId,
626
- });
627
-
628
- controller.enqueue({
629
- type: 'tool-call',
630
- toolCallId,
631
- toolName: toolCall.function.name,
632
- input: toolCall.function.arguments,
633
- });
634
- }
635
- }
636
- },
637
-
638
- flush(controller) {
639
- // end any blocks that haven't been ended yet
640
- for (const [blockId, block] of Object.entries(contentBlocks)) {
641
- if (!block.ended) {
642
- controller.enqueue({
643
- type: block.type === 'text' ? 'text-end' : 'reasoning-end',
644
- id: blockId,
645
- });
646
- }
647
- }
648
-
649
- controller.enqueue({
650
- type: 'finish',
651
- finishReason,
652
- usage: usage ?? {
653
- inputTokens: {
654
- total: 0,
655
- noCache: 0,
656
- cacheRead: 0,
657
- cacheWrite: 0,
658
- },
659
- outputTokens: { total: 0, text: 0, reasoning: 0 },
660
- },
661
- ...(serviceTier != null && {
662
- providerMetadata: { xai: { serviceTier } },
663
- }),
664
- });
665
- },
666
- }),
667
- ),
668
- request: { body },
669
- response: { headers: responseHeaders },
670
- };
671
- }
672
- }
673
-
674
- // XAI API Response Schemas
675
- const xaiUsageSchema = z
676
- .object({
677
- prompt_tokens: z.number(),
678
- completion_tokens: z.number(),
679
- total_tokens: z.number(),
680
- cost_in_usd_ticks: z.number().nullish(),
681
- prompt_tokens_details: z
682
- .object({
683
- text_tokens: z.number().nullish(),
684
- audio_tokens: z.number().nullish(),
685
- image_tokens: z.number().nullish(),
686
- cached_tokens: z.number().nullish(),
687
- })
688
- .catchall(z.json())
689
- .nullish(),
690
- completion_tokens_details: z
691
- .object({
692
- reasoning_tokens: z.number().nullish(),
693
- audio_tokens: z.number().nullish(),
694
- accepted_prediction_tokens: z.number().nullish(),
695
- rejected_prediction_tokens: z.number().nullish(),
696
- })
697
- .catchall(z.json())
698
- .nullish(),
699
- })
700
- .catchall(z.json());
701
-
702
- export type XaiChatUsage = z.infer<typeof xaiUsageSchema>;
703
-
704
- export const xaiChatResponseSchema = z.object({
705
- id: z.string().nullish(),
706
- created: z.number().nullish(),
707
- model: z.string().nullish(),
708
- choices: z
709
- .array(
710
- z.object({
711
- message: z.object({
712
- role: z.enum(['assistant', 'tool']),
713
- content: z.string().nullish(),
714
- reasoning_content: z.string().nullish(),
715
- tool_calls: z
716
- .array(
717
- z.object({
718
- id: z.string(),
719
- type: z.literal('function'),
720
- function: z.object({
721
- name: z.string(),
722
- arguments: z.string(),
723
- }),
724
- }),
725
- )
726
- .nullish(),
727
- }),
728
- index: z.number(),
729
- finish_reason: z.string().nullish(),
730
- }),
731
- )
732
- .nullish(),
733
- object: z.literal('chat.completion').nullish(),
734
- usage: xaiUsageSchema.nullish(),
735
- citations: z.array(z.string().url()).nullish(),
736
- service_tier: z.string().nullish(),
737
- code: z.string().nullish(),
738
- error: z.string().nullish(),
739
- });
740
-
741
- export type XaiChatResponse = z.infer<typeof xaiChatResponseSchema>;
742
-
743
- const xaiChatChunkSchema = z.object({
744
- id: z.string().nullish(),
745
- created: z.number().nullish(),
746
- model: z.string().nullish(),
747
- choices: z.array(
748
- z.object({
749
- delta: z.object({
750
- role: z.enum(['assistant']).optional(),
751
- content: z.string().nullish(),
752
- reasoning_content: z.string().nullish(),
753
- tool_calls: z
754
- .array(
755
- z.object({
756
- id: z.string(),
757
- type: z.literal('function'),
758
- function: z.object({
759
- name: z.string(),
760
- arguments: z.string(),
761
- }),
762
- }),
763
- )
764
- .nullish(),
765
- }),
766
- finish_reason: z.string().nullish(),
767
- index: z.number(),
768
- }),
769
- ),
770
- usage: xaiUsageSchema.nullish(),
771
- citations: z.array(z.string().url()).nullish(),
772
- service_tier: z.string().nullish(),
773
- });
774
-
775
- const xaiStreamErrorSchema = z.object({
776
- code: z.string(),
777
- error: z.string(),
778
- });