@ai-sdk/xai 4.0.38 → 4.0.40

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/docs/01-xai.mdx CHANGED
@@ -59,10 +59,10 @@ You can use the following optional settings to customize the xAI provider instan
59
59
  ## Language Models
60
60
 
61
61
  You can create [xAI models](https://console.x.ai) using a provider instance. The
62
- first argument is the model id, e.g. `grok-4.20-non-reasoning`.
62
+ first argument is the model id, e.g. `grok-4.6`.
63
63
 
64
64
  ```ts
65
- const model = xai('grok-4.20-non-reasoning');
65
+ const model = xai('grok-4.6');
66
66
  ```
67
67
 
68
68
  <Note>
@@ -81,7 +81,7 @@ import { xai } from '@ai-sdk/xai';
81
81
  import { generateText } from 'ai';
82
82
 
83
83
  const { text } = await generateText({
84
- model: xai('grok-4.20-non-reasoning'),
84
+ model: xai('grok-4.6'),
85
85
  prompt: 'Write a vegetarian lasagna recipe for 4 people.',
86
86
  });
87
87
  ```
@@ -199,7 +199,7 @@ calling pattern.
199
199
  The xAI Responses API is the default when using `xai(modelId)` (since AI SDK 7). You can also use `xai.responses(modelId)` explicitly. This enables the model to autonomously orchestrate tool calls and research on xAI's servers.
200
200
 
201
201
  ```ts
202
- const model = xai.responses('grok-4.20-non-reasoning');
202
+ const model = xai.responses('grok-4.6');
203
203
  ```
204
204
 
205
205
  The Responses API provides server-side tools that the model can autonomously execute during its reasoning process:
@@ -221,7 +221,7 @@ import { xai } from '@ai-sdk/xai';
221
221
  import { generateText } from 'ai';
222
222
 
223
223
  const { text } = await generateText({
224
- model: xai.responses('grok-3'),
224
+ model: xai.responses('grok-4.6'),
225
225
  messages: [
226
226
  {
227
227
  role: 'user',
@@ -246,7 +246,7 @@ import { xai } from '@ai-sdk/xai';
246
246
  import { generateText } from 'ai';
247
247
 
248
248
  const { text } = await generateText({
249
- model: xai('grok-4.3'),
249
+ model: xai('grok-4.6'),
250
250
  messages: [
251
251
  {
252
252
  role: 'user',
@@ -283,7 +283,7 @@ import { xai } from '@ai-sdk/xai';
283
283
  import { generateText } from 'ai';
284
284
 
285
285
  const { text, sources } = await generateText({
286
- model: xai.responses('grok-4.20-non-reasoning'),
286
+ model: xai.responses('grok-4.6'),
287
287
  prompt: 'What are the latest developments in AI?',
288
288
  tools: {
289
289
  web_search: xai.tools.webSearch({
@@ -321,7 +321,7 @@ The X search tool enables searching X (Twitter) for posts, with filtering by han
321
321
 
322
322
  ```ts
323
323
  const { text, sources } = await generateText({
324
- model: xai.responses('grok-4.20-non-reasoning'),
324
+ model: xai.responses('grok-4.6'),
325
325
  prompt: 'What are people saying about AI on X this week?',
326
326
  tools: {
327
327
  x_search: xai.tools.xSearch({
@@ -367,7 +367,7 @@ The code execution tool enables the model to write and execute Python code for c
367
367
 
368
368
  ```ts
369
369
  const { text } = await generateText({
370
- model: xai.responses('grok-4.20-non-reasoning'),
370
+ model: xai.responses('grok-4.6'),
371
371
  prompt:
372
372
  'Calculate the compound interest for $10,000 at 5% annually for 10 years',
373
373
  tools: {
@@ -382,7 +382,7 @@ The view image tool enables the model to view and analyze images:
382
382
 
383
383
  ```ts
384
384
  const { text } = await generateText({
385
- model: xai.responses('grok-4.20-non-reasoning'),
385
+ model: xai.responses('grok-4.6'),
386
386
  prompt: 'Describe what you see in the image',
387
387
  tools: {
388
388
  view_image: xai.tools.viewImage(),
@@ -396,7 +396,7 @@ The view X video tool enables the model to view and analyze videos from X (Twitt
396
396
 
397
397
  ```ts
398
398
  const { text } = await generateText({
399
- model: xai.responses('grok-4.20-non-reasoning'),
399
+ model: xai.responses('grok-4.6'),
400
400
  prompt: 'Summarize the content of this X video',
401
401
  tools: {
402
402
  view_x_video: xai.tools.viewXVideo(),
@@ -413,7 +413,7 @@ import { xai } from '@ai-sdk/xai';
413
413
  import { generateText } from 'ai';
414
414
 
415
415
  const result = await generateText({
416
- model: xai.responses('grok-4.5'),
416
+ model: xai.responses('grok-4.6'),
417
417
  prompt:
418
418
  'Generate an image of a corgi surfing a big wave, in the style of a Japanese woodblock print',
419
419
  tools: {
@@ -451,7 +451,7 @@ The MCP server tool enables the model to connect to remote [Model Context Protoc
451
451
 
452
452
  ```ts
453
453
  const { text } = await generateText({
454
- model: xai.responses('grok-4.20-non-reasoning'),
454
+ model: xai.responses('grok-4.6'),
455
455
  prompt: 'Use the weather tool to check conditions in San Francisco',
456
456
  tools: {
457
457
  weather_server: xai.tools.mcpServer({
@@ -499,7 +499,7 @@ import { xai, type XaiLanguageModelResponsesOptions } from '@ai-sdk/xai';
499
499
  import { streamText } from 'ai';
500
500
 
501
501
  const result = streamText({
502
- model: xai.responses('grok-4.20-reasoning'),
502
+ model: xai.responses('grok-4.6'),
503
503
  prompt: 'What documents do you have access to?',
504
504
  tools: {
505
505
  file_search: xai.tools.fileSearch({
@@ -546,7 +546,7 @@ import { xai } from '@ai-sdk/xai';
546
546
  import { streamText } from 'ai';
547
547
 
548
548
  const { stream } = streamText({
549
- model: xai.responses('grok-4.20-non-reasoning'),
549
+ model: xai.responses('grok-4.6'),
550
550
  prompt: 'Research AI safety developments and calculate risk metrics',
551
551
  tools: {
552
552
  web_search: xai.tools.webSearch(),
@@ -580,7 +580,7 @@ import { xai, type XaiLanguageModelResponsesOptions } from '@ai-sdk/xai';
580
580
  import { generateText } from 'ai';
581
581
 
582
582
  const result = await generateText({
583
- model: xai.responses('grok-4.5'),
583
+ model: xai.responses('grok-4.6'),
584
584
  providerOptions: {
585
585
  xai: {
586
586
  reasoningEffort: 'high',
@@ -746,6 +746,45 @@ const result = await generateSpeech({
746
746
 
747
747
  Whether to normalize written-form input text before synthesizing speech.
748
748
 
749
+ - **withTimestamps** _boolean_
750
+
751
+ Return character-level timing metadata alongside the audio. The timing data
752
+ and total duration are exposed via `providerMetadata.xai` (see below).
753
+
754
+ - **replace** _Record&lt;string, string&gt;_
755
+
756
+ Map of phrases to spoken substitutions applied before synthesis. Values may
757
+ be respellings (`{ 'Acme Mobile': 'Acme Mobull' }`) or IPA phonetics
758
+ (`{ nginx: '/ˈɛndʒɪn ˈɛks/' }`).
759
+
760
+ ### Provider Metadata
761
+
762
+ xAI speech results include provider-specific metadata under
763
+ `providerMetadata.xai`:
764
+
765
+ - **traceId** _string_ — the xAI trace ID for the request, useful for
766
+ debugging with xAI support.
767
+ - **duration** _number_ — total audio duration in seconds (only with
768
+ `withTimestamps`).
769
+ - **contentType** _string_ — MIME type of the decoded audio, e.g.
770
+ `'audio/mpeg'` (only with `withTimestamps`).
771
+ - **audioTimestamps** `{ graphChars: string[]; graphTimes: [number, number][] }` —
772
+ per-character alignment data (only with `withTimestamps`). `graphChars[i]`
773
+ is the character spoken during the `[start, end]` interval
774
+ `graphTimes[i]`, in seconds.
775
+
776
+ ```ts
777
+ const result = await generateSpeech({
778
+ model: xai.speech(),
779
+ text: 'Hello world.',
780
+ providerOptions: {
781
+ xai: { withTimestamps: true } satisfies XaiSpeechModelOptions,
782
+ },
783
+ });
784
+
785
+ const { traceId, duration, audioTimestamps } = result.providerMetadata.xai;
786
+ ```
787
+
749
788
  ### Model Capabilities
750
789
 
751
790
  | Model | Language | Speed | Output Formats |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/xai",
3
- "version": "4.0.38",
3
+ "version": "4.0.40",
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-utils": "5.0.27",
33
- "@ai-sdk/provider": "4.0.7"
32
+ "@ai-sdk/provider": "4.0.7",
33
+ "@ai-sdk/provider-utils": "5.0.27"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "22.19.19",
package/src/xai-error.ts CHANGED
@@ -15,15 +15,25 @@ const responsesErrorSchema = z.object({
15
15
  error: z.string(),
16
16
  });
17
17
 
18
+ // Text to Speech error shape, e.g. {"error":"speed must be between 0.7 and 1.5"}
19
+ const speechErrorSchema = z.object({
20
+ error: z.string(),
21
+ });
22
+
18
23
  export const xaiErrorDataSchema = z.union([
19
24
  chatCompletionsErrorSchema,
20
25
  responsesErrorSchema,
26
+ speechErrorSchema,
21
27
  ]);
22
28
 
23
29
  export type XaiErrorData = z.infer<typeof xaiErrorDataSchema>;
24
30
 
25
31
  export const xaiFailedResponseHandler = createJsonErrorResponseHandler({
26
32
  errorSchema: xaiErrorDataSchema,
27
- errorToMessage: data =>
28
- 'code' in data ? `${data.code}: ${data.error}` : data.error.message,
33
+ errorToMessage: data => {
34
+ if (typeof data.error === 'string') {
35
+ return 'code' in data ? `${data.code}: ${data.error}` : data.error;
36
+ }
37
+ return data.error.message;
38
+ },
29
39
  });
@@ -46,6 +46,20 @@ export const xaiSpeechModelOptionsSchema = lazySchema(() =>
46
46
  * Normalize written-form text into spoken-form text before synthesis.
47
47
  */
48
48
  textNormalization: z.boolean().nullish(),
49
+
50
+ /**
51
+ * Return character-level timing metadata alongside the audio. When
52
+ * enabled, the response carries per-character start/end times and the
53
+ * total duration, exposed via `providerMetadata.xai`.
54
+ */
55
+ withTimestamps: z.boolean().nullish(),
56
+
57
+ /**
58
+ * Map of phrases to spoken substitutions applied before synthesis.
59
+ * Values may be respellings (`{ 'Acme Mobile': 'Acme Mobull' }`) or IPA
60
+ * phonetics (`{ nginx: '/ˈɛndʒɪn ˈɛks/' }`).
61
+ */
62
+ replace: z.record(z.string(), z.string()).nullish(),
49
63
  }),
50
64
  ),
51
65
  );
@@ -1,7 +1,9 @@
1
1
  import type { SharedV4Warning, SpeechModelV4 } from '@ai-sdk/provider';
2
2
  import {
3
3
  combineHeaders,
4
+ convertBase64ToUint8Array,
4
5
  createBinaryResponseHandler,
6
+ createJsonResponseHandler,
5
7
  parseProviderOptions,
6
8
  postJsonToApi,
7
9
  resolve,
@@ -11,6 +13,7 @@ import {
11
13
  type FetchFunction,
12
14
  type Resolvable,
13
15
  } from '@ai-sdk/provider-utils';
16
+ import { z } from 'zod/v4';
14
17
  import { xaiFailedResponseHandler } from './xai-error';
15
18
  import { xaiSpeechModelOptionsSchema } from './xai-speech-model-options';
16
19
 
@@ -122,22 +125,30 @@ export class XaiSpeechModel implements SpeechModelV4 {
122
125
  speed,
123
126
  optimize_streaming_latency: xaiOptions?.optimizeStreamingLatency,
124
127
  text_normalization: xaiOptions?.textNormalization,
128
+ with_timestamps: xaiOptions?.withTimestamps,
129
+ replace: xaiOptions?.replace,
125
130
  };
126
131
 
127
- return { requestBody, warnings };
132
+ return {
133
+ requestBody,
134
+ warnings,
135
+ withTimestamps: xaiOptions?.withTimestamps === true,
136
+ };
128
137
  }
129
138
 
130
139
  async doGenerate(
131
140
  options: Parameters<SpeechModelV4['doGenerate']>[0],
132
141
  ): Promise<Awaited<ReturnType<SpeechModelV4['doGenerate']>>> {
133
142
  const currentDate = this.config._internal?.currentDate?.() ?? new Date();
134
- const { requestBody, warnings } = await this.getArgs(options);
135
-
136
- const {
137
- value: audio,
138
- responseHeaders,
139
- rawValue: rawResponse,
140
- } = await postJsonToApi({
143
+ const { requestBody, warnings, withTimestamps } =
144
+ await this.getArgs(options);
145
+
146
+ // With `with_timestamps` the API returns a JSON envelope carrying
147
+ // base64-encoded audio plus character-level timings instead of raw
148
+ // audio bytes.
149
+ const { value, responseHeaders, rawValue } = await postJsonToApi<
150
+ Uint8Array | XaiSpeechTimestampsResponse
151
+ >({
141
152
  url: `${this.config.baseURL}/tts`,
142
153
  headers: combineHeaders(
143
154
  this.config.headers ? await resolve(this.config.headers) : undefined,
@@ -145,11 +156,30 @@ export class XaiSpeechModel implements SpeechModelV4 {
145
156
  ),
146
157
  body: requestBody,
147
158
  failedResponseHandler: xaiFailedResponseHandler,
148
- successfulResponseHandler: createBinaryResponseHandler(),
159
+ successfulResponseHandler: withTimestamps
160
+ ? createJsonResponseHandler(xaiSpeechTimestampsResponseSchema)
161
+ : createBinaryResponseHandler(),
149
162
  abortSignal: options.abortSignal,
150
163
  fetch: this.config.fetch,
151
164
  });
152
165
 
166
+ let audio: Uint8Array;
167
+ let envelope: XaiSpeechTimestampsResponse | undefined;
168
+ if (value instanceof Uint8Array) {
169
+ audio = value;
170
+ } else {
171
+ envelope = value;
172
+ // Empty audio is returned as-is so the core layer throws
173
+ // NoSpeechGeneratedError.
174
+ audio =
175
+ envelope.audio != null
176
+ ? convertBase64ToUint8Array(envelope.audio)
177
+ : new Uint8Array(0);
178
+ }
179
+
180
+ // xAI returns a trace id on every response (success and error).
181
+ const traceId = responseHeaders?.['x-trace-id'];
182
+
153
183
  return {
154
184
  audio,
155
185
  warnings,
@@ -160,8 +190,46 @@ export class XaiSpeechModel implements SpeechModelV4 {
160
190
  timestamp: currentDate,
161
191
  modelId: this.modelId,
162
192
  headers: responseHeaders,
163
- body: rawResponse,
193
+ body: rawValue,
194
+ },
195
+ providerMetadata: {
196
+ xai: {
197
+ ...(traceId != null ? { traceId } : {}),
198
+ ...(envelope?.duration != null
199
+ ? { duration: envelope.duration }
200
+ : {}),
201
+ ...(envelope?.content_type != null
202
+ ? { contentType: envelope.content_type }
203
+ : {}),
204
+ ...(envelope?.audio_timestamps != null
205
+ ? {
206
+ audioTimestamps: {
207
+ graphChars: envelope.audio_timestamps.graph_chars,
208
+ graphTimes: envelope.audio_timestamps.graph_times,
209
+ },
210
+ }
211
+ : {}),
212
+ },
164
213
  },
165
214
  };
166
215
  }
167
216
  }
217
+
218
+ // Minimal schema for the `with_timestamps` JSON envelope: only the fields
219
+ // the implementation reads, with `.nullish()` so provider API changes don't
220
+ // break parsing.
221
+ const xaiSpeechTimestampsResponseSchema = z.object({
222
+ audio: z.string().nullish(),
223
+ content_type: z.string().nullish(),
224
+ duration: z.number().nullish(),
225
+ audio_timestamps: z
226
+ .object({
227
+ graph_chars: z.array(z.string()),
228
+ graph_times: z.array(z.tuple([z.number(), z.number()])),
229
+ })
230
+ .nullish(),
231
+ });
232
+
233
+ type XaiSpeechTimestampsResponse = z.infer<
234
+ typeof xaiSpeechTimestampsResponseSchema
235
+ >;
@@ -581,19 +581,30 @@ export class XaiVideoModel implements VideoModelV4 {
581
581
  statusResponse.status === 'done' ||
582
582
  (statusResponse.status == null && statusResponse.video?.url)
583
583
  ) {
584
+ // Terminal outcomes, so they are reported the same way as an upstream `failed`
584
585
  if (statusResponse.video?.respect_moderation === false) {
585
- throw new AISDKError({
586
- name: 'XAI_VIDEO_MODERATION_ERROR',
587
- message:
586
+ return {
587
+ status: 'error' as const,
588
+ error:
588
589
  'Video generation was blocked due to a content policy violation.',
589
- });
590
+ response: {
591
+ timestamp: currentDate,
592
+ modelId: this.modelId,
593
+ headers: responseHeaders,
594
+ },
595
+ };
590
596
  }
591
597
 
592
598
  if (!statusResponse.video?.url) {
593
- throw new AISDKError({
594
- name: 'XAI_VIDEO_GENERATION_ERROR',
595
- message: 'Video generation completed but no video URL was returned.',
596
- });
599
+ return {
600
+ status: 'error' as const,
601
+ error: 'Video generation completed but no video URL was returned.',
602
+ response: {
603
+ timestamp: currentDate,
604
+ modelId: this.modelId,
605
+ headers: responseHeaders,
606
+ },
607
+ };
597
608
  }
598
609
 
599
610
  return {