@tanstack/ai-grok 0.14.8 → 0.14.10
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/dist/esm/adapters/image.js +228 -193
- package/dist/esm/adapters/image.js.map +1 -1
- package/dist/esm/adapters/summarize.js +49 -12
- package/dist/esm/adapters/summarize.js.map +1 -1
- package/dist/esm/adapters/text.js +82 -38
- package/dist/esm/adapters/text.js.map +1 -1
- package/dist/esm/adapters/transcription.js +133 -114
- package/dist/esm/adapters/transcription.js.map +1 -1
- package/dist/esm/adapters/tts.js +147 -130
- package/dist/esm/adapters/tts.js.map +1 -1
- package/dist/esm/adapters/video.js +275 -226
- package/dist/esm/adapters/video.js.map +1 -1
- package/dist/esm/image/image-provider-options.js +64 -66
- package/dist/esm/image/image-provider-options.js.map +1 -1
- package/dist/esm/index.js +3 -31
- package/dist/esm/model-meta.js +149 -47
- package/dist/esm/model-meta.js.map +1 -1
- package/dist/esm/realtime/adapter.js +729 -812
- package/dist/esm/realtime/adapter.js.map +1 -1
- package/dist/esm/realtime/index.js +3 -0
- package/dist/esm/realtime/token.js +78 -72
- package/dist/esm/realtime/token.js.map +1 -1
- package/dist/esm/tools/index.js +57 -96
- package/dist/esm/tools/index.js.map +1 -1
- package/dist/esm/utils/audio.js +116 -162
- package/dist/esm/utils/audio.js.map +1 -1
- package/dist/esm/utils/client.js +22 -16
- package/dist/esm/utils/client.js.map +1 -1
- package/dist/esm/utils/index.js +5 -0
- package/dist/esm/utils/schema-converter.js +2 -0
- package/dist/esm/video/video-provider-options.js +83 -57
- package/dist/esm/video/video-provider-options.js.map +1 -1
- package/package.json +8 -8
- package/src/realtime/adapter.ts +1 -1
- package/src/utils/audio.ts +1 -1
- package/dist/esm/index.js.map +0 -1
package/dist/esm/adapters/tts.js
CHANGED
|
@@ -1,142 +1,159 @@
|
|
|
1
|
-
import { BaseTTSAdapter } from "@tanstack/ai/adapters";
|
|
2
|
-
import { generateId } from "@tanstack/ai-utils";
|
|
3
1
|
import { getGrokApiKeyFromEnv } from "../utils/client.js";
|
|
4
|
-
import "@tanstack/openai-base";
|
|
5
2
|
import { arrayBufferToBase64 } from "../utils/audio.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
3
|
+
import { generateId } from "../utils/index.js";
|
|
4
|
+
import { BaseTTSAdapter } from "@tanstack/ai/adapters";
|
|
5
|
+
//#region src/adapters/tts.ts
|
|
6
|
+
var DEFAULT_GROK_BASE_URL = "https://api.x.ai/v1";
|
|
7
|
+
/**
|
|
8
|
+
* Grok Text-to-Speech Adapter.
|
|
9
|
+
*
|
|
10
|
+
* Talks to `POST {baseURL}/tts` per
|
|
11
|
+
* https://docs.x.ai/developers/model-capabilities/audio/text-to-speech
|
|
12
|
+
*/
|
|
13
|
+
var GrokSpeechAdapter = class extends BaseTTSAdapter {
|
|
14
|
+
name = "grok";
|
|
15
|
+
apiKey;
|
|
16
|
+
baseURL;
|
|
17
|
+
defaultHeaders;
|
|
18
|
+
constructor(config, model) {
|
|
19
|
+
super(model, config);
|
|
20
|
+
this.apiKey = config.apiKey;
|
|
21
|
+
this.baseURL = (config.baseURL ?? DEFAULT_GROK_BASE_URL).replace(/\/+$/, "");
|
|
22
|
+
this.defaultHeaders = config.defaultHeaders ?? {};
|
|
23
|
+
}
|
|
24
|
+
async generateSpeech(options) {
|
|
25
|
+
const { logger } = options;
|
|
26
|
+
const { model, text, voice, format, modelOptions } = options;
|
|
27
|
+
logger.request(`activity=generateSpeech provider=grok model=${model}`, {
|
|
28
|
+
provider: "grok",
|
|
29
|
+
model
|
|
30
|
+
});
|
|
31
|
+
const { body, codec, sampleRateForContentType } = buildTTSRequestBody({
|
|
32
|
+
text,
|
|
33
|
+
voice,
|
|
34
|
+
format,
|
|
35
|
+
modelOptions
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetch(`${this.baseURL}/tts`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: {
|
|
41
|
+
...this.defaultHeaders,
|
|
42
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
43
|
+
"Content-Type": "application/json"
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify(body)
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const errorText = await response.text();
|
|
49
|
+
throw new Error(`Grok TTS request failed: ${response.status} ${errorText}`);
|
|
50
|
+
}
|
|
51
|
+
const audio = arrayBufferToBase64(await response.arrayBuffer());
|
|
52
|
+
return {
|
|
53
|
+
id: generateId(this.name),
|
|
54
|
+
model,
|
|
55
|
+
audio,
|
|
56
|
+
format: codec,
|
|
57
|
+
contentType: getContentType(codec, sampleRateForContentType)
|
|
58
|
+
};
|
|
59
|
+
} catch (error) {
|
|
60
|
+
logger.errors("grok.generateSpeech fatal", {
|
|
61
|
+
error,
|
|
62
|
+
source: "grok.generateSpeech"
|
|
63
|
+
});
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Build the JSON body for `POST /v1/tts`, resolving codec / sample-rate / voice
|
|
70
|
+
* defaults in one place.
|
|
71
|
+
*
|
|
72
|
+
* Returns the request `body`, the resolved `codec`, and the `sampleRateForContentType`
|
|
73
|
+
* used by the caller to label the response via `getContentType`.
|
|
74
|
+
*/
|
|
68
75
|
function buildTTSRequestBody(options) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
if (modelOptions?.text_normalization !== void 0) {
|
|
94
|
-
body.text_normalization = modelOptions.text_normalization;
|
|
95
|
-
}
|
|
96
|
-
return { body, codec, sampleRateForContentType };
|
|
76
|
+
const { text, voice, format, modelOptions } = options;
|
|
77
|
+
const codec = pickCodec(modelOptions?.codec, format);
|
|
78
|
+
const callerSampleRate = modelOptions?.sample_rate;
|
|
79
|
+
const pcmDefault = 24e3;
|
|
80
|
+
const needsRateInContentType = codec === "pcm";
|
|
81
|
+
const outputFormat = { codec };
|
|
82
|
+
if (callerSampleRate !== void 0) outputFormat.sample_rate = callerSampleRate;
|
|
83
|
+
else if (needsRateInContentType) outputFormat.sample_rate = pcmDefault;
|
|
84
|
+
if (codec === "mp3" && modelOptions?.bit_rate !== void 0) outputFormat.bit_rate = modelOptions.bit_rate;
|
|
85
|
+
const sampleRateForContentType = callerSampleRate ?? pcmDefault;
|
|
86
|
+
const body = {
|
|
87
|
+
text,
|
|
88
|
+
voice_id: voice ?? "eve",
|
|
89
|
+
language: modelOptions?.language ?? "en",
|
|
90
|
+
output_format: outputFormat
|
|
91
|
+
};
|
|
92
|
+
if (modelOptions?.optimize_streaming_latency !== void 0) body.optimize_streaming_latency = modelOptions.optimize_streaming_latency;
|
|
93
|
+
if (modelOptions?.text_normalization !== void 0) body.text_normalization = modelOptions.text_normalization;
|
|
94
|
+
return {
|
|
95
|
+
body,
|
|
96
|
+
codec,
|
|
97
|
+
sampleRateForContentType
|
|
98
|
+
};
|
|
97
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Maps the cross-provider `TTSOptions.format` onto Grok's supported codecs.
|
|
102
|
+
* `opus`, `aac`, and `flac` are not supported by xAI TTS (which only exposes
|
|
103
|
+
* mp3/wav/pcm/mulaw/alaw) — we fall back to mp3. An explicit
|
|
104
|
+
* `modelOptions.codec` always wins.
|
|
105
|
+
*/
|
|
98
106
|
function pickCodec(codecOverride, format) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
default:
|
|
111
|
-
return "mp3";
|
|
112
|
-
}
|
|
107
|
+
if (codecOverride) return codecOverride;
|
|
108
|
+
if (!format) return "mp3";
|
|
109
|
+
switch (format) {
|
|
110
|
+
case "mp3":
|
|
111
|
+
case "wav":
|
|
112
|
+
case "pcm": return format;
|
|
113
|
+
case "flac":
|
|
114
|
+
case "opus":
|
|
115
|
+
case "aac": return "mp3";
|
|
116
|
+
default: return "mp3";
|
|
117
|
+
}
|
|
113
118
|
}
|
|
114
119
|
function getContentType(codec, sampleRate) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
case "mulaw":
|
|
123
|
-
return sampleRate === 8e3 ? "audio/basic" : `audio/PCMU;rate=${sampleRate}`;
|
|
124
|
-
case "alaw":
|
|
125
|
-
return sampleRate === 8e3 ? "audio/x-alaw-basic" : `audio/PCMA;rate=${sampleRate}`;
|
|
126
|
-
}
|
|
120
|
+
switch (codec) {
|
|
121
|
+
case "mp3": return "audio/mpeg";
|
|
122
|
+
case "wav": return "audio/wav";
|
|
123
|
+
case "pcm": return `audio/L16;rate=${sampleRate}`;
|
|
124
|
+
case "mulaw": return sampleRate === 8e3 ? "audio/basic" : `audio/PCMU;rate=${sampleRate}`;
|
|
125
|
+
case "alaw": return sampleRate === 8e3 ? "audio/x-alaw-basic" : `audio/PCMA;rate=${sampleRate}`;
|
|
126
|
+
}
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Creates a Grok speech (TTS) adapter with an explicit API key.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* const adapter = createGrokSpeech('grok-tts', 'xai-...')
|
|
134
|
+
* const result = await generateSpeech({
|
|
135
|
+
* adapter,
|
|
136
|
+
* text: 'Hello from Grok',
|
|
137
|
+
* voice: 'eve',
|
|
138
|
+
* })
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
128
141
|
function createGrokSpeech(model, apiKey, config) {
|
|
129
|
-
|
|
142
|
+
return new GrokSpeechAdapter({
|
|
143
|
+
apiKey,
|
|
144
|
+
...config
|
|
145
|
+
}, model);
|
|
130
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Creates a Grok speech (TTS) adapter, reading the API key from
|
|
149
|
+
* `XAI_API_KEY` in the environment.
|
|
150
|
+
*
|
|
151
|
+
* @throws Error if `XAI_API_KEY` is not set.
|
|
152
|
+
*/
|
|
131
153
|
function grokSpeech(model, config) {
|
|
132
|
-
|
|
133
|
-
return createGrokSpeech(model, apiKey, config);
|
|
154
|
+
return createGrokSpeech(model, getGrokApiKeyFromEnv(), config);
|
|
134
155
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
getContentType,
|
|
140
|
-
grokSpeech
|
|
141
|
-
};
|
|
142
|
-
//# sourceMappingURL=tts.js.map
|
|
156
|
+
//#endregion
|
|
157
|
+
export { GrokSpeechAdapter, buildTTSRequestBody, createGrokSpeech, getContentType, grokSpeech };
|
|
158
|
+
|
|
159
|
+
//# sourceMappingURL=tts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tts.js","sources":["../../../src/adapters/tts.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport { arrayBufferToBase64, generateId, getGrokApiKeyFromEnv } from '../utils'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GrokTTSModel } from '../model-meta'\nimport type {\n GrokTTSCodec,\n GrokTTSProviderOptions,\n} from '../audio/tts-provider-options'\n\nconst DEFAULT_GROK_BASE_URL = 'https://api.x.ai/v1'\n\n/**\n * Configuration for the Grok TTS adapter.\n *\n * Unlike chat/image/summarize adapters, TTS does not use the OpenAI SDK\n * because xAI's `/v1/tts` endpoint is not OpenAI-compatible. This config\n * is a minimal subset suitable for direct `fetch` calls.\n */\nexport interface GrokSpeechConfig {\n apiKey: string\n baseURL?: string\n /** Additional headers to merge into every request (e.g., test IDs). */\n defaultHeaders?: Record<string, string>\n}\n\n/**\n * Grok Text-to-Speech Adapter.\n *\n * Talks to `POST {baseURL}/tts` per\n * https://docs.x.ai/developers/model-capabilities/audio/text-to-speech\n */\nexport class GrokSpeechAdapter<\n TModel extends GrokTTSModel,\n> extends BaseTTSAdapter<TModel, GrokTTSProviderOptions> {\n readonly name = 'grok' as const\n\n private readonly apiKey: string\n private readonly baseURL: string\n private readonly defaultHeaders: Record<string, string>\n\n constructor(config: GrokSpeechConfig, model: TModel) {\n super(model, config)\n this.apiKey = config.apiKey\n this.baseURL = (config.baseURL ?? DEFAULT_GROK_BASE_URL).replace(/\\/+$/, '')\n this.defaultHeaders = config.defaultHeaders ?? {}\n }\n\n async generateSpeech(\n options: TTSOptions<GrokTTSProviderOptions>,\n ): Promise<TTSResult> {\n const { logger } = options\n const { model, text, voice, format, modelOptions } = options\n\n logger.request(`activity=generateSpeech provider=grok model=${model}`, {\n provider: 'grok',\n model,\n })\n\n const { body, codec, sampleRateForContentType } = buildTTSRequestBody({\n text,\n voice,\n format,\n modelOptions,\n })\n\n try {\n const response = await fetch(`${this.baseURL}/tts`, {\n method: 'POST',\n headers: {\n // `defaultHeaders` first so the adapter's Authorization / Content-Type\n // always win — otherwise a caller-supplied `Authorization` header\n // could silently clobber the bearer token.\n ...this.defaultHeaders,\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `Grok TTS request failed: ${response.status} ${errorText}`,\n )\n }\n\n const arrayBuffer = await response.arrayBuffer()\n const audio = arrayBufferToBase64(arrayBuffer)\n\n return {\n id: generateId(this.name),\n model,\n audio,\n format: codec,\n contentType: getContentType(codec, sampleRateForContentType),\n }\n } catch (error) {\n logger.errors('grok.generateSpeech fatal', {\n error,\n source: 'grok.generateSpeech',\n })\n throw error\n }\n }\n}\n\n/**\n * Build the JSON body for `POST /v1/tts`, resolving codec / sample-rate / voice\n * defaults in one place.\n *\n * Returns the request `body`, the resolved `codec`, and the `sampleRateForContentType`\n * used by the caller to label the response via `getContentType`.\n */\nexport function buildTTSRequestBody(options: {\n text: string\n voice: string | undefined\n format: TTSOptions['format'] | undefined\n modelOptions: GrokTTSProviderOptions | undefined\n}): {\n body: Record<string, unknown>\n codec: GrokTTSCodec\n sampleRateForContentType: number\n} {\n const { text, voice, format, modelOptions } = options\n\n const codec = pickCodec(modelOptions?.codec, format)\n\n // Only forward `sample_rate` when either:\n // - the caller explicitly set `modelOptions.sample_rate`, or\n // - the codec's Content-Type carries the rate (pcm → audio/L16;rate=…).\n // For mp3/wav/opus/aac/flac we leave sample_rate unset so xAI's server\n // default applies.\n const callerSampleRate = modelOptions?.sample_rate\n // Default sample rate documented in GrokTTSProviderOptions is 24000 Hz —\n // used only when we MUST attach a rate to the contentType (pcm) and the\n // caller didn't pick one.\n const pcmDefault = 24000\n const needsRateInContentType = codec === 'pcm'\n\n const outputFormat: Record<string, unknown> = { codec }\n if (callerSampleRate !== undefined) {\n outputFormat.sample_rate = callerSampleRate\n } else if (needsRateInContentType) {\n outputFormat.sample_rate = pcmDefault\n }\n if (codec === 'mp3' && modelOptions?.bit_rate !== undefined) {\n outputFormat.bit_rate = modelOptions.bit_rate\n }\n\n // pcm embeds the rate in `audio/L16;rate=…`; mulaw/alaw embed it in\n // `audio/PCMU;rate=…` / `audio/PCMA;rate=…` when non-default. mp3/wav\n // don't carry a rate parameter so the value is unused for those.\n const sampleRateForContentType = callerSampleRate ?? pcmDefault\n\n const body: Record<string, unknown> = {\n text,\n voice_id: voice ?? 'eve',\n language: modelOptions?.language ?? 'en',\n output_format: outputFormat,\n }\n if (modelOptions?.optimize_streaming_latency !== undefined) {\n body.optimize_streaming_latency = modelOptions.optimize_streaming_latency\n }\n if (modelOptions?.text_normalization !== undefined) {\n body.text_normalization = modelOptions.text_normalization\n }\n\n return { body, codec, sampleRateForContentType }\n}\n\n/**\n * Maps the cross-provider `TTSOptions.format` onto Grok's supported codecs.\n * `opus`, `aac`, and `flac` are not supported by xAI TTS (which only exposes\n * mp3/wav/pcm/mulaw/alaw) — we fall back to mp3. An explicit\n * `modelOptions.codec` always wins.\n */\nfunction pickCodec(\n codecOverride: GrokTTSCodec | undefined,\n format: TTSOptions['format'] | undefined,\n): GrokTTSCodec {\n if (codecOverride) return codecOverride\n if (!format) return 'mp3'\n switch (format) {\n case 'mp3':\n case 'wav':\n case 'pcm':\n return format\n case 'flac':\n case 'opus':\n case 'aac':\n return 'mp3'\n default:\n return 'mp3'\n }\n}\n\nexport function getContentType(\n codec: GrokTTSCodec,\n sampleRate: number,\n): string {\n switch (codec) {\n case 'mp3':\n return 'audio/mpeg'\n case 'wav':\n return 'audio/wav'\n case 'pcm':\n // `audio/L16` requires a `rate` parameter per RFC 3551/3555.\n return `audio/L16;rate=${sampleRate}`\n case 'mulaw':\n // `audio/basic` is 8 kHz mono by RFC 2046 registration. For non-8kHz\n // streams xAI still produces mulaw-encoded bytes at the requested\n // rate, but the registered MIME can't carry that rate — so we use\n // the non-standard but commonly-supported `audio/PCMU;rate=…` (RFC 3551\n // RTP payload name) whenever the caller asked for a rate other than\n // 8000, and keep `audio/basic` for the standard 8kHz case.\n return sampleRate === 8000\n ? 'audio/basic'\n : `audio/PCMU;rate=${sampleRate}`\n case 'alaw':\n return sampleRate === 8000\n ? 'audio/x-alaw-basic'\n : `audio/PCMA;rate=${sampleRate}`\n }\n}\n\n/**\n * Creates a Grok speech (TTS) adapter with an explicit API key.\n *\n * @example\n * ```typescript\n * const adapter = createGrokSpeech('grok-tts', 'xai-...')\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello from Grok',\n * voice: 'eve',\n * })\n * ```\n */\nexport function createGrokSpeech<TModel extends GrokTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokSpeechConfig, 'apiKey'>,\n): GrokSpeechAdapter<TModel> {\n return new GrokSpeechAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok speech (TTS) adapter, reading the API key from\n * `XAI_API_KEY` in the environment.\n *\n * @throws Error if `XAI_API_KEY` is not set.\n */\nexport function grokSpeech<TModel extends GrokTTSModel>(\n model: TModel,\n config?: Omit<GrokSpeechConfig, 'apiKey'>,\n): GrokSpeechAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokSpeech(model, apiKey, config)\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tts.js","names":[],"sources":["../../../src/adapters/tts.ts"],"sourcesContent":["import { BaseTTSAdapter } from '@tanstack/ai/adapters'\nimport { arrayBufferToBase64, generateId, getGrokApiKeyFromEnv } from '../utils'\nimport type { TTSOptions, TTSResult } from '@tanstack/ai'\nimport type { GrokTTSModel } from '../model-meta'\nimport type {\n GrokTTSCodec,\n GrokTTSProviderOptions,\n} from '../audio/tts-provider-options'\n\nconst DEFAULT_GROK_BASE_URL = 'https://api.x.ai/v1'\n\n/**\n * Configuration for the Grok TTS adapter.\n *\n * Unlike chat/image/summarize adapters, TTS does not use the OpenAI SDK\n * because xAI's `/v1/tts` endpoint is not OpenAI-compatible. This config\n * is a minimal subset suitable for direct `fetch` calls.\n */\nexport interface GrokSpeechConfig {\n apiKey: string\n baseURL?: string\n /** Additional headers to merge into every request (e.g., test IDs). */\n defaultHeaders?: Record<string, string>\n}\n\n/**\n * Grok Text-to-Speech Adapter.\n *\n * Talks to `POST {baseURL}/tts` per\n * https://docs.x.ai/developers/model-capabilities/audio/text-to-speech\n */\nexport class GrokSpeechAdapter<\n TModel extends GrokTTSModel,\n> extends BaseTTSAdapter<TModel, GrokTTSProviderOptions> {\n readonly name = 'grok' as const\n\n private readonly apiKey: string\n private readonly baseURL: string\n private readonly defaultHeaders: Record<string, string>\n\n constructor(config: GrokSpeechConfig, model: TModel) {\n super(model, config)\n this.apiKey = config.apiKey\n this.baseURL = (config.baseURL ?? DEFAULT_GROK_BASE_URL).replace(/\\/+$/, '')\n this.defaultHeaders = config.defaultHeaders ?? {}\n }\n\n async generateSpeech(\n options: TTSOptions<GrokTTSProviderOptions>,\n ): Promise<TTSResult> {\n const { logger } = options\n const { model, text, voice, format, modelOptions } = options\n\n logger.request(`activity=generateSpeech provider=grok model=${model}`, {\n provider: 'grok',\n model,\n })\n\n const { body, codec, sampleRateForContentType } = buildTTSRequestBody({\n text,\n voice,\n format,\n modelOptions,\n })\n\n try {\n const response = await fetch(`${this.baseURL}/tts`, {\n method: 'POST',\n headers: {\n // `defaultHeaders` first so the adapter's Authorization / Content-Type\n // always win — otherwise a caller-supplied `Authorization` header\n // could silently clobber the bearer token.\n ...this.defaultHeaders,\n Authorization: `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `Grok TTS request failed: ${response.status} ${errorText}`,\n )\n }\n\n const arrayBuffer = await response.arrayBuffer()\n const audio = arrayBufferToBase64(arrayBuffer)\n\n return {\n id: generateId(this.name),\n model,\n audio,\n format: codec,\n contentType: getContentType(codec, sampleRateForContentType),\n }\n } catch (error) {\n logger.errors('grok.generateSpeech fatal', {\n error,\n source: 'grok.generateSpeech',\n })\n throw error\n }\n }\n}\n\n/**\n * Build the JSON body for `POST /v1/tts`, resolving codec / sample-rate / voice\n * defaults in one place.\n *\n * Returns the request `body`, the resolved `codec`, and the `sampleRateForContentType`\n * used by the caller to label the response via `getContentType`.\n */\nexport function buildTTSRequestBody(options: {\n text: string\n voice: string | undefined\n format: TTSOptions['format'] | undefined\n modelOptions: GrokTTSProviderOptions | undefined\n}): {\n body: Record<string, unknown>\n codec: GrokTTSCodec\n sampleRateForContentType: number\n} {\n const { text, voice, format, modelOptions } = options\n\n const codec = pickCodec(modelOptions?.codec, format)\n\n // Only forward `sample_rate` when either:\n // - the caller explicitly set `modelOptions.sample_rate`, or\n // - the codec's Content-Type carries the rate (pcm → audio/L16;rate=…).\n // For mp3/wav/opus/aac/flac we leave sample_rate unset so xAI's server\n // default applies.\n const callerSampleRate = modelOptions?.sample_rate\n // Default sample rate documented in GrokTTSProviderOptions is 24000 Hz —\n // used only when we MUST attach a rate to the contentType (pcm) and the\n // caller didn't pick one.\n const pcmDefault = 24000\n const needsRateInContentType = codec === 'pcm'\n\n const outputFormat: Record<string, unknown> = { codec }\n if (callerSampleRate !== undefined) {\n outputFormat.sample_rate = callerSampleRate\n } else if (needsRateInContentType) {\n outputFormat.sample_rate = pcmDefault\n }\n if (codec === 'mp3' && modelOptions?.bit_rate !== undefined) {\n outputFormat.bit_rate = modelOptions.bit_rate\n }\n\n // pcm embeds the rate in `audio/L16;rate=…`; mulaw/alaw embed it in\n // `audio/PCMU;rate=…` / `audio/PCMA;rate=…` when non-default. mp3/wav\n // don't carry a rate parameter so the value is unused for those.\n const sampleRateForContentType = callerSampleRate ?? pcmDefault\n\n const body: Record<string, unknown> = {\n text,\n voice_id: voice ?? 'eve',\n language: modelOptions?.language ?? 'en',\n output_format: outputFormat,\n }\n if (modelOptions?.optimize_streaming_latency !== undefined) {\n body.optimize_streaming_latency = modelOptions.optimize_streaming_latency\n }\n if (modelOptions?.text_normalization !== undefined) {\n body.text_normalization = modelOptions.text_normalization\n }\n\n return { body, codec, sampleRateForContentType }\n}\n\n/**\n * Maps the cross-provider `TTSOptions.format` onto Grok's supported codecs.\n * `opus`, `aac`, and `flac` are not supported by xAI TTS (which only exposes\n * mp3/wav/pcm/mulaw/alaw) — we fall back to mp3. An explicit\n * `modelOptions.codec` always wins.\n */\nfunction pickCodec(\n codecOverride: GrokTTSCodec | undefined,\n format: TTSOptions['format'] | undefined,\n): GrokTTSCodec {\n if (codecOverride) return codecOverride\n if (!format) return 'mp3'\n switch (format) {\n case 'mp3':\n case 'wav':\n case 'pcm':\n return format\n case 'flac':\n case 'opus':\n case 'aac':\n return 'mp3'\n default:\n return 'mp3'\n }\n}\n\nexport function getContentType(\n codec: GrokTTSCodec,\n sampleRate: number,\n): string {\n switch (codec) {\n case 'mp3':\n return 'audio/mpeg'\n case 'wav':\n return 'audio/wav'\n case 'pcm':\n // `audio/L16` requires a `rate` parameter per RFC 3551/3555.\n return `audio/L16;rate=${sampleRate}`\n case 'mulaw':\n // `audio/basic` is 8 kHz mono by RFC 2046 registration. For non-8kHz\n // streams xAI still produces mulaw-encoded bytes at the requested\n // rate, but the registered MIME can't carry that rate — so we use\n // the non-standard but commonly-supported `audio/PCMU;rate=…` (RFC 3551\n // RTP payload name) whenever the caller asked for a rate other than\n // 8000, and keep `audio/basic` for the standard 8kHz case.\n return sampleRate === 8000\n ? 'audio/basic'\n : `audio/PCMU;rate=${sampleRate}`\n case 'alaw':\n return sampleRate === 8000\n ? 'audio/x-alaw-basic'\n : `audio/PCMA;rate=${sampleRate}`\n }\n}\n\n/**\n * Creates a Grok speech (TTS) adapter with an explicit API key.\n *\n * @example\n * ```typescript\n * const adapter = createGrokSpeech('grok-tts', 'xai-...')\n * const result = await generateSpeech({\n * adapter,\n * text: 'Hello from Grok',\n * voice: 'eve',\n * })\n * ```\n */\nexport function createGrokSpeech<TModel extends GrokTTSModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<GrokSpeechConfig, 'apiKey'>,\n): GrokSpeechAdapter<TModel> {\n return new GrokSpeechAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * Creates a Grok speech (TTS) adapter, reading the API key from\n * `XAI_API_KEY` in the environment.\n *\n * @throws Error if `XAI_API_KEY` is not set.\n */\nexport function grokSpeech<TModel extends GrokTTSModel>(\n model: TModel,\n config?: Omit<GrokSpeechConfig, 'apiKey'>,\n): GrokSpeechAdapter<TModel> {\n const apiKey = getGrokApiKeyFromEnv()\n return createGrokSpeech(model, apiKey, config)\n}\n"],"mappings":"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"}
|