@voiceinput/openai 0.1.0-beta.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hirad Arshadiyarahmadi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # `@voiceinput/openai`
2
+
3
+ OpenAI Realtime transcription adapter for VoiceInput. The root export is
4
+ browser-safe; `@voiceinput/openai/server` mints ephemeral client credentials
5
+ with a long-lived API key.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @voiceinput/react@next @voiceinput/openai@next
11
+ ```
12
+
13
+ ## Browser adapter
14
+
15
+ ```ts
16
+ import { openai } from "@voiceinput/openai";
17
+
18
+ const provider = openai({
19
+ tokenEndpoint: "/api/voice-token",
20
+ });
21
+ ```
22
+
23
+ Pass `provider` to `VoiceInputProvider` or directly to `useVoiceInput`.
24
+
25
+ ### Defaults and shared-option mapping
26
+
27
+ - Model: `gpt-transcribe` (`OPENAI_DEFAULT_MODEL`)
28
+ - Audio: mono PCM16 at 24 kHz
29
+ - Omitted language: provider automatic detection
30
+ - `language`: normalized to its ISO 639-1 primary language code
31
+ - `vocabulary`: `keywords` for `gpt-live-transcribe*` models; a transcription
32
+ prompt for committed-turn transcription models
33
+ - `endpointing`: server VAD with 500 ms silence when omitted, manual commit when
34
+ `false`, or server VAD with the requested `silence_duration_ms`
35
+ - `gpt-live-transcribe*`: manual commit only; omitted endpointing maps to
36
+ `null`. Explicit server endpointing fails with `unsupported-feature` before
37
+ permission.
38
+
39
+ Vocabulary accepts at most 100 trimmed terms, each at most 200 characters and
40
+ without angle brackets or line breaks. Invalid or unsupported settings fail
41
+ before microphone permission with distinct error codes.
42
+
43
+ ### `OpenAIVoiceInputProviderOptions`
44
+
45
+ | Option | Purpose |
46
+ | --------------- | ------------------------------------------------------------------ |
47
+ | `tokenEndpoint` | Required same-origin endpoint that returns an ephemeral credential |
48
+ | `model` | Model ID; default `gpt-transcribe` |
49
+ | `fetch` | Test/runtime override for `globalThis.fetch` |
50
+ | `webSocket` | Test/runtime override for `globalThis.WebSocket` |
51
+ | `realtimeUrl` | Realtime WebSocket URL override |
52
+
53
+ The last three options are provider-factory escape hatches, primarily useful for
54
+ controlled infrastructure and deterministic tests.
55
+
56
+ ## Server token handler
57
+
58
+ ```ts
59
+ import { createOpenAITokenHandler } from "@voiceinput/openai/server";
60
+
61
+ export const POST = createOpenAITokenHandler({
62
+ apiKey: process.env.OPENAI_API_KEY!,
63
+ authorize: async (request) => {
64
+ const user = await authenticate(request);
65
+ return user ? { subject: user.id } : null;
66
+ },
67
+ rateLimit: async ({ subject }) => {
68
+ return (await underQuota(subject))
69
+ ? { allowed: true }
70
+ : { allowed: false, retryAfterSeconds: 60 };
71
+ },
72
+ });
73
+ ```
74
+
75
+ The handler accepts only `POST`, always requires `authorize`, sends
76
+ `Cache-Control: no-store`, and never returns or logs the long-lived API key.
77
+ Returning `null` from `authorize` produces `401`. A denied `rateLimit` produces
78
+ `429` before a provider credential is minted. Requests must be JSON and are
79
+ limited to 16 KiB. Authorization and rate-limit callbacks receive independent
80
+ request bodies. If `onTokenIssued` throws, credential delivery fails closed.
81
+
82
+ ### `CreateOpenAITokenHandlerOptions`
83
+
84
+ | Option | Purpose |
85
+ | --------------------------- | ------------------------------------------------------------------- |
86
+ | `apiKey` | Required server-only OpenAI key |
87
+ | `authorize(request)` | Required application authorization; returns `{ subject }` or `null` |
88
+ | `model` | Default model; default `gpt-transcribe` |
89
+ | `allowedModels` | Models a browser request may select; defaults to only `model` |
90
+ | `organization`, `project` | Optional OpenAI request headers |
91
+ | `safetyIdentifier(context)` | Optional per-subject OpenAI safety identifier |
92
+ | `rateLimit(context)` | Optional application quota check |
93
+ | `onTokenIssued(metadata)` | Metadata-only audit callback with subject, model, and expiry |
94
+ | `fetch`, `clientSecretUrl` | Transport/endpoint overrides |
95
+
96
+ `OpenAITokenHandlerContext` contains `request`, `subject`, and `model`.
97
+ `OpenAITokenIssuedMetadata` contains `provider: "openai"`, `subject`, `model`,
98
+ and `expiresAt`.
99
+
100
+ The default commits separate phrases during a recording, allowing undo and
101
+ correction to work at phrase boundaries. Live checks on 2026-09-04 confirmed
102
+ that `gpt-live-transcribe` rejects server VAD and does not commit until Stop. It
103
+ remains available for earlier interim feedback: set its model in both the
104
+ adapter and token handler and use `endpointing: false`. In that mode a recording
105
+ is one segment; editing its interim suppresses insertion until the next
106
+ recording. See [provider certification](../../docs/provider-certification.md)
107
+ for evidence and the latency tradeoff.
108
+
109
+ ## Public API
110
+
111
+ Browser root:
112
+
113
+ - `openai(options)`
114
+ - `OPENAI_DEFAULT_MODEL`
115
+ - `OpenAIVoiceInputProviderOptions`
116
+
117
+ Server-only entry point:
118
+
119
+ - `createOpenAITokenHandler(options)`
120
+ - `CreateOpenAITokenHandlerOptions`
121
+ - `OpenAIAuthorization`
122
+ - `OpenAIRateLimitResult`
123
+ - `OpenAITokenHandlerContext`
124
+ - `OpenAITokenIssuedMetadata`
125
+
126
+ ## Security
127
+
128
+ Import `/server` only from server code. The package export is disabled under the
129
+ browser condition. Never place `OPENAI_API_KEY` in a public environment variable
130
+ or send it to `openai()`. The browser adapter obtains an ephemeral credential
131
+ from your authenticated endpoint and then streams audio directly to OpenAI.
132
+
133
+ See the
134
+ [Next.js](https://github.com/VoiceInput/voiceinput/blob/main/docs/nextjs.md),
135
+ [Vite/Hono](https://github.com/VoiceInput/voiceinput/blob/main/docs/vite-hono.md),
136
+ and
137
+ [Express](https://github.com/VoiceInput/voiceinput/blob/main/docs/express.md)
138
+ integration guides.
package/dist/index.cjs ADDED
@@ -0,0 +1,486 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_session_config = require("./session-config-CmoDDiY6.cjs");
3
+ let _voiceinput_provider_transport = require("@voiceinput/provider/transport");
4
+ let _voiceinput_provider = require("@voiceinput/provider");
5
+ //#region src/index.ts
6
+ const DEFAULT_REALTIME_URL = "wss://api.openai.com/v1/realtime";
7
+ function openai(options) {
8
+ const model = validateFactoryString(options.model ?? "gpt-transcribe", "model");
9
+ const tokenEndpoint = validateFactoryString(String(options.tokenEndpoint), "tokenEndpoint");
10
+ const realtimeUrl = validateFactoryString(options.realtimeUrl ?? DEFAULT_REALTIME_URL, "realtimeUrl");
11
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
12
+ const WebSocketImplementation = options.webSocket ?? globalThis.WebSocket;
13
+ const validateProviderOptions = (transcriptionOptions) => validateOptions(transcriptionOptions, model);
14
+ return Object.freeze({
15
+ specificationVersion: "v1",
16
+ provider: "openai",
17
+ modelId: model,
18
+ sampleRate: require_session_config.OPENAI_SAMPLE_RATE,
19
+ validateOptions: validateProviderOptions,
20
+ async doOpen(callOptions) {
21
+ validateProviderOptions(callOptions);
22
+ return await openSession({
23
+ callOptions,
24
+ fetchImplementation,
25
+ model,
26
+ realtimeUrl,
27
+ tokenEndpoint,
28
+ WebSocketImplementation
29
+ });
30
+ }
31
+ });
32
+ }
33
+ function validateOptions(options, model) {
34
+ try {
35
+ require_session_config.validateOpenAITokenRequest({
36
+ model,
37
+ ...options
38
+ });
39
+ } catch (cause) {
40
+ if (_voiceinput_provider.VoiceInputError.isInstance(cause)) throw cause;
41
+ throw new _voiceinput_provider.VoiceInputError({
42
+ code: "invalid-configuration",
43
+ message: cause instanceof Error ? cause.message : "Invalid OpenAI transcription options.",
44
+ provider: "openai",
45
+ cause
46
+ });
47
+ }
48
+ }
49
+ async function openSession(options) {
50
+ const { abortSignal } = options.callOptions;
51
+ throwIfAborted(abortSignal);
52
+ if (typeof options.fetchImplementation !== "function") throw unsupportedBrowserFeature("fetch");
53
+ if (typeof options.WebSocketImplementation !== "function") throw unsupportedBrowserFeature("WebSocket");
54
+ const tokenRequest = require_session_config.validateOpenAITokenRequest({
55
+ model: options.model,
56
+ ...options.callOptions
57
+ });
58
+ const credential = await requestCredential(options.fetchImplementation, options.tokenEndpoint, tokenRequest, abortSignal);
59
+ throwIfAborted(abortSignal);
60
+ return await createSession(new options.WebSocketImplementation(options.realtimeUrl, ["realtime", `openai-insecure-api-key.${credential.value}`]), tokenRequest, abortSignal);
61
+ }
62
+ async function requestCredential(fetchImplementation, tokenEndpoint, tokenRequest, abortSignal) {
63
+ let response;
64
+ try {
65
+ response = await fetchImplementation(tokenEndpoint, {
66
+ method: "POST",
67
+ headers: { "Content-Type": "application/json" },
68
+ body: JSON.stringify(tokenRequest),
69
+ credentials: "same-origin",
70
+ signal: abortSignal
71
+ });
72
+ } catch (cause) {
73
+ throwIfAborted(abortSignal);
74
+ throw new _voiceinput_provider.VoiceInputError({
75
+ code: "network-error",
76
+ message: "Unable to reach the OpenAI token endpoint.",
77
+ provider: "openai",
78
+ retryable: true,
79
+ cause
80
+ });
81
+ }
82
+ if (!response.ok) throw await tokenResponseError(response);
83
+ let value;
84
+ try {
85
+ value = await response.json();
86
+ } catch (cause) {
87
+ throw new _voiceinput_provider.VoiceInputError({
88
+ code: "token-error",
89
+ message: "The OpenAI token endpoint returned invalid JSON.",
90
+ provider: "openai",
91
+ cause
92
+ });
93
+ }
94
+ if (!isRecord(value) || typeof value["value"] !== "string" || value["value"].length === 0 || typeof value["expires_at"] !== "number" || !Number.isFinite(value["expires_at"])) throw new _voiceinput_provider.VoiceInputError({
95
+ code: "token-error",
96
+ message: "The OpenAI token endpoint returned an invalid credential.",
97
+ provider: "openai"
98
+ });
99
+ return {
100
+ value: value["value"],
101
+ expires_at: value["expires_at"]
102
+ };
103
+ }
104
+ async function tokenResponseError(response) {
105
+ const retryAfterMs = parseRetryAfter(response.headers.get("Retry-After"));
106
+ if (response.status === 401 || response.status === 403) return new _voiceinput_provider.VoiceInputError({
107
+ code: "unauthorized",
108
+ message: "The OpenAI token endpoint rejected this request.",
109
+ provider: "openai"
110
+ });
111
+ if (response.status === 429) return new _voiceinput_provider.VoiceInputError({
112
+ code: "rate-limited",
113
+ message: "The OpenAI token endpoint rate limit was exceeded.",
114
+ provider: "openai",
115
+ retryable: true,
116
+ ...retryAfterMs === void 0 ? {} : { retryAfterMs }
117
+ });
118
+ const safeError = await readSafeTokenError(response);
119
+ if (safeError !== void 0) return new _voiceinput_provider.VoiceInputError({
120
+ ...safeError,
121
+ provider: "openai"
122
+ });
123
+ return new _voiceinput_provider.VoiceInputError({
124
+ code: "token-error",
125
+ message: "The OpenAI token endpoint did not issue a credential.",
126
+ provider: "openai",
127
+ retryable: response.status >= 500
128
+ });
129
+ }
130
+ async function readSafeTokenError(response) {
131
+ if (response.status !== 400 || response.headers.get("X-VoiceInput-Error") !== "1" || response.headers.get("Content-Type")?.split(";", 1)[0]?.trim() !== "application/json") return;
132
+ const text = await readBoundedErrorText(response);
133
+ if (text === void 0) return void 0;
134
+ try {
135
+ const value = JSON.parse(text);
136
+ const error = isRecord(value) ? value["error"] : void 0;
137
+ if (!isRecord(error) || error["code"] !== "invalid-configuration" && error["code"] !== "unsupported-feature" || typeof error["message"] !== "string" || error["message"].length === 0 || error["message"].length > 1e3) return;
138
+ return {
139
+ code: error["code"],
140
+ message: error["message"]
141
+ };
142
+ } catch {
143
+ return;
144
+ }
145
+ }
146
+ async function readBoundedErrorText(response) {
147
+ const reader = response.body?.getReader();
148
+ if (reader === void 0) return void 0;
149
+ const decoder = new TextDecoder();
150
+ let bytesRead = 0;
151
+ let text = "";
152
+ try {
153
+ while (true) {
154
+ const { done, value } = await reader.read();
155
+ if (done) return text + decoder.decode();
156
+ bytesRead += value.byteLength;
157
+ if (bytesRead > 4096) {
158
+ await reader.cancel().catch(() => {});
159
+ return;
160
+ }
161
+ text += decoder.decode(value, { stream: true });
162
+ }
163
+ } catch {
164
+ return;
165
+ }
166
+ }
167
+ async function createSession(socket, tokenRequest, abortSignal) {
168
+ let streamController;
169
+ const stream = new ReadableStream({ start(controller) {
170
+ streamController = controller;
171
+ } });
172
+ if (streamController === void 0) throw new _voiceinput_provider.VoiceInputError({
173
+ code: "provider-error",
174
+ message: "Unable to initialize the OpenAI transcript stream.",
175
+ provider: "openai"
176
+ });
177
+ const transcripts = /* @__PURE__ */ new Map();
178
+ const transcriptOrder = [];
179
+ const closedItems = /* @__PURE__ */ new Set();
180
+ let audioSent = false;
181
+ let closed = false;
182
+ let failed = false;
183
+ let finishing = false;
184
+ let manualCommitPending = false;
185
+ let vadCommitPending = false;
186
+ const manualCommitEventId = "voiceinput-finish";
187
+ const closeStream = () => {
188
+ if (closed) return;
189
+ closed = true;
190
+ abortSignal.removeEventListener("abort", abort);
191
+ streamController?.close();
192
+ };
193
+ const closeSocket = (reason) => {
194
+ if (socket.readyState === 1 || socket.readyState === 0) socket.close(1e3, reason);
195
+ };
196
+ const finishCleanly = () => {
197
+ closeStream();
198
+ closeSocket("finished");
199
+ };
200
+ const fail = (error) => {
201
+ if (closed || failed) return;
202
+ failed = true;
203
+ streamController?.enqueue({
204
+ type: "error",
205
+ error
206
+ });
207
+ closeStream();
208
+ closeSocket("aborted");
209
+ };
210
+ const maybeFinish = () => {
211
+ if (finishing && !manualCommitPending && !vadCommitPending && transcripts.size === 0) finishCleanly();
212
+ };
213
+ const emitInterim = (state) => {
214
+ if (state.delta && state.delta !== state.lastEmittedInterim) {
215
+ state.lastEmittedInterim = state.delta;
216
+ streamController?.enqueue({
217
+ type: "interim",
218
+ text: state.delta,
219
+ segmentId: transcriptOrder[0]
220
+ });
221
+ }
222
+ };
223
+ const flushFinals = () => {
224
+ while (transcriptOrder.length > 0) {
225
+ const itemId = transcriptOrder[0];
226
+ const state = transcripts.get(itemId);
227
+ if (state?.final === void 0) {
228
+ if (state !== void 0) emitInterim(state);
229
+ break;
230
+ }
231
+ transcriptOrder.shift();
232
+ transcripts.delete(itemId);
233
+ closedItems.add(itemId);
234
+ streamController?.enqueue({
235
+ type: "final",
236
+ text: state.final,
237
+ segmentId: itemId
238
+ });
239
+ }
240
+ maybeFinish();
241
+ };
242
+ const ensureTranscript = (itemId) => {
243
+ let state = transcripts.get(itemId);
244
+ if (state === void 0) {
245
+ state = { delta: "" };
246
+ transcripts.set(itemId, state);
247
+ transcriptOrder.push(itemId);
248
+ }
249
+ return state;
250
+ };
251
+ const handleMessage = (event) => {
252
+ if (closed) return;
253
+ try {
254
+ const value = JSON.parse(String(event.data));
255
+ if (!isRecord(value) || typeof value["type"] !== "string") throw new TypeError("OpenAI sent an invalid Realtime event.");
256
+ const type = value["type"];
257
+ if (type === "input_audio_buffer.speech_started") {
258
+ streamController?.enqueue({ type: "speech-start" });
259
+ return;
260
+ }
261
+ if (type === "input_audio_buffer.speech_stopped") {
262
+ vadCommitPending = true;
263
+ streamController?.enqueue({ type: "speech-end" });
264
+ return;
265
+ }
266
+ if (type === "input_audio_buffer.committed") {
267
+ const itemId = readString(value, "item_id");
268
+ if (closedItems.has(itemId)) return;
269
+ manualCommitPending = false;
270
+ vadCommitPending = false;
271
+ ensureTranscript(itemId);
272
+ flushFinals();
273
+ return;
274
+ }
275
+ if (type === "conversation.item.input_audio_transcription.delta") {
276
+ const itemId = readString(value, "item_id");
277
+ if (closedItems.has(itemId)) return;
278
+ const state = ensureTranscript(itemId);
279
+ state.delta += readString(value, "delta");
280
+ if (transcriptOrder[0] === itemId) emitInterim(state);
281
+ return;
282
+ }
283
+ if (type === "conversation.item.input_audio_transcription.completed") {
284
+ const itemId = readString(value, "item_id");
285
+ if (closedItems.has(itemId)) return;
286
+ ensureTranscript(itemId).final = readString(value, "transcript");
287
+ flushFinals();
288
+ return;
289
+ }
290
+ if (type === "conversation.item.input_audio_transcription.failed") {
291
+ fail(normalizeTranscriptionFailure(value));
292
+ return;
293
+ }
294
+ if (type === "error") {
295
+ if (isExpectedEmptyCommitError(value, manualCommitEventId)) {
296
+ manualCommitPending = false;
297
+ maybeFinish();
298
+ return;
299
+ }
300
+ fail(normalizeRealtimeError(value));
301
+ }
302
+ } catch (cause) {
303
+ fail(new _voiceinput_provider.VoiceInputError({
304
+ code: "provider-error",
305
+ message: "OpenAI sent an invalid Realtime event.",
306
+ provider: "openai",
307
+ cause
308
+ }));
309
+ }
310
+ };
311
+ const handleClose = (event) => {
312
+ if (closed) return;
313
+ if (event.code === 1e3) closeStream();
314
+ else fail(new _voiceinput_provider.VoiceInputError({
315
+ code: "network-error",
316
+ message: "The OpenAI Realtime connection closed unexpectedly.",
317
+ provider: "openai",
318
+ retryable: true,
319
+ cause: event
320
+ }));
321
+ };
322
+ const abort = () => {
323
+ if (closed) return;
324
+ closeStream();
325
+ closeSocket("aborted");
326
+ };
327
+ socket.addEventListener("message", handleMessage);
328
+ socket.addEventListener("close", handleClose);
329
+ socket.addEventListener("error", () => {
330
+ fail(new _voiceinput_provider.VoiceInputError({
331
+ code: "network-error",
332
+ message: "The OpenAI Realtime connection failed.",
333
+ provider: "openai",
334
+ retryable: true
335
+ }));
336
+ });
337
+ abortSignal.addEventListener("abort", abort, { once: true });
338
+ await waitForOpen(socket, abortSignal);
339
+ sendEvent(socket, {
340
+ type: "session.update",
341
+ session: require_session_config.createOpenAITranscriptionSession(tokenRequest)
342
+ });
343
+ return {
344
+ stream,
345
+ sendAudio(chunk) {
346
+ if (closed || finishing) return;
347
+ audioSent ||= chunk.length > 0;
348
+ if (chunk.length > 0) return (0, _voiceinput_provider_transport.sendWithBackpressure)(socket, Math.ceil(chunk.byteLength * 4 / 3) + 128, abortSignal, "openai", () => sendEvent(socket, {
349
+ type: "input_audio_buffer.append",
350
+ audio: encodePcm16(chunk)
351
+ }));
352
+ },
353
+ finish() {
354
+ if (closed || finishing) return;
355
+ finishing = true;
356
+ if (!audioSent) {
357
+ finishCleanly();
358
+ return;
359
+ }
360
+ if (!vadCommitPending) {
361
+ manualCommitPending = true;
362
+ sendEvent(socket, {
363
+ type: "input_audio_buffer.commit",
364
+ event_id: manualCommitEventId
365
+ });
366
+ }
367
+ maybeFinish();
368
+ },
369
+ abort
370
+ };
371
+ }
372
+ function waitForOpen(socket, abortSignal) {
373
+ if (socket.readyState === 1) return Promise.resolve();
374
+ return new Promise((resolve, reject) => {
375
+ const cleanup = () => {
376
+ socket.removeEventListener("open", handleOpen);
377
+ socket.removeEventListener("error", handleError);
378
+ abortSignal.removeEventListener("abort", handleAbort);
379
+ };
380
+ const handleOpen = () => {
381
+ cleanup();
382
+ resolve();
383
+ };
384
+ const handleError = (event) => {
385
+ cleanup();
386
+ reject(new _voiceinput_provider.VoiceInputError({
387
+ code: "network-error",
388
+ message: "Unable to open the OpenAI Realtime connection.",
389
+ provider: "openai",
390
+ retryable: true,
391
+ cause: event
392
+ }));
393
+ };
394
+ const handleAbort = () => {
395
+ cleanup();
396
+ reject(abortSignal.reason);
397
+ };
398
+ socket.addEventListener("open", handleOpen, { once: true });
399
+ socket.addEventListener("error", handleError, { once: true });
400
+ abortSignal.addEventListener("abort", handleAbort, { once: true });
401
+ });
402
+ }
403
+ function sendEvent(socket, event) {
404
+ try {
405
+ socket.send(JSON.stringify(event));
406
+ } catch (cause) {
407
+ throw new _voiceinput_provider.VoiceInputError({
408
+ code: "network-error",
409
+ message: "Unable to send data to OpenAI Realtime.",
410
+ provider: "openai",
411
+ retryable: true,
412
+ cause
413
+ });
414
+ }
415
+ }
416
+ function encodePcm16(chunk) {
417
+ const bytes = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
418
+ let binary = "";
419
+ for (let offset = 0; offset < bytes.length; offset += 8192) binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));
420
+ return btoa(binary);
421
+ }
422
+ function normalizeRealtimeError(value) {
423
+ const error = isRecord(value["error"]) ? value["error"] : value;
424
+ const code = typeof error["code"] === "string" ? error["code"] : "";
425
+ const message = typeof error["message"] === "string" ? error["message"] : "OpenAI Realtime reported an error.";
426
+ const rateLimited = code.includes("rate_limit");
427
+ return new _voiceinput_provider.VoiceInputError({
428
+ code: rateLimited ? "rate-limited" : "provider-error",
429
+ message,
430
+ provider: "openai",
431
+ retryable: rateLimited || code.includes("server_error"),
432
+ cause: value
433
+ });
434
+ }
435
+ function normalizeTranscriptionFailure(value) {
436
+ const error = isRecord(value["error"]) ? value["error"] : {};
437
+ return new _voiceinput_provider.VoiceInputError({
438
+ code: "provider-error",
439
+ message: typeof error["message"] === "string" ? error["message"] : "OpenAI could not transcribe an audio turn.",
440
+ provider: "openai",
441
+ retryable: typeof error["code"] === "string" && error["code"].includes("server_error"),
442
+ cause: value
443
+ });
444
+ }
445
+ function isExpectedEmptyCommitError(value, eventId) {
446
+ const error = isRecord(value["error"]) ? value["error"] : {};
447
+ return error["code"] === "input_audio_buffer_commit_empty" && error["event_id"] === eventId;
448
+ }
449
+ function readString(value, key) {
450
+ const result = value[key];
451
+ if (typeof result !== "string") throw new TypeError(`${key} must be a string.`);
452
+ return result;
453
+ }
454
+ function parseRetryAfter(value) {
455
+ if (value === null) return;
456
+ const seconds = Number(value);
457
+ if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1e3);
458
+ const date = Date.parse(value);
459
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
460
+ }
461
+ function throwIfAborted(signal) {
462
+ if (signal.aborted) throw signal.reason;
463
+ }
464
+ function validateFactoryString(value, name) {
465
+ if (value.trim().length === 0) throw new _voiceinput_provider.VoiceInputError({
466
+ code: "invalid-configuration",
467
+ message: `${name} must be a non-empty string.`,
468
+ provider: "openai"
469
+ });
470
+ return value;
471
+ }
472
+ function unsupportedBrowserFeature(feature) {
473
+ return new _voiceinput_provider.VoiceInputError({
474
+ code: "unsupported-browser",
475
+ message: `OpenAI voice input requires browser ${feature} support.`,
476
+ provider: "openai"
477
+ });
478
+ }
479
+ function isRecord(value) {
480
+ return typeof value === "object" && value !== null && !Array.isArray(value);
481
+ }
482
+ //#endregion
483
+ exports.OPENAI_DEFAULT_MODEL = require_session_config.OPENAI_DEFAULT_MODEL;
484
+ exports.openai = openai;
485
+
486
+ //# sourceMappingURL=index.cjs.map