@juspay/neurolink 12.47.14 → 12.48.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
  3. package/dist/browser/neurolink.min.js +578 -565
  4. package/dist/cli/factories/commandFactory.d.ts +7 -0
  5. package/dist/cli/factories/commandFactory.js +26 -0
  6. package/dist/cli/loop/optionsSchema.js +9 -0
  7. package/dist/context/budgetChecker.js +54 -2
  8. package/dist/context/contextDecision.js +2 -0
  9. package/dist/context/toolPairRepair.d.ts +12 -0
  10. package/dist/context/toolPairRepair.js +29 -3
  11. package/dist/core/baseProvider.d.ts +92 -3
  12. package/dist/core/baseProvider.js +254 -10
  13. package/dist/core/conversationMemoryManager.js +17 -2
  14. package/dist/core/loopEngine.js +1 -1
  15. package/dist/core/modules/MessageBuilder.js +11 -3
  16. package/dist/core/modules/TelemetryHandler.d.ts +2 -2
  17. package/dist/core/modules/ToolsManager.d.ts +35 -0
  18. package/dist/core/modules/ToolsManager.js +159 -55
  19. package/dist/core/nativeGenerateLoop.d.ts +58 -1
  20. package/dist/core/nativeGenerateLoop.js +277 -3
  21. package/dist/core/redisConversationMemoryManager.js +10 -3
  22. package/dist/core/resolveRequestKind.d.ts +24 -4
  23. package/dist/core/resolveRequestKind.js +31 -4
  24. package/dist/core/toolExecutionRecorder.d.ts +6 -0
  25. package/dist/core/toolExecutionRecorder.js +24 -2
  26. package/dist/core/toolRoutingDecision.js +1 -0
  27. package/dist/factories/mediaHandlerCatalog.js +5 -0
  28. package/dist/factories/providerDescriptors.js +15 -0
  29. package/dist/files/fileTools.d.ts +1 -1
  30. package/dist/index.d.ts +5 -2
  31. package/dist/index.js +9 -2
  32. package/dist/memory/condensationPrompt.d.ts +8 -0
  33. package/dist/memory/condensationPrompt.js +29 -0
  34. package/dist/memory/hippocampusInitializer.d.ts +14 -2
  35. package/dist/memory/hippocampusInitializer.js +64 -3
  36. package/dist/neurolink.d.ts +252 -3
  37. package/dist/neurolink.js +898 -43
  38. package/dist/providers/amazonBedrock/client.js +6 -0
  39. package/dist/providers/amazonSagemaker.js +14 -1
  40. package/dist/providers/anthropic/client.js +148 -19
  41. package/dist/providers/anthropic/loopAdapter.js +2 -2
  42. package/dist/providers/googleAiStudio/client.js +2 -1
  43. package/dist/providers/openaiChatCompletionsBase.js +103 -23
  44. package/dist/providers/systemOneDecision.d.ts +6 -0
  45. package/dist/providers/systemOneDecision.js +14 -63
  46. package/dist/providers/typesafe.d.ts +6 -0
  47. package/dist/providers/typesafe.js +6 -0
  48. package/dist/rag/retrieval/searchDecision.js +1 -0
  49. package/dist/routing/classifierRouter.js +6 -5
  50. package/dist/routing/classifierStrategies.js +8 -1
  51. package/dist/types/classifierRouter.d.ts +16 -3
  52. package/dist/types/common.d.ts +2 -0
  53. package/dist/types/config.d.ts +9 -0
  54. package/dist/types/context.d.ts +8 -1
  55. package/dist/types/conversation.d.ts +87 -0
  56. package/dist/types/decision.d.ts +210 -1
  57. package/dist/types/decision.js +9 -1
  58. package/dist/types/dispatch.d.ts +11 -4
  59. package/dist/types/generate.d.ts +118 -54
  60. package/dist/types/loopEngine.d.ts +7 -2
  61. package/dist/types/memory.d.ts +177 -9
  62. package/dist/types/openaiCompatible.d.ts +11 -3
  63. package/dist/types/stream.d.ts +13 -16
  64. package/dist/types/stt.d.ts +58 -1
  65. package/dist/types/tools.d.ts +14 -0
  66. package/dist/types/tools.js +8 -0
  67. package/dist/types/tts.d.ts +108 -0
  68. package/dist/types/voice.d.ts +41 -4
  69. package/dist/utils/decisionAnswers.d.ts +31 -1
  70. package/dist/utils/decisionAnswers.js +114 -0
  71. package/dist/utils/decisionLimits.d.ts +39 -0
  72. package/dist/utils/decisionLimits.js +116 -0
  73. package/dist/utils/logger.d.ts +7 -0
  74. package/dist/utils/logger.js +9 -0
  75. package/dist/utils/messageBuilder.d.ts +45 -1
  76. package/dist/utils/messageBuilder.js +393 -16
  77. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  78. package/dist/utils/multimodalOptionsBuilder.js +1 -0
  79. package/dist/utils/speechText.d.ts +41 -0
  80. package/dist/utils/speechText.js +364 -0
  81. package/dist/utils/toolResultStatus.d.ts +26 -0
  82. package/dist/utils/toolResultStatus.js +73 -0
  83. package/dist/utils/ttsProcessor.d.ts +29 -0
  84. package/dist/utils/ttsProcessor.js +84 -2
  85. package/dist/voice/index.d.ts +1 -0
  86. package/dist/voice/index.js +3 -0
  87. package/dist/voice/providers/ElevenLabsSTT.d.ts +42 -0
  88. package/dist/voice/providers/ElevenLabsSTT.js +351 -0
  89. package/dist/voice/providers/ElevenLabsTTS.d.ts +81 -1
  90. package/dist/voice/providers/ElevenLabsTTS.js +415 -74
  91. package/docs-site/static/search-index.json +48 -33
  92. package/package.json +5 -2
  93. package/dist/utils/toolEndEmitter.d.ts +0 -25
  94. package/dist/utils/toolEndEmitter.js +0 -64
@@ -0,0 +1,42 @@
1
+ /**
2
+ * ElevenLabs Speech-to-Text (Scribe) Handler
3
+ *
4
+ * Batch transcription via `POST /v1/speech-to-text`. Shares the
5
+ * `ELEVENLABS_API_KEY` / `ELEVENLABS_BASE_URL` environment with the ElevenLabs
6
+ * TTS and music handlers — same account, different endpoint.
7
+ *
8
+ * @module voice/providers/ElevenLabsSTT
9
+ */
10
+ import type { TTSAudioFormat, STTHandler, STTLanguage, STTOptions, STTResult } from "../../types/index.js";
11
+ /**
12
+ * ElevenLabs Scribe Speech-to-Text Handler
13
+ *
14
+ * Batch-only (no `transcribeStream`). Word timings and speaker labels come
15
+ * back on every response when the audio has them; `diarize` asks Scribe to
16
+ * attribute words to speakers.
17
+ *
18
+ * @see https://elevenlabs.io/docs/api-reference/speech-to-text/convert
19
+ */
20
+ export declare class ElevenLabsSTT implements STTHandler {
21
+ private readonly apiKey;
22
+ private readonly baseUrl;
23
+ /** Scribe accepts files up to 1 GB / ~4.5 hours; 2 hours is a conservative cap. */
24
+ readonly maxAudioDuration = 7200;
25
+ readonly supportsStreaming = false;
26
+ constructor(apiKey?: string);
27
+ isConfigured(): boolean;
28
+ getSupportedFormats(): TTSAudioFormat[];
29
+ getSupportedLanguages(): Promise<STTLanguage[]>;
30
+ transcribe(audio: Buffer | ArrayBuffer, options?: STTOptions): Promise<STTResult>;
31
+ private toSTTResult;
32
+ /** A finite number clamped to [0, 1]; `undefined` for anything else. */
33
+ private static unitInterval;
34
+ /** Mean `exp(logprob)` over the words that carry a finite one, clamped to [0, 1]; `undefined` when none does. */
35
+ private static meanWordConfidence;
36
+ /** Scribe takes ISO 639-1/3 codes; a BCP-47 tag like `en-US` or `pt_BR` is reduced to its primary subtag. */
37
+ private toScribeLanguageCode;
38
+ private getMimeType;
39
+ /** 429 and 5xx are worth a resend; every other status is a fixed error. */
40
+ private static isRetriableStatus;
41
+ private static normalizeBaseUrl;
42
+ }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * ElevenLabs Speech-to-Text (Scribe) Handler
3
+ *
4
+ * Batch transcription via `POST /v1/speech-to-text`. Shares the
5
+ * `ELEVENLABS_API_KEY` / `ELEVENLABS_BASE_URL` environment with the ElevenLabs
6
+ * TTS and music handlers — same account, different endpoint.
7
+ *
8
+ * @module voice/providers/ElevenLabsSTT
9
+ */
10
+ import { logger } from "../../utils/logger.js";
11
+ import { STTError } from "../errors.js";
12
+ const PROVIDER = "elevenlabs-stt";
13
+ const DEFAULT_BASE_URL = "https://api.elevenlabs.io/v1";
14
+ const DEFAULT_TIMEOUT_MS = 60_000;
15
+ /** Largest delay `setTimeout` honours; above it the timer fires at once. */
16
+ const MAX_TIMEOUT_MS = 2_147_483_647;
17
+ /** `scribe_v1` is deprecated in ElevenLabs' model list; v2 is the current batch model. */
18
+ const DEFAULT_MODEL = "scribe_v2";
19
+ /**
20
+ * ElevenLabs Scribe Speech-to-Text Handler
21
+ *
22
+ * Batch-only (no `transcribeStream`). Word timings and speaker labels come
23
+ * back on every response when the audio has them; `diarize` asks Scribe to
24
+ * attribute words to speakers.
25
+ *
26
+ * @see https://elevenlabs.io/docs/api-reference/speech-to-text/convert
27
+ */
28
+ export class ElevenLabsSTT {
29
+ apiKey;
30
+ baseUrl;
31
+ /** Scribe accepts files up to 1 GB / ~4.5 hours; 2 hours is a conservative cap. */
32
+ maxAudioDuration = 7200;
33
+ supportsStreaming = false;
34
+ constructor(apiKey) {
35
+ const resolvedKey = (apiKey ?? process.env.ELEVENLABS_API_KEY ?? "").trim();
36
+ this.apiKey = resolvedKey.length > 0 ? resolvedKey : null;
37
+ this.baseUrl = ElevenLabsSTT.normalizeBaseUrl(process.env.ELEVENLABS_BASE_URL ?? DEFAULT_BASE_URL);
38
+ }
39
+ isConfigured() {
40
+ return this.apiKey !== null;
41
+ }
42
+ getSupportedFormats() {
43
+ return [
44
+ "mp3",
45
+ "wav",
46
+ "ogg",
47
+ "opus",
48
+ "m4a",
49
+ "flac",
50
+ "webm",
51
+ "mp4",
52
+ "mpeg",
53
+ "mpga",
54
+ ];
55
+ }
56
+ async getSupportedLanguages() {
57
+ // Scribe auto-detects among 99 languages; this is the commonly requested subset.
58
+ return [
59
+ {
60
+ code: "en",
61
+ name: "English",
62
+ supportsDiarization: true,
63
+ supportsPunctuation: true,
64
+ },
65
+ {
66
+ code: "es",
67
+ name: "Spanish",
68
+ supportsDiarization: true,
69
+ supportsPunctuation: true,
70
+ },
71
+ {
72
+ code: "fr",
73
+ name: "French",
74
+ supportsDiarization: true,
75
+ supportsPunctuation: true,
76
+ },
77
+ {
78
+ code: "de",
79
+ name: "German",
80
+ supportsDiarization: true,
81
+ supportsPunctuation: true,
82
+ },
83
+ {
84
+ code: "it",
85
+ name: "Italian",
86
+ supportsDiarization: true,
87
+ supportsPunctuation: true,
88
+ },
89
+ {
90
+ code: "pt",
91
+ name: "Portuguese",
92
+ supportsDiarization: true,
93
+ supportsPunctuation: true,
94
+ },
95
+ {
96
+ code: "hi",
97
+ name: "Hindi",
98
+ supportsDiarization: true,
99
+ supportsPunctuation: true,
100
+ },
101
+ {
102
+ code: "ja",
103
+ name: "Japanese",
104
+ supportsDiarization: true,
105
+ supportsPunctuation: true,
106
+ },
107
+ {
108
+ code: "ko",
109
+ name: "Korean",
110
+ supportsDiarization: true,
111
+ supportsPunctuation: true,
112
+ },
113
+ {
114
+ code: "zh",
115
+ name: "Chinese",
116
+ supportsDiarization: true,
117
+ supportsPunctuation: true,
118
+ },
119
+ {
120
+ code: "ar",
121
+ name: "Arabic",
122
+ supportsDiarization: true,
123
+ supportsPunctuation: true,
124
+ },
125
+ ];
126
+ }
127
+ async transcribe(audio, options = {}) {
128
+ if (!this.apiKey) {
129
+ throw STTError.providerNotConfigured(PROVIDER);
130
+ }
131
+ const audioBuffer = Buffer.isBuffer(audio) ? audio : Buffer.from(audio);
132
+ if (audioBuffer.length === 0) {
133
+ throw STTError.audioEmpty(PROVIDER);
134
+ }
135
+ const elevenOptions = options;
136
+ const model = elevenOptions.model ?? DEFAULT_MODEL;
137
+ const format = options.format ?? "wav";
138
+ const startTime = Date.now();
139
+ try {
140
+ const formData = new FormData();
141
+ formData.append("file", new Blob([new Uint8Array(audioBuffer)], {
142
+ type: this.getMimeType(format),
143
+ }), `audio.${format}`);
144
+ formData.append("model_id", model);
145
+ // Only forward a language the caller actually set: Scribe auto-detects
146
+ // otherwise, and an injected default would pin every request to one
147
+ // language. (STTProcessor passes options through untouched, so a caller
148
+ // who leaves `language` unset reaches here with it unset.)
149
+ if (options.language) {
150
+ formData.append("language_code", this.toScribeLanguageCode(options.language));
151
+ }
152
+ // ElevenLabs defaults this to true; a plain transcript is the better
153
+ // default for text that feeds a prompt.
154
+ formData.append("tag_audio_events", String(elevenOptions.tagAudioEvents ?? false));
155
+ const diarize = options.speakerDiarization ?? options.diarization ?? false;
156
+ formData.append("diarize", String(diarize));
157
+ if (diarize && options.speakerCount !== undefined) {
158
+ formData.append("num_speakers", String(options.speakerCount));
159
+ }
160
+ const baseUrl = elevenOptions.baseUrl
161
+ ? ElevenLabsSTT.normalizeBaseUrl(elevenOptions.baseUrl)
162
+ : this.baseUrl;
163
+ const timeoutMs = typeof elevenOptions.timeoutMs === "number" &&
164
+ Number.isFinite(elevenOptions.timeoutMs) &&
165
+ elevenOptions.timeoutMs > 0
166
+ ? Math.min(elevenOptions.timeoutMs, MAX_TIMEOUT_MS)
167
+ : DEFAULT_TIMEOUT_MS;
168
+ // The timer covers the response headers AND the JSON body: a server
169
+ // that stalls mid-body used to hold the transcription — and the
170
+ // generate() it fed — until undici's own ~5-minute body timeout, long
171
+ // after `timeoutMs` had passed.
172
+ const controller = new AbortController();
173
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
174
+ const timedOut = (err) => err instanceof Error && err.name === "AbortError"
175
+ ? STTError.transcriptionFailed(`ElevenLabs STT request timed out after ${timeoutMs}ms`, PROVIDER, err)
176
+ : undefined;
177
+ let data;
178
+ try {
179
+ let response;
180
+ try {
181
+ response = await fetch(`${baseUrl}/speech-to-text`, {
182
+ method: "POST",
183
+ headers: { "xi-api-key": this.apiKey },
184
+ body: formData,
185
+ signal: controller.signal,
186
+ });
187
+ }
188
+ catch (fetchErr) {
189
+ throw timedOut(fetchErr) ?? fetchErr;
190
+ }
191
+ if (!response.ok) {
192
+ // A body that PARSES to `null` isn't caught by `.catch()`, and
193
+ // `.detail` on `null` throws — see ElevenLabsTTS.ts's identical fix.
194
+ const errorData = (await response.json().catch(() => null)) ??
195
+ Object.create(null);
196
+ const detail = errorData.detail;
197
+ const errorMessage = (typeof detail === "string" ? detail : detail?.message) ||
198
+ `HTTP ${response.status}`;
199
+ const failure = STTError.transcriptionFailed(errorMessage, PROVIDER);
200
+ // `transcriptionFailed` defaults to retriable; a 4xx (bad audio, bad
201
+ // key, unsupported option) can never succeed on resend.
202
+ throw new STTError({
203
+ code: failure.code,
204
+ message: failure.message,
205
+ category: failure.category,
206
+ severity: failure.severity,
207
+ retriable: ElevenLabsSTT.isRetriableStatus(response.status),
208
+ context: { provider: PROVIDER, status: response.status },
209
+ });
210
+ }
211
+ try {
212
+ data = (await response.json());
213
+ }
214
+ catch (bodyErr) {
215
+ throw (timedOut(bodyErr) ??
216
+ STTError.transcriptionFailed(`ElevenLabs STT response body failed: ${bodyErr instanceof Error ? bodyErr.message : String(bodyErr)}`, PROVIDER, bodyErr instanceof Error ? bodyErr : undefined));
217
+ }
218
+ }
219
+ finally {
220
+ clearTimeout(timeoutId);
221
+ }
222
+ const latency = Date.now() - startTime;
223
+ const result = this.toSTTResult(data, model, latency);
224
+ logger.info(`[ElevenLabsSTTHandler] Transcribed ${audioBuffer.length} bytes in ${latency}ms`);
225
+ return result;
226
+ }
227
+ catch (err) {
228
+ if (err instanceof STTError) {
229
+ throw err;
230
+ }
231
+ const errorMessage = err instanceof Error ? err.message : String(err || "Unknown error");
232
+ logger.error(`[ElevenLabsSTTHandler] Transcription failed: ${errorMessage}`);
233
+ throw STTError.transcriptionFailed(errorMessage, PROVIDER, err instanceof Error ? err : undefined);
234
+ }
235
+ }
236
+ toSTTResult(data, model, latency) {
237
+ const spoken = (data.words ?? []).filter((word) => typeof word.text === "string" &&
238
+ word.type !== "spacing" &&
239
+ word.type !== "audio_event" &&
240
+ word.text.trim().length > 0);
241
+ // `confidence` is required on STTResult, so it is always a number — but
242
+ // which number is recorded, never guessed at. The transcript confidence
243
+ // proper is the mean per-word `exp(logprob)`, the same conversion
244
+ // Whisper's `avg_logprob` gets. When no word carries a logprob the field
245
+ // falls back to `language_probability` — how sure Scribe is about the
246
+ // LANGUAGE, not the words: garbled and clean audio both score ~0.99 on
247
+ // it — and when that is missing too it is 0. `metadata.confidenceSource`
248
+ // names the one used, and `languageProbability` is always kept under its
249
+ // own name so a caller can tell the two apart.
250
+ const wordConfidence = ElevenLabsSTT.meanWordConfidence(spoken);
251
+ const languageProbability = ElevenLabsSTT.unitInterval(data.language_probability);
252
+ let confidence;
253
+ let confidenceSource;
254
+ if (wordConfidence !== undefined) {
255
+ confidence = wordConfidence;
256
+ confidenceSource = "word_logprobs";
257
+ }
258
+ else if (languageProbability !== undefined) {
259
+ confidence = languageProbability;
260
+ confidenceSource = "language_probability";
261
+ }
262
+ else {
263
+ confidence = 0;
264
+ confidenceSource = "none";
265
+ }
266
+ const result = {
267
+ text: data.text ?? "",
268
+ confidence,
269
+ language: data.language_code,
270
+ metadata: {
271
+ latency,
272
+ provider: PROVIDER,
273
+ model,
274
+ confidenceSource,
275
+ languageProbability: data.language_probability,
276
+ },
277
+ };
278
+ if (spoken.length > 0) {
279
+ const speakers = new Set();
280
+ result.words = spoken.map((word) => {
281
+ if (word.speaker_id) {
282
+ speakers.add(word.speaker_id);
283
+ }
284
+ return {
285
+ word: word.text,
286
+ startTime: word.start,
287
+ endTime: word.end,
288
+ speaker: word.speaker_id,
289
+ };
290
+ });
291
+ if (speakers.size > 0) {
292
+ result.speakers = [...speakers];
293
+ }
294
+ const last = spoken[spoken.length - 1];
295
+ if (typeof last?.end === "number") {
296
+ result.duration = last.end;
297
+ }
298
+ }
299
+ return result;
300
+ }
301
+ /** A finite number clamped to [0, 1]; `undefined` for anything else. */
302
+ static unitInterval(value) {
303
+ return typeof value === "number" && Number.isFinite(value)
304
+ ? Math.min(1, Math.max(0, value))
305
+ : undefined;
306
+ }
307
+ /** Mean `exp(logprob)` over the words that carry a finite one, clamped to [0, 1]; `undefined` when none does. */
308
+ static meanWordConfidence(words) {
309
+ let sum = 0;
310
+ let count = 0;
311
+ for (const word of words) {
312
+ if (typeof word.logprob === "number" && Number.isFinite(word.logprob)) {
313
+ sum += Math.exp(word.logprob);
314
+ count += 1;
315
+ }
316
+ }
317
+ return count === 0 ? undefined : Math.min(1, Math.max(0, sum / count));
318
+ }
319
+ /** Scribe takes ISO 639-1/3 codes; a BCP-47 tag like `en-US` or `pt_BR` is reduced to its primary subtag. */
320
+ toScribeLanguageCode(language) {
321
+ return (language.trim().split(/[-_]/, 1)[0] ?? language).toLowerCase();
322
+ }
323
+ getMimeType(format) {
324
+ const mimeTypes = {
325
+ mp3: "audio/mpeg",
326
+ mpeg: "audio/mpeg",
327
+ mpga: "audio/mpeg",
328
+ wav: "audio/wav",
329
+ ogg: "audio/ogg",
330
+ opus: "audio/ogg",
331
+ m4a: "audio/mp4",
332
+ mp4: "audio/mp4",
333
+ flac: "audio/flac",
334
+ webm: "audio/webm",
335
+ };
336
+ return mimeTypes[format] ?? "application/octet-stream";
337
+ }
338
+ /** 429 and 5xx are worth a resend; every other status is a fixed error. */
339
+ static isRetriableStatus(status) {
340
+ return status === 429 || status >= 500;
341
+ }
342
+ static normalizeBaseUrl(baseUrl) {
343
+ // A loop rather than /\/+$/: the input is caller-supplied and CodeQL
344
+ // flags that regex as polynomial on long runs of "/".
345
+ let end = baseUrl.length;
346
+ while (end > 0 && baseUrl.charCodeAt(end - 1) === 47) {
347
+ end--;
348
+ }
349
+ return baseUrl.slice(0, end);
350
+ }
351
+ }
@@ -16,6 +16,10 @@ import type { TTSHandler, TTSOptions, TTSResult, TTSVoice } from "../../types/in
16
16
  export declare class ElevenLabsTTS implements TTSHandler {
17
17
  private readonly apiKey;
18
18
  private readonly baseUrl;
19
+ /** `ELEVENLABS_VOICE_ID`, else Rachel; a per-request `voice` still wins. */
20
+ private readonly defaultVoiceId;
21
+ /** `ELEVENLABS_MODEL`, else `eleven_multilingual_v2`; a per-request `model` still wins. */
22
+ private readonly defaultModel;
19
23
  private voicesCache;
20
24
  private static readonly CACHE_TTL_MS;
21
25
  /**
@@ -23,15 +27,91 @@ export declare class ElevenLabsTTS implements TTSHandler {
23
27
  */
24
28
  readonly maxTextLength = 5000;
25
29
  constructor(apiKey?: string);
30
+ /** A trimmed, non-empty environment value, else `undefined`. */
31
+ private static envValue;
26
32
  isConfigured(): boolean;
27
33
  getVoices(languageCode?: string): Promise<TTSVoice[]>;
28
34
  synthesize(text: string, options?: TTSOptions): Promise<TTSResult>;
35
+ /**
36
+ * Issue the synthesis request, retrying on 429 / 5xx / network failure /
37
+ * per-attempt timeout up to `retries` more times. A `Retry-After` header
38
+ * sets the pause before the next attempt (capped); otherwise exponential
39
+ * backoff from 500 ms. Any other failure — 4xx, or a body that is not audio
40
+ * — is thrown on the first attempt.
41
+ *
42
+ * Total wall-clock is bounded by `timeoutMs × (1 + retries)` plus the pauses
43
+ * between attempts; `BaseProvider`'s outer synthesis timeout is raised to at
44
+ * least that figure (see `resolveTTSHandlerBudgetMs`).
45
+ *
46
+ * `signal` is the caller's cancellation: once it fires, the attempt in
47
+ * flight is aborted and no further attempt is made. Without it, a caller
48
+ * that had already given up (its own timeout, an aborted request) left the
49
+ * loop to pause and issue a second, billable request nobody consumed.
50
+ */
51
+ private fetchWithRetry;
52
+ /**
53
+ * A backoff pause that ends as soon as `signal` fires, so a cancelled
54
+ * caller never waits it out. The timer is owned here and cleared on the
55
+ * way out: racing a bare `delay()` against the signal settled the promise
56
+ * at the abort but left the backoff timer armed for up to `RETRY_AFTER_CAP_MS`,
57
+ * holding the event loop open after the caller had already been answered.
58
+ */
59
+ private static pause;
60
+ /** The non-retriable error a caller-cancelled synthesis surfaces as. */
61
+ private static cancelledError;
62
+ /**
63
+ * One attempt, end to end: the per-attempt timer covers the response
64
+ * headers AND the audio body, so a stalled or truncated body is a timed-out
65
+ * attempt the retry loop can resend rather than a hang that only the outer
66
+ * provider budget ends. The caller's `signal` aborts the same controller,
67
+ * and an abort it caused is reported as a cancellation, not a timeout.
68
+ */
69
+ private fetchOnce;
70
+ /** Map an abort from the per-attempt timer to a retriable timeout error. */
71
+ private static asAttemptError;
72
+ private static isRetriableStatus;
73
+ /**
74
+ * 429 / 5xx, a per-attempt timeout or broken body (already a retriable
75
+ * `TTSError`), or a transport failure. Any other throw — a `TypeError` for
76
+ * an invalid header value or URL, thrown before a request exists — is
77
+ * permanent and is not retried; it used to be, on the grounds that it was
78
+ * an `Error`.
79
+ */
80
+ private static isRetriableFailure;
81
+ /**
82
+ * A socket-level failure: undici wraps one as `TypeError("fetch failed")`
83
+ * with the system error on `cause`, and the codes are checked on both.
84
+ */
85
+ private static isTransportError;
86
+ private static errorCode;
87
+ /** A BCP-47 tag reduced to its ISO 639-1 primary subtag: `en-US` → `en`, `pt_BR` → `pt`. */
88
+ private static toIso639Primary;
89
+ /** One of `allowed`, else `fallback` with a warning — never an unlisted value in `output_format`. */
90
+ private static resolveBitrate;
91
+ /** `Retry-After` as delta-seconds or an HTTP-date, in milliseconds. */
92
+ private static parseRetryAfter;
93
+ /** A positive finite `timeoutMs`, clamped to the largest delay a timer honours; otherwise the default. */
94
+ private static resolveTimeoutMs;
95
+ private static resolveRetries;
96
+ private static normalizeBaseUrl;
97
+ /**
98
+ * Assemble `voice_settings`. The camelCase options are the documented
99
+ * surface; `voiceSettings` passes raw fields through and wins on conflict.
100
+ * `speed` comes from the generic `TTSOptions.speed` (0.25–4.0) clamped into
101
+ * ElevenLabs' accepted 0.7–1.2 — clamped with a warning rather than
102
+ * rejected, because the generic validator has already accepted the value.
103
+ */
104
+ private buildVoiceSettings;
29
105
  /**
30
106
  * Map gender string to standard type
31
107
  */
32
108
  private mapGender;
33
109
  /**
34
- * Map TTSAudioFormat to ElevenLabs output format
110
+ * Map TTSAudioFormat to an ElevenLabs `output_format`.
111
+ *
112
+ * ElevenLabs has no `ogg_*` family: Ogg/Opus is `opus_48000_<kbps>`. Both
113
+ * `ogg` and `opus` map there. `wav` maps to raw `pcm_44100` (headerless —
114
+ * see `effectiveFormat`). Anything else falls back to `mp3_44100_128`.
35
115
  */
36
116
  private mapFormat;
37
117
  /**