@happyvertical/speech 0.110.1 → 0.111.0

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 (53) hide show
  1. package/AGENT.md +3 -0
  2. package/README.md +46 -3
  3. package/dist/adapters/local/audio.d.ts.map +1 -1
  4. package/dist/adapters/local/worker.d.ts +8 -5
  5. package/dist/adapters/local/worker.d.ts.map +1 -1
  6. package/dist/adapters/openai-compatible-transcriber.d.ts +3 -0
  7. package/dist/adapters/openai-compatible-transcriber.d.ts.map +1 -1
  8. package/dist/adapters/openai-compatible.d.ts +3 -0
  9. package/dist/adapters/openai-compatible.d.ts.map +1 -1
  10. package/dist/adapters/openai-realtime.d.ts +8 -0
  11. package/dist/adapters/openai-realtime.d.ts.map +1 -1
  12. package/dist/adapters/qwen3.d.ts +6 -0
  13. package/dist/adapters/qwen3.d.ts.map +1 -1
  14. package/dist/adapters/studio-server.d.ts +11 -0
  15. package/dist/adapters/studio-server.d.ts.map +1 -1
  16. package/dist/adapters/voxtral-realtime.d.ts +8 -0
  17. package/dist/adapters/voxtral-realtime.d.ts.map +1 -1
  18. package/dist/browser.js +2 -2
  19. package/dist/chunks/errors-CzsR_5dE.js +122 -0
  20. package/dist/chunks/errors-CzsR_5dE.js.map +1 -0
  21. package/dist/chunks/{registry-DFSF3WoM.js → registry-DhB2589P.js} +5 -5
  22. package/dist/chunks/registry-DhB2589P.js.map +1 -0
  23. package/dist/chunks/{wav-vzrhhZbu.js → wav-PxVZJX9V.js} +15 -5
  24. package/dist/chunks/{wav-vzrhhZbu.js.map → wav-PxVZJX9V.js.map} +1 -1
  25. package/dist/chunks/{websocket-YbAfY6YI.js → websocket-DLZLLyD9.js} +2 -2
  26. package/dist/chunks/{websocket-YbAfY6YI.js.map → websocket-DLZLLyD9.js.map} +1 -1
  27. package/dist/conversation-server.js +2 -2
  28. package/dist/conversation.js +1 -1
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +302 -20
  32. package/dist/index.js.map +1 -1
  33. package/dist/local.js +21 -12
  34. package/dist/local.js.map +1 -1
  35. package/dist/pcm.js +2 -2
  36. package/dist/shared/audio.d.ts.map +1 -1
  37. package/dist/shared/errors.d.ts +48 -1
  38. package/dist/shared/errors.d.ts.map +1 -1
  39. package/dist/shared/http.d.ts +3 -0
  40. package/dist/shared/http.d.ts.map +1 -1
  41. package/dist/shared/probe.d.ts +90 -0
  42. package/dist/shared/probe.d.ts.map +1 -0
  43. package/dist/shared/streaming-factory.d.ts.map +1 -1
  44. package/dist/shared/streaming-types.d.ts +10 -1
  45. package/dist/shared/streaming-types.d.ts.map +1 -1
  46. package/dist/shared/types.d.ts +14 -0
  47. package/dist/shared/types.d.ts.map +1 -1
  48. package/dist/shared/wav.d.ts +12 -1
  49. package/dist/shared/wav.d.ts.map +1 -1
  50. package/package.json +1 -1
  51. package/dist/chunks/errors-Cs0vX-mV.js +0 -64
  52. package/dist/chunks/errors-Cs0vX-mV.js.map +0 -1
  53. package/dist/chunks/registry-DFSF3WoM.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,6 +1,172 @@
1
- import { a as SpeechProviderError, i as SpeechError, n as PcmCaptureError, o as WavFormatError, r as SpeechConfigurationError, t as InvalidSpeechAdapterError } from "./chunks/errors-Cs0vX-mV.js";
2
- import { i as toWebSocketUrl, n as messageDataToText, r as resolveWebSocketFactory, t as isBrowserRuntime } from "./chunks/websocket-YbAfY6YI.js";
3
- import { _ as mimeTypeToAudioExtension, a as TRANSCRIBER_ENV_KEYS, c as hasTranscriberEnv, d as parseOptionalInteger, f as readEnv, g as DEFAULT_MAX_AUDIO_BYTES, h as reportSpeechUsage, i as registeredOptionalTranscribers, l as mergeHeaderInits, m as audioSecondsFromProviderUsage, n as optionalTranscriberEntry, o as defaultEnv, p as resolveTranscriberConfig, s as hasAnyEnv, t as getOptionalTranscriberFactory, u as parseHeadersEnv, v as normalizeAudioInput, y as normalizeMimeType } from "./chunks/registry-DFSF3WoM.js";
1
+ import { a as SpeechError, c as SpeechProviderUnavailableError, i as SpeechConfigurationError, l as WavFormatError, n as PcmCaptureError, o as SpeechInvalidAudioError, r as SpeechAudioTooLargeError, s as SpeechProviderError, t as InvalidSpeechAdapterError } from "./chunks/errors-CzsR_5dE.js";
2
+ import { i as toWebSocketUrl, n as messageDataToText, r as resolveWebSocketFactory, t as isBrowserRuntime } from "./chunks/websocket-DLZLLyD9.js";
3
+ import { _ as mimeTypeToAudioExtension, a as TRANSCRIBER_ENV_KEYS, c as hasTranscriberEnv, d as parseOptionalInteger, f as readEnv, g as DEFAULT_MAX_AUDIO_BYTES, h as reportSpeechUsage, i as registeredOptionalTranscribers, l as mergeHeaderInits, m as audioSecondsFromProviderUsage, n as optionalTranscriberEntry, o as defaultEnv, p as resolveTranscriberConfig, s as hasAnyEnv, t as getOptionalTranscriberFactory, u as parseHeadersEnv, v as normalizeAudioInput, y as normalizeMimeType } from "./chunks/registry-DhB2589P.js";
4
+ //#region src/shared/probe.ts
5
+ /**
6
+ * Readiness probes (#1345): check that a configured speech endpoint answers,
7
+ * accepts the credentials, and (where the server lists models) serves the
8
+ * configured model, without sending audio or text to synthesize.
9
+ */
10
+ var DEFAULT_SPEECH_PROBE_TIMEOUT_MS = 1e4;
11
+ /** A model list from `GET /models` is read up to this many bytes. */
12
+ var MAX_MODEL_LIST_BYTES = 4 * 1024 * 1024;
13
+ /**
14
+ * Derives the OpenAI-style model list URL from a resolved endpoint: the
15
+ * trailing `/<resource>` is replaced with `/models` (the query is kept). An
16
+ * endpoint that does not end in `resource` gets `models` resolved beside it.
17
+ */
18
+ function modelsUrlFor(endpoint, resource) {
19
+ const url = new URL(endpoint);
20
+ const suffix = `/${resource.replace(/^\/+|\/+$/g, "")}`;
21
+ const pathname = url.pathname.replace(/\/+$/, "");
22
+ url.pathname = pathname.endsWith(suffix) ? `${pathname.slice(0, -suffix.length)}/models` : `${pathname.replace(/\/[^/]*$/, "")}/models`;
23
+ url.hash = "";
24
+ return url.toString();
25
+ }
26
+ /** `ws:`/`wss:` to `http:`/`https:`; other URLs are returned unchanged. */
27
+ function toHttpUrl(url) {
28
+ const parsed = new URL(url);
29
+ if (parsed.protocol === "ws:") parsed.protocol = "http:";
30
+ else if (parsed.protocol === "wss:") parsed.protocol = "https:";
31
+ return parsed.toString();
32
+ }
33
+ function unsupportedProbe(adapter, message) {
34
+ return {
35
+ ok: false,
36
+ adapter,
37
+ reason: "unsupported",
38
+ message,
39
+ latencyMs: 0
40
+ };
41
+ }
42
+ /**
43
+ * Sends one `GET` and classifies the outcome. Resolves for every outcome
44
+ * except a caller abort, which rejects with the signal's reason.
45
+ */
46
+ async function probeHttpEndpoint(target, options = {}) {
47
+ const startedAt = Date.now();
48
+ const { adapter, model } = target;
49
+ const fail = (reason, message, status) => ({
50
+ ok: false,
51
+ adapter,
52
+ reason,
53
+ ...status !== void 0 ? { status } : {},
54
+ ...model !== void 0 ? { model } : {},
55
+ message,
56
+ latencyMs: Date.now() - startedAt
57
+ });
58
+ const pass = (checked) => ({
59
+ ok: true,
60
+ adapter,
61
+ checked,
62
+ ...model !== void 0 ? { model } : {},
63
+ latencyMs: Date.now() - startedAt
64
+ });
65
+ options.signal?.throwIfAborted();
66
+ let url;
67
+ let fetchImpl;
68
+ let headers;
69
+ try {
70
+ url = target.url();
71
+ fetchImpl = resolveFetch(target.fetch);
72
+ headers = mergeHeaders({
73
+ apiKey: target.apiKey,
74
+ headers: target.headers
75
+ }, options.headers);
76
+ } catch (error) {
77
+ if (error instanceof SpeechConfigurationError) return fail("misconfigured", error.message);
78
+ return fail("misconfigured", `${adapter} has an invalid header or credential`);
79
+ }
80
+ if (target.listsModels) headers.set("accept", "application/json");
81
+ const timeoutMs = [options.timeoutMs, target.timeoutMs].find((value) => typeof value === "number" && Number.isFinite(value) && value > 0) ?? 1e4;
82
+ const timeout = new AbortController();
83
+ const timer = setTimeout(() => timeout.abort(), timeoutMs);
84
+ const signal = options.signal ? AbortSignal.any([options.signal, timeout.signal]) : timeout.signal;
85
+ try {
86
+ let response;
87
+ try {
88
+ response = await fetchImpl(url, {
89
+ method: "GET",
90
+ headers,
91
+ signal
92
+ });
93
+ } catch {
94
+ if (options.signal?.aborted) throw options.signal.reason;
95
+ if (timeout.signal.aborted) return fail("timeout", `${adapter} did not answer in ${timeoutMs} ms`);
96
+ return fail("unreachable", `${adapter} speech service could not be reached`);
97
+ }
98
+ if (response.status === 401 || response.status === 403) {
99
+ await discard(response);
100
+ return fail("unauthorized", `${adapter} rejected the credentials (HTTP ${response.status})`, response.status);
101
+ }
102
+ if (!target.listsModels) {
103
+ await discard(response);
104
+ return response.status < 500 ? pass("reachability") : fail("provider_error", `${adapter} answered HTTP ${response.status}`, response.status);
105
+ }
106
+ if ([
107
+ 404,
108
+ 405,
109
+ 501
110
+ ].includes(response.status)) {
111
+ await discard(response);
112
+ return pass("reachability");
113
+ }
114
+ if (!response.ok) {
115
+ await discard(response);
116
+ return fail("provider_error", `${adapter} model list failed with HTTP ${response.status}`, response.status);
117
+ }
118
+ let ids;
119
+ try {
120
+ ids = readModelIds(await readBounded(response, MAX_MODEL_LIST_BYTES));
121
+ } catch {
122
+ if (options.signal?.aborted) throw options.signal.reason;
123
+ if (timeout.signal.aborted) return fail("timeout", `${adapter} did not answer in ${timeoutMs} ms`);
124
+ }
125
+ if (!ids) return fail("provider_error", `${adapter} model list was not an OpenAI-style { data: [{ id }] } list`, response.status);
126
+ if (model === void 0) return pass("reachability");
127
+ return ids.includes(model) ? pass("model") : fail("model_not_found", `${adapter} does not list the configured model`, response.status);
128
+ } finally {
129
+ clearTimeout(timer);
130
+ }
131
+ }
132
+ function readModelIds(text) {
133
+ let parsed;
134
+ try {
135
+ parsed = JSON.parse(text);
136
+ } catch {
137
+ return;
138
+ }
139
+ const data = parsed?.data;
140
+ if (!Array.isArray(data)) return void 0;
141
+ return data.map((entry) => entry?.id).filter((id) => typeof id === "string");
142
+ }
143
+ async function readBounded(response, limit) {
144
+ if (!response.body) return response.text();
145
+ const reader = response.body.getReader();
146
+ const chunks = [];
147
+ let total = 0;
148
+ for (;;) {
149
+ const { done, value } = await reader.read();
150
+ if (done) break;
151
+ total += value.byteLength;
152
+ if (total > limit) {
153
+ await reader.cancel().catch(() => void 0);
154
+ throw new Error("model list too large");
155
+ }
156
+ chunks.push(value);
157
+ }
158
+ const bytes = new Uint8Array(total);
159
+ let offset = 0;
160
+ for (const chunk of chunks) {
161
+ bytes.set(chunk, offset);
162
+ offset += chunk.byteLength;
163
+ }
164
+ return new TextDecoder().decode(bytes);
165
+ }
166
+ async function discard(response) {
167
+ await response.body?.cancel().catch(() => void 0);
168
+ }
169
+ //#endregion
4
170
  //#region src/shared/retry.ts
5
171
  /**
6
172
  * Retry with exponential backoff for speech provider calls. Retries only
@@ -162,6 +328,16 @@ var HttpSpeechAdapter = class {
162
328
  this.headers = options.headers;
163
329
  this.timeoutMs = options.timeoutMs;
164
330
  }
331
+ /** Runs a readiness probe with this adapter's fetch, auth and headers. */
332
+ probeEndpoint(target, options) {
333
+ return probeHttpEndpoint({
334
+ ...target,
335
+ fetch: this.fetchImpl,
336
+ apiKey: this.apiKey,
337
+ headers: this.headers,
338
+ timeoutMs: this.timeoutMs
339
+ }, options);
340
+ }
165
341
  /**
166
342
  * POSTs to `path` (relative to `baseUrl`, or an absolute URL). The optional
167
343
  * `retry` policy retries 429/5xx responses; `timeoutMs` applies per attempt.
@@ -177,15 +353,21 @@ var HttpSpeechAdapter = class {
177
353
  const requestSignal = composeAbortSignals(init.signal, timeoutController?.signal);
178
354
  try {
179
355
  requestSignal.signal?.throwIfAborted();
180
- const response = await this.fetchImpl(resolveSpeechUrl(this.baseUrl, path), {
181
- ...init,
182
- method: "POST",
183
- headers: mergeHeaders({
184
- apiKey: this.apiKey,
185
- headers: this.headers
186
- }, init.headers),
187
- signal: requestSignal.signal
188
- });
356
+ let response;
357
+ try {
358
+ response = await this.fetchImpl(resolveSpeechUrl(this.baseUrl, path), {
359
+ ...init,
360
+ method: "POST",
361
+ headers: mergeHeaders({
362
+ apiKey: this.apiKey,
363
+ headers: this.headers
364
+ }, init.headers),
365
+ signal: requestSignal.signal
366
+ });
367
+ } catch (error) {
368
+ if (requestSignal.signal?.aborted) throw error;
369
+ throw new SpeechProviderUnavailableError(adapterName, `${adapterName} speech service could not be reached`, error);
370
+ }
189
371
  await assertOk(response, adapterName, this.apiKey);
190
372
  return await readResponse(response);
191
373
  } finally {
@@ -544,7 +726,7 @@ function unwrapRawAudio(bytes, mimeType, adapter) {
544
726
  })
545
727
  };
546
728
  if (RAW_PCM_TYPES.has(essence)) {
547
- if (essence === "audio/l16" && bytes.byteLength % 2 !== 0) throw new SpeechConfigurationError(`audio/L16 payload has an odd byte length (${bytes.byteLength}); 16-bit samples need an even number of bytes`, adapter);
729
+ if (essence === "audio/l16" && bytes.byteLength % 2 !== 0) throw new SpeechInvalidAudioError(`audio/L16 payload has an odd byte length (${bytes.byteLength}); 16-bit samples need an even number of bytes`, adapter);
548
730
  return {
549
731
  bytes: essence === "audio/l16" ? swapBytePairs(bytes) : bytes,
550
732
  format: compact({
@@ -570,7 +752,7 @@ function unwrapRawAudio(bytes, mimeType, adapter) {
570
752
  channels
571
753
  })
572
754
  };
573
- throw new SpeechConfigurationError(`${adapter} streams raw audio and cannot decode ${essence}; send PCM WAV or raw PCM, or use an HTTP transcriber for compressed recordings`, adapter);
755
+ throw new SpeechInvalidAudioError(`${adapter} streams raw audio and cannot decode ${essence}; send PCM WAV or raw PCM, or use an HTTP transcriber for compressed recordings`, adapter);
574
756
  }
575
757
  /** Returns a copy with each 16-bit sample's bytes swapped. */
576
758
  function swapBytePairs(bytes) {
@@ -585,7 +767,7 @@ function isRiffWave(bytes) {
585
767
  return bytes.byteLength >= 12 && ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WAVE";
586
768
  }
587
769
  function parseWav(bytes, adapter) {
588
- if (!isRiffWave(bytes)) throw new SpeechConfigurationError("Invalid WAV header", adapter);
770
+ if (!isRiffWave(bytes)) throw new SpeechInvalidAudioError("Invalid WAV header", adapter);
589
771
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
590
772
  let offset = 12;
591
773
  let format;
@@ -603,7 +785,7 @@ function parseWav(bytes, adapter) {
603
785
  channels
604
786
  };
605
787
  } else if (id === "data") {
606
- if (!format) throw new SpeechConfigurationError("WAV data chunk precedes its fmt chunk", adapter);
788
+ if (!format) throw new SpeechInvalidAudioError("WAV data chunk precedes its fmt chunk", adapter);
607
789
  const end = Math.min(bytes.byteLength, body + size);
608
790
  return {
609
791
  bytes: bytes.subarray(body, end),
@@ -612,13 +794,13 @@ function parseWav(bytes, adapter) {
612
794
  }
613
795
  offset = body + size + size % 2;
614
796
  }
615
- throw new SpeechConfigurationError("WAV file has no data chunk", adapter);
797
+ throw new SpeechInvalidAudioError("WAV file has no data chunk", adapter);
616
798
  }
617
799
  function wavEncoding(audioFormat, bitsPerSample, adapter) {
618
800
  if ((audioFormat === 1 || audioFormat === 65534) && bitsPerSample === 16) return "pcm16";
619
801
  if (audioFormat === 7 && bitsPerSample === 8) return "g711_ulaw";
620
802
  if (audioFormat === 6 && bitsPerSample === 8) return "g711_alaw";
621
- throw new SpeechConfigurationError(`Unsupported WAV encoding (format ${audioFormat}, ${bitsPerSample}-bit); expected 16-bit PCM or 8-bit G.711`, adapter);
803
+ throw new SpeechInvalidAudioError(`Unsupported WAV encoding (format ${audioFormat}, ${bitsPerSample}-bit); expected 16-bit PCM or 8-bit G.711`, adapter);
622
804
  }
623
805
  function ascii(bytes, offset, length) {
624
806
  return String.fromCharCode(...bytes.subarray(offset, offset + length));
@@ -1581,6 +1763,29 @@ var OpenAIRealtimeTranscriber = class {
1581
1763
  resolveTurnLimit(ADAPTER$1, void 0, options);
1582
1764
  }
1583
1765
  /**
1766
+ * `GET` of the server's model list (the realtime URL's `/models`, over
1767
+ * HTTP), confirming the configured model is listed. Needs `apiKey` or no
1768
+ * auth (vLLM): a browser `clientSecret` alone cannot read the list, so the
1769
+ * result is then `unsupported`.
1770
+ */
1771
+ probe(options) {
1772
+ if (!this.options.apiKey && this.options.clientSecret) return Promise.resolve(unsupportedProbe(ADAPTER$1, `${ADAPTER$1} with only a clientSecret cannot be probed without opening a session`));
1773
+ return probeHttpEndpoint({
1774
+ adapter: ADAPTER$1,
1775
+ url: () => {
1776
+ const url = new URL(modelsUrlFor(toHttpUrl(this.url), "realtime"));
1777
+ url.searchParams.delete("intent");
1778
+ return url.toString();
1779
+ },
1780
+ listsModels: true,
1781
+ model: this.options.model?.trim() || "gpt-4o-transcribe",
1782
+ fetch: this.options.fetch,
1783
+ apiKey: this.options.apiKey,
1784
+ headers: this.options.headers,
1785
+ timeoutMs: this.options.connectTimeoutMs
1786
+ }, options);
1787
+ }
1788
+ /**
1584
1789
  * A turn limit applies only to `manual` sessions with `maxTurnSeconds` set;
1585
1790
  * under server or semantic VAD the provider ends turns itself.
1586
1791
  */
@@ -1822,6 +2027,29 @@ var VoxtralRealtimeTranscriber = class {
1822
2027
  assertTurnDetection(options.turnDetection);
1823
2028
  this.resolveTurnLimit();
1824
2029
  }
2030
+ /**
2031
+ * `GET` of the server's model list (the realtime URL's `/models`, over
2032
+ * HTTP), confirming the configured model is listed. Needs `apiKey` or no
2033
+ * auth (vLLM): a browser `clientSecret` alone cannot read the list, so the
2034
+ * result is then `unsupported`.
2035
+ */
2036
+ probe(options) {
2037
+ if (!this.options.apiKey && this.options.clientSecret) return Promise.resolve(unsupportedProbe(ADAPTER, `${ADAPTER} with only a clientSecret cannot be probed without opening a session`));
2038
+ return probeHttpEndpoint({
2039
+ adapter: ADAPTER,
2040
+ url: () => {
2041
+ const url = new URL(modelsUrlFor(toHttpUrl(this.url), "realtime"));
2042
+ url.searchParams.delete("intent");
2043
+ return url.toString();
2044
+ },
2045
+ listsModels: true,
2046
+ model: this.options.model?.trim() || "mistralai/Voxtral-Mini-4B-Realtime-2602",
2047
+ fetch: this.options.fetch,
2048
+ apiKey: this.options.apiKey,
2049
+ headers: this.options.headers,
2050
+ timeoutMs: this.options.connectTimeoutMs
2051
+ }, options);
2052
+ }
1825
2053
  turnLimit(options = {}) {
1826
2054
  assertFormat(options.format);
1827
2055
  return describeTurnLimit(this.resolveTurnLimit(options), VOXTRAL_REALTIME_AUDIO_FORMAT);
@@ -1911,6 +2139,15 @@ var OpenAICompatibleSpeechSynthesizer = class extends HttpSpeechAdapter {
1911
2139
  model: speech.model ?? String(payload.model)
1912
2140
  };
1913
2141
  }
2142
+ /** `GET <api root>/models`, confirming `defaultModel` is listed. */
2143
+ probe(options) {
2144
+ return this.probeEndpoint({
2145
+ adapter: this.type,
2146
+ url: () => modelsUrlFor(this.speechPath ? resolveSpeechUrl(this.baseUrl, this.speechPath) : resolveOpenAICompatibleUrl(this.baseUrl, "audio/speech"), "audio/speech"),
2147
+ listsModels: true,
2148
+ model: this.defaultModel
2149
+ }, options);
2150
+ }
1914
2151
  };
1915
2152
  //#endregion
1916
2153
  //#region src/adapters/openai-compatible-transcriber.ts
@@ -2015,6 +2252,15 @@ var OpenAICompatibleTranscriber = class extends HttpSpeechAdapter {
2015
2252
  timestampGranularities
2016
2253
  };
2017
2254
  }
2255
+ /** `GET <api root>/models`, confirming the configured `model` is listed. */
2256
+ probe(options) {
2257
+ return this.probeEndpoint({
2258
+ adapter: this.type,
2259
+ url: () => modelsUrlFor(this.endpoint, "audio/transcriptions"),
2260
+ listsModels: true,
2261
+ model: this.model
2262
+ }, options);
2263
+ }
2018
2264
  };
2019
2265
  function isUnsupportedFormatError(error, wire) {
2020
2266
  return error instanceof SpeechProviderError && (error.status === 400 || error.status === 422) && (wire.responseFormat === "verbose_json" || Boolean(wire.timestampGranularities?.length)) && UNSUPPORTED_FORMAT_ERROR.test(error.responseBody ?? "");
@@ -2049,6 +2295,17 @@ var Qwen3SpeechSynthesizer = class extends HttpSpeechAdapter {
2049
2295
  model: speech.model ?? request.model ?? this.defaultModel
2050
2296
  };
2051
2297
  }
2298
+ /**
2299
+ * `GET` of the model list beside `speechPath` (`/v1/models` by default).
2300
+ * The model is not required to be listed: Qwen3 servers name it freely.
2301
+ */
2302
+ probe(options) {
2303
+ return this.probeEndpoint({
2304
+ adapter: this.type,
2305
+ url: () => modelsUrlFor(resolveSpeechUrl(this.baseUrl, this.speechPath), "audio/speech"),
2306
+ listsModels: true
2307
+ }, options);
2308
+ }
2052
2309
  };
2053
2310
  //#endregion
2054
2311
  //#region src/adapters/studio-server.ts
@@ -2076,6 +2333,17 @@ var StudioServerTranscriber = class extends HttpSpeechAdapter {
2076
2333
  signal: request.signal
2077
2334
  }, (response) => readTranscriptResponse(response, this.type));
2078
2335
  }
2336
+ /**
2337
+ * `GET` of `baseUrl`: Studio Server has no model list, so a success only
2338
+ * shows a server answered (`checked: 'reachability'`).
2339
+ */
2340
+ probe(options) {
2341
+ return this.probeEndpoint({
2342
+ adapter: this.type,
2343
+ url: () => normalizeBaseUrl(this.baseUrl),
2344
+ listsModels: false
2345
+ }, options);
2346
+ }
2079
2347
  };
2080
2348
  var StudioServerSpeechSynthesizer = class extends HttpSpeechAdapter {
2081
2349
  type = "studio-server";
@@ -2093,6 +2361,17 @@ var StudioServerSpeechSynthesizer = class extends HttpSpeechAdapter {
2093
2361
  signal: request.signal
2094
2362
  }, (response) => readSynthesizedSpeechResponse(response, this.type));
2095
2363
  }
2364
+ /**
2365
+ * `GET` of `baseUrl`: Studio Server has no model list, so a success only
2366
+ * shows a server answered (`checked: 'reachability'`).
2367
+ */
2368
+ probe(options) {
2369
+ return this.probeEndpoint({
2370
+ adapter: this.type,
2371
+ url: () => normalizeBaseUrl(this.baseUrl),
2372
+ listsModels: false
2373
+ }, options);
2374
+ }
2096
2375
  };
2097
2376
  //#endregion
2098
2377
  //#region src/shared/streaming-factory.ts
@@ -2190,8 +2469,10 @@ function parseMaxTurnSeconds(value) {
2190
2469
  function wrapStreamingTranscriber(streaming, options = {}) {
2191
2470
  const chunkBytes = options.chunkBytes ?? 64 * 1024;
2192
2471
  if (!Number.isInteger(chunkBytes) || chunkBytes <= 0) throw new SpeechConfigurationError("chunkBytes must be a positive integer", streaming.type);
2472
+ const probe = streaming.probe?.bind(streaming);
2193
2473
  return {
2194
2474
  type: streaming.type,
2475
+ ...probe ? { probe } : {},
2195
2476
  async transcribe(request) {
2196
2477
  const audio = await normalizeAudioInput(request.audio, {
2197
2478
  mimeType: request.mimeType,
@@ -2308,7 +2589,8 @@ function getWrappedStreamingTranscriber(type, options, context) {
2308
2589
  model: options.model,
2309
2590
  headers: options.headers,
2310
2591
  timeoutMs: options.timeoutMs,
2311
- onUsage: options.onUsage
2592
+ onUsage: options.onUsage,
2593
+ fetch: options.fetch
2312
2594
  })
2313
2595
  }, {
2314
2596
  env: context.env,
@@ -2568,6 +2850,6 @@ var ClientSecretClient = class extends HttpSpeechAdapter {
2568
2850
  }
2569
2851
  };
2570
2852
  //#endregion
2571
- export { CLIENT_SECRET_TTL_RANGE, DEFAULT_CLIENT_SECRET_TTL_SECONDS, DEFAULT_MAX_AUDIO_BYTES, DEFAULT_MAX_TURN_SECONDS, DEFAULT_ROLLOVER_MIN_SILENCE_MS, DEFAULT_ROLLOVER_SILENCE_THRESHOLD, DEFAULT_ROLLOVER_WINDOW_SECONDS, InvalidSpeechAdapterError, OPENAI_REALTIME_DEFAULT_MODEL, OPENAI_REALTIME_DEFAULT_URL, PcmCaptureError, RealtimeTranscriptionSession, STREAMING_TRANSCRIBER_ENV_KEYS, STREAMING_TRANSCRIBER_TYPES, SpeechConfigurationError, SpeechError, SpeechProviderError, TRANSCRIBER_ENV_KEYS, VOXTRAL_REALTIME_AUDIO_FORMAT, VOXTRAL_REALTIME_DEFAULT_MODEL, WavFormatError, audioSecondsForBytes, bytesPerSecond, createStreamingClientSecret, getAvailableSpeechAdapters, getSpeech, getSpeechSynthesizer, getStreamingTranscriber, getTranscriber, isBrowserRuntime, isStreamingTranscriberType, mimeTypeToAudioExtension, openAIRealtimeProtocol, openAIRealtimeTranscriptionSession, parseMaxTurnSeconds, parseOpenAIRealtimeEvent, parseVoxtralRealtimeEvent, resolveClientSecretUrl, resolveOpenAIRealtimeFormat, resolveRealtimeUrl, resolveVoxtralRealtimeUrl, transcriberInputFormat, unwrapRawAudio, voxtralRealtimeProtocol, wrapStreamingTranscriber };
2853
+ export { CLIENT_SECRET_TTL_RANGE, DEFAULT_CLIENT_SECRET_TTL_SECONDS, DEFAULT_MAX_AUDIO_BYTES, DEFAULT_MAX_TURN_SECONDS, DEFAULT_ROLLOVER_MIN_SILENCE_MS, DEFAULT_ROLLOVER_SILENCE_THRESHOLD, DEFAULT_ROLLOVER_WINDOW_SECONDS, DEFAULT_SPEECH_PROBE_TIMEOUT_MS, InvalidSpeechAdapterError, OPENAI_REALTIME_DEFAULT_MODEL, OPENAI_REALTIME_DEFAULT_URL, PcmCaptureError, RealtimeTranscriptionSession, STREAMING_TRANSCRIBER_ENV_KEYS, STREAMING_TRANSCRIBER_TYPES, SpeechAudioTooLargeError, SpeechConfigurationError, SpeechError, SpeechInvalidAudioError, SpeechProviderError, SpeechProviderUnavailableError, TRANSCRIBER_ENV_KEYS, VOXTRAL_REALTIME_AUDIO_FORMAT, VOXTRAL_REALTIME_DEFAULT_MODEL, WavFormatError, audioSecondsForBytes, bytesPerSecond, createStreamingClientSecret, getAvailableSpeechAdapters, getSpeech, getSpeechSynthesizer, getStreamingTranscriber, getTranscriber, isBrowserRuntime, isStreamingTranscriberType, mimeTypeToAudioExtension, openAIRealtimeProtocol, openAIRealtimeTranscriptionSession, parseMaxTurnSeconds, parseOpenAIRealtimeEvent, parseVoxtralRealtimeEvent, resolveClientSecretUrl, resolveOpenAIRealtimeFormat, resolveRealtimeUrl, resolveVoxtralRealtimeUrl, transcriberInputFormat, unwrapRawAudio, voxtralRealtimeProtocol, wrapStreamingTranscriber };
2572
2854
 
2573
2855
  //# sourceMappingURL=index.js.map