@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.
Files changed (36) hide show
  1. package/dist/esm/adapters/image.js +228 -193
  2. package/dist/esm/adapters/image.js.map +1 -1
  3. package/dist/esm/adapters/summarize.js +49 -12
  4. package/dist/esm/adapters/summarize.js.map +1 -1
  5. package/dist/esm/adapters/text.js +82 -38
  6. package/dist/esm/adapters/text.js.map +1 -1
  7. package/dist/esm/adapters/transcription.js +133 -114
  8. package/dist/esm/adapters/transcription.js.map +1 -1
  9. package/dist/esm/adapters/tts.js +147 -130
  10. package/dist/esm/adapters/tts.js.map +1 -1
  11. package/dist/esm/adapters/video.js +275 -226
  12. package/dist/esm/adapters/video.js.map +1 -1
  13. package/dist/esm/image/image-provider-options.js +64 -66
  14. package/dist/esm/image/image-provider-options.js.map +1 -1
  15. package/dist/esm/index.js +3 -31
  16. package/dist/esm/model-meta.js +149 -47
  17. package/dist/esm/model-meta.js.map +1 -1
  18. package/dist/esm/realtime/adapter.js +729 -812
  19. package/dist/esm/realtime/adapter.js.map +1 -1
  20. package/dist/esm/realtime/index.js +3 -0
  21. package/dist/esm/realtime/token.js +78 -72
  22. package/dist/esm/realtime/token.js.map +1 -1
  23. package/dist/esm/tools/index.js +57 -96
  24. package/dist/esm/tools/index.js.map +1 -1
  25. package/dist/esm/utils/audio.js +116 -162
  26. package/dist/esm/utils/audio.js.map +1 -1
  27. package/dist/esm/utils/client.js +22 -16
  28. package/dist/esm/utils/client.js.map +1 -1
  29. package/dist/esm/utils/index.js +5 -0
  30. package/dist/esm/utils/schema-converter.js +2 -0
  31. package/dist/esm/video/video-provider-options.js +83 -57
  32. package/dist/esm/video/video-provider-options.js.map +1 -1
  33. package/package.json +8 -8
  34. package/src/realtime/adapter.ts +1 -1
  35. package/src/utils/audio.ts +1 -1
  36. package/dist/esm/index.js.map +0 -1
@@ -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
- const DEFAULT_GROK_BASE_URL = "https://api.x.ai/v1";
7
- class GrokSpeechAdapter extends BaseTTSAdapter {
8
- name = "grok";
9
- apiKey;
10
- baseURL;
11
- defaultHeaders;
12
- constructor(config, model) {
13
- super(model, config);
14
- this.apiKey = config.apiKey;
15
- this.baseURL = (config.baseURL ?? DEFAULT_GROK_BASE_URL).replace(/\/+$/, "");
16
- this.defaultHeaders = config.defaultHeaders ?? {};
17
- }
18
- async generateSpeech(options) {
19
- const { logger } = options;
20
- const { model, text, voice, format, modelOptions } = options;
21
- logger.request(`activity=generateSpeech provider=grok model=${model}`, {
22
- provider: "grok",
23
- model
24
- });
25
- const { body, codec, sampleRateForContentType } = buildTTSRequestBody({
26
- text,
27
- voice,
28
- format,
29
- modelOptions
30
- });
31
- try {
32
- const response = await fetch(`${this.baseURL}/tts`, {
33
- method: "POST",
34
- headers: {
35
- // `defaultHeaders` first so the adapter's Authorization / Content-Type
36
- // always win — otherwise a caller-supplied `Authorization` header
37
- // could silently clobber the bearer token.
38
- ...this.defaultHeaders,
39
- Authorization: `Bearer ${this.apiKey}`,
40
- "Content-Type": "application/json"
41
- },
42
- body: JSON.stringify(body)
43
- });
44
- if (!response.ok) {
45
- const errorText = await response.text();
46
- throw new Error(
47
- `Grok TTS request failed: ${response.status} ${errorText}`
48
- );
49
- }
50
- const arrayBuffer = await response.arrayBuffer();
51
- const audio = arrayBufferToBase64(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
- }
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
- const { text, voice, format, modelOptions } = options;
70
- const codec = pickCodec(modelOptions?.codec, format);
71
- const callerSampleRate = modelOptions?.sample_rate;
72
- const pcmDefault = 24e3;
73
- const needsRateInContentType = codec === "pcm";
74
- const outputFormat = { codec };
75
- if (callerSampleRate !== void 0) {
76
- outputFormat.sample_rate = callerSampleRate;
77
- } else if (needsRateInContentType) {
78
- outputFormat.sample_rate = pcmDefault;
79
- }
80
- if (codec === "mp3" && modelOptions?.bit_rate !== void 0) {
81
- outputFormat.bit_rate = modelOptions.bit_rate;
82
- }
83
- const sampleRateForContentType = callerSampleRate ?? pcmDefault;
84
- const body = {
85
- text,
86
- voice_id: voice ?? "eve",
87
- language: modelOptions?.language ?? "en",
88
- output_format: outputFormat
89
- };
90
- if (modelOptions?.optimize_streaming_latency !== void 0) {
91
- body.optimize_streaming_latency = modelOptions.optimize_streaming_latency;
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
- if (codecOverride) return codecOverride;
100
- if (!format) return "mp3";
101
- switch (format) {
102
- case "mp3":
103
- case "wav":
104
- case "pcm":
105
- return format;
106
- case "flac":
107
- case "opus":
108
- case "aac":
109
- return "mp3";
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
- switch (codec) {
116
- case "mp3":
117
- return "audio/mpeg";
118
- case "wav":
119
- return "audio/wav";
120
- case "pcm":
121
- return `audio/L16;rate=${sampleRate}`;
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
- return new GrokSpeechAdapter({ apiKey, ...config }, model);
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
- const apiKey = getGrokApiKeyFromEnv();
133
- return createGrokSpeech(model, apiKey, config);
154
+ return createGrokSpeech(model, getGrokApiKeyFromEnv(), config);
134
155
  }
135
- export {
136
- GrokSpeechAdapter,
137
- buildTTSRequestBody,
138
- createGrokSpeech,
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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"}