@ai-sdk/xai 4.0.39 → 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
@@ -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<string, string>_
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.39",
3
+ "version": "4.0.40",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
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
+ >;