@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 +21 -0
- package/README.md +138 -0
- package/dist/index.cjs +486 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +484 -0
- package/dist/index.js.map +1 -0
- package/dist/server.cjs +165 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +38 -0
- package/dist/server.d.cts.map +1 -0
- package/dist/server.d.ts +38 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +164 -0
- package/dist/server.js.map +1 -0
- package/dist/session-config-CmoDDiY6.cjs +114 -0
- package/dist/session-config-CmoDDiY6.cjs.map +1 -0
- package/dist/session-config-CuDb4m9Q.js +91 -0
- package/dist/session-config-CuDb4m9Q.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["OPENAI_SAMPLE_RATE","VoiceInputError","validateOpenAITokenRequest","createOpenAITranscriptionSession","sendWithBackpressure"],"sources":["../src/index.ts"],"sourcesContent":["import { sendWithBackpressure } from \"@voiceinput/provider/transport\";\nimport {\n VoiceInputError,\n type VoiceInputProviderV1,\n type VoiceInputProviderV1CallOptions,\n type VoiceInputProviderV1Session,\n type VoiceInputProviderV1StreamPart,\n type VoiceTranscriptionOptions,\n} from \"@voiceinput/provider\";\n\nimport {\n OPENAI_DEFAULT_MODEL,\n OPENAI_SAMPLE_RATE,\n createOpenAITranscriptionSession,\n validateOpenAITokenRequest,\n type OpenAITokenRequest,\n} from \"./session-config.js\";\n\nconst DEFAULT_REALTIME_URL = \"wss://api.openai.com/v1/realtime\";\n\nexport interface OpenAIVoiceInputProviderOptions {\n readonly tokenEndpoint: string | URL;\n readonly model?: string;\n readonly fetch?: typeof globalThis.fetch;\n readonly webSocket?: typeof globalThis.WebSocket;\n readonly realtimeUrl?: string;\n}\n\ninterface OpenAICredential {\n value: string;\n expires_at: number;\n}\n\ninterface TranscriptState {\n delta: string;\n final?: string;\n lastEmittedInterim?: string;\n}\n\nexport function openai(\n options: OpenAIVoiceInputProviderOptions,\n): VoiceInputProviderV1 {\n const model = validateFactoryString(\n options.model ?? OPENAI_DEFAULT_MODEL,\n \"model\",\n );\n const tokenEndpoint = validateFactoryString(\n String(options.tokenEndpoint),\n \"tokenEndpoint\",\n );\n const realtimeUrl = validateFactoryString(\n options.realtimeUrl ?? DEFAULT_REALTIME_URL,\n \"realtimeUrl\",\n );\n const fetchImplementation = options.fetch ?? globalThis.fetch;\n const WebSocketImplementation = options.webSocket ?? globalThis.WebSocket;\n const validateProviderOptions = (\n transcriptionOptions: VoiceTranscriptionOptions,\n ): void => validateOptions(transcriptionOptions, model);\n\n return Object.freeze({\n specificationVersion: \"v1\" as const,\n provider: \"openai\",\n modelId: model,\n sampleRate: OPENAI_SAMPLE_RATE,\n validateOptions: validateProviderOptions,\n async doOpen(callOptions: VoiceInputProviderV1CallOptions) {\n validateProviderOptions(callOptions);\n return await openSession({\n callOptions,\n fetchImplementation,\n model,\n realtimeUrl,\n tokenEndpoint,\n WebSocketImplementation,\n });\n },\n });\n}\n\nfunction validateOptions(\n options: VoiceTranscriptionOptions,\n model: string,\n): void {\n try {\n validateOpenAITokenRequest({\n model,\n ...options,\n });\n } catch (cause) {\n if (VoiceInputError.isInstance(cause)) {\n throw cause;\n }\n throw new VoiceInputError({\n code: \"invalid-configuration\",\n message:\n cause instanceof Error\n ? cause.message\n : \"Invalid OpenAI transcription options.\",\n provider: \"openai\",\n cause,\n });\n }\n}\n\nasync function openSession(options: {\n callOptions: VoiceInputProviderV1CallOptions;\n fetchImplementation: typeof globalThis.fetch;\n model: string;\n realtimeUrl: string;\n tokenEndpoint: string;\n WebSocketImplementation: typeof globalThis.WebSocket;\n}): Promise<VoiceInputProviderV1Session> {\n const { abortSignal } = options.callOptions;\n throwIfAborted(abortSignal);\n if (typeof options.fetchImplementation !== \"function\") {\n throw unsupportedBrowserFeature(\"fetch\");\n }\n if (typeof options.WebSocketImplementation !== \"function\") {\n throw unsupportedBrowserFeature(\"WebSocket\");\n }\n const tokenRequest = validateOpenAITokenRequest({\n model: options.model,\n ...options.callOptions,\n });\n const credential = await requestCredential(\n options.fetchImplementation,\n options.tokenEndpoint,\n tokenRequest,\n abortSignal,\n );\n throwIfAborted(abortSignal);\n\n const socket = new options.WebSocketImplementation(options.realtimeUrl, [\n \"realtime\",\n `openai-insecure-api-key.${credential.value}`,\n ]);\n return await createSession(socket, tokenRequest, abortSignal);\n}\n\nasync function requestCredential(\n fetchImplementation: typeof globalThis.fetch,\n tokenEndpoint: string,\n tokenRequest: OpenAITokenRequest,\n abortSignal: AbortSignal,\n): Promise<OpenAICredential> {\n let response: Response;\n try {\n response = await fetchImplementation(tokenEndpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(tokenRequest),\n credentials: \"same-origin\",\n signal: abortSignal,\n });\n } catch (cause) {\n throwIfAborted(abortSignal);\n throw new VoiceInputError({\n code: \"network-error\",\n message: \"Unable to reach the OpenAI token endpoint.\",\n provider: \"openai\",\n retryable: true,\n cause,\n });\n }\n\n if (!response.ok) {\n throw await tokenResponseError(response);\n }\n\n let value: unknown;\n try {\n value = await response.json();\n } catch (cause) {\n throw new VoiceInputError({\n code: \"token-error\",\n message: \"The OpenAI token endpoint returned invalid JSON.\",\n provider: \"openai\",\n cause,\n });\n }\n if (\n !isRecord(value) ||\n typeof value[\"value\"] !== \"string\" ||\n value[\"value\"].length === 0 ||\n typeof value[\"expires_at\"] !== \"number\" ||\n !Number.isFinite(value[\"expires_at\"])\n ) {\n throw new VoiceInputError({\n code: \"token-error\",\n message: \"The OpenAI token endpoint returned an invalid credential.\",\n provider: \"openai\",\n });\n }\n return { value: value[\"value\"], expires_at: value[\"expires_at\"] };\n}\n\nasync function tokenResponseError(\n response: Response,\n): Promise<VoiceInputError> {\n const retryAfterMs = parseRetryAfter(response.headers.get(\"Retry-After\"));\n if (response.status === 401 || response.status === 403) {\n return new VoiceInputError({\n code: \"unauthorized\",\n message: \"The OpenAI token endpoint rejected this request.\",\n provider: \"openai\",\n });\n }\n if (response.status === 429) {\n return new VoiceInputError({\n code: \"rate-limited\",\n message: \"The OpenAI token endpoint rate limit was exceeded.\",\n provider: \"openai\",\n retryable: true,\n ...(retryAfterMs === undefined ? {} : { retryAfterMs }),\n });\n }\n const safeError = await readSafeTokenError(response);\n if (safeError !== undefined) {\n return new VoiceInputError({ ...safeError, provider: \"openai\" });\n }\n return new VoiceInputError({\n code: \"token-error\",\n message: \"The OpenAI token endpoint did not issue a credential.\",\n provider: \"openai\",\n retryable: response.status >= 500,\n });\n}\n\nasync function readSafeTokenError(response: Response): Promise<\n | {\n code: \"invalid-configuration\" | \"unsupported-feature\";\n message: string;\n }\n | undefined\n> {\n if (\n response.status !== 400 ||\n response.headers.get(\"X-VoiceInput-Error\") !== \"1\" ||\n response.headers.get(\"Content-Type\")?.split(\";\", 1)[0]?.trim() !==\n \"application/json\"\n ) {\n return undefined;\n }\n const text = await readBoundedErrorText(response);\n if (text === undefined) return undefined;\n try {\n const value = JSON.parse(text) as unknown;\n const error = isRecord(value) ? value[\"error\"] : undefined;\n if (\n !isRecord(error) ||\n (error[\"code\"] !== \"invalid-configuration\" &&\n error[\"code\"] !== \"unsupported-feature\") ||\n typeof error[\"message\"] !== \"string\" ||\n error[\"message\"].length === 0 ||\n error[\"message\"].length > 1_000\n ) {\n return undefined;\n }\n return { code: error[\"code\"], message: error[\"message\"] };\n } catch {\n return undefined;\n }\n}\n\nasync function readBoundedErrorText(\n response: Response,\n): Promise<string | undefined> {\n const reader = response.body?.getReader();\n if (reader === undefined) return undefined;\n const decoder = new TextDecoder();\n let bytesRead = 0;\n let text = \"\";\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) return text + decoder.decode();\n bytesRead += value.byteLength;\n if (bytesRead > 4_096) {\n await reader.cancel().catch(() => {});\n return undefined;\n }\n text += decoder.decode(value, { stream: true });\n }\n } catch {\n return undefined;\n }\n}\n\nasync function createSession(\n socket: WebSocket,\n tokenRequest: OpenAITokenRequest,\n abortSignal: AbortSignal,\n): Promise<VoiceInputProviderV1Session> {\n let streamController:\n ReadableStreamDefaultController<VoiceInputProviderV1StreamPart> | undefined;\n const stream = new ReadableStream<VoiceInputProviderV1StreamPart>({\n start(controller) {\n streamController = controller;\n },\n });\n if (streamController === undefined) {\n throw new VoiceInputError({\n code: \"provider-error\",\n message: \"Unable to initialize the OpenAI transcript stream.\",\n provider: \"openai\",\n });\n }\n\n const transcripts = new Map<string, TranscriptState>();\n const transcriptOrder: string[] = [];\n const closedItems = new Set<string>();\n let audioSent = false;\n let closed = false;\n let failed = false;\n let finishing = false;\n let manualCommitPending = false;\n let vadCommitPending = false;\n const manualCommitEventId = \"voiceinput-finish\";\n\n const closeStream = (): void => {\n if (closed) {\n return;\n }\n closed = true;\n abortSignal.removeEventListener(\"abort\", abort);\n streamController?.close();\n };\n\n const closeSocket = (reason: \"aborted\" | \"finished\"): void => {\n if (socket.readyState === 1 || socket.readyState === 0) {\n socket.close(1000, reason);\n }\n };\n\n const finishCleanly = (): void => {\n closeStream();\n closeSocket(\"finished\");\n };\n\n const fail = (error: VoiceInputError): void => {\n if (closed || failed) {\n return;\n }\n failed = true;\n streamController?.enqueue({ type: \"error\", error });\n closeStream();\n closeSocket(\"aborted\");\n };\n\n const maybeFinish = (): void => {\n if (\n finishing &&\n !manualCommitPending &&\n !vadCommitPending &&\n transcripts.size === 0\n ) {\n finishCleanly();\n }\n };\n\n const emitInterim = (state: TranscriptState): void => {\n if (state.delta && state.delta !== state.lastEmittedInterim) {\n state.lastEmittedInterim = state.delta;\n streamController?.enqueue({\n type: \"interim\",\n text: state.delta,\n segmentId: transcriptOrder[0] as string,\n });\n }\n };\n\n const flushFinals = (): void => {\n while (transcriptOrder.length > 0) {\n const itemId = transcriptOrder[0] as string;\n const state = transcripts.get(itemId);\n if (state?.final === undefined) {\n if (state !== undefined) {\n emitInterim(state);\n }\n break;\n }\n transcriptOrder.shift();\n transcripts.delete(itemId);\n closedItems.add(itemId);\n streamController?.enqueue({\n type: \"final\",\n text: state.final,\n segmentId: itemId,\n });\n }\n maybeFinish();\n };\n\n const ensureTranscript = (itemId: string): TranscriptState => {\n let state = transcripts.get(itemId);\n if (state === undefined) {\n state = { delta: \"\" };\n transcripts.set(itemId, state);\n transcriptOrder.push(itemId);\n }\n return state;\n };\n\n const handleMessage = (event: MessageEvent): void => {\n if (closed) {\n return;\n }\n try {\n const value = JSON.parse(String(event.data)) as unknown;\n if (!isRecord(value) || typeof value[\"type\"] !== \"string\") {\n throw new TypeError(\"OpenAI sent an invalid Realtime event.\");\n }\n const type = value[\"type\"];\n if (type === \"input_audio_buffer.speech_started\") {\n streamController?.enqueue({ type: \"speech-start\" });\n return;\n }\n if (type === \"input_audio_buffer.speech_stopped\") {\n vadCommitPending = true;\n streamController?.enqueue({ type: \"speech-end\" });\n return;\n }\n if (type === \"input_audio_buffer.committed\") {\n const itemId = readString(value, \"item_id\");\n if (closedItems.has(itemId)) return;\n manualCommitPending = false;\n vadCommitPending = false;\n ensureTranscript(itemId);\n flushFinals();\n return;\n }\n if (type === \"conversation.item.input_audio_transcription.delta\") {\n const itemId = readString(value, \"item_id\");\n if (closedItems.has(itemId)) return;\n const state = ensureTranscript(itemId);\n state.delta += readString(value, \"delta\");\n if (transcriptOrder[0] === itemId) {\n emitInterim(state);\n }\n return;\n }\n if (type === \"conversation.item.input_audio_transcription.completed\") {\n const itemId = readString(value, \"item_id\");\n if (closedItems.has(itemId)) return;\n ensureTranscript(itemId).final = readString(value, \"transcript\");\n flushFinals();\n return;\n }\n if (type === \"conversation.item.input_audio_transcription.failed\") {\n fail(normalizeTranscriptionFailure(value));\n return;\n }\n if (type === \"error\") {\n if (isExpectedEmptyCommitError(value, manualCommitEventId)) {\n manualCommitPending = false;\n maybeFinish();\n return;\n }\n fail(normalizeRealtimeError(value));\n }\n } catch (cause) {\n fail(\n new VoiceInputError({\n code: \"provider-error\",\n message: \"OpenAI sent an invalid Realtime event.\",\n provider: \"openai\",\n cause,\n }),\n );\n }\n };\n\n const handleClose = (event: CloseEvent): void => {\n if (closed) {\n return;\n }\n if (event.code === 1000) {\n closeStream();\n } else {\n fail(\n new VoiceInputError({\n code: \"network-error\",\n message: \"The OpenAI Realtime connection closed unexpectedly.\",\n provider: \"openai\",\n retryable: true,\n cause: event,\n }),\n );\n }\n };\n\n const abort = (): void => {\n if (closed) {\n return;\n }\n closeStream();\n closeSocket(\"aborted\");\n };\n\n socket.addEventListener(\"message\", handleMessage);\n socket.addEventListener(\"close\", handleClose);\n socket.addEventListener(\"error\", () => {\n fail(\n new VoiceInputError({\n code: \"network-error\",\n message: \"The OpenAI Realtime connection failed.\",\n provider: \"openai\",\n retryable: true,\n }),\n );\n });\n abortSignal.addEventListener(\"abort\", abort, { once: true });\n\n await waitForOpen(socket, abortSignal);\n sendEvent(socket, {\n type: \"session.update\",\n session: createOpenAITranscriptionSession(tokenRequest),\n });\n\n return {\n stream,\n sendAudio(chunk) {\n if (closed || finishing) {\n return;\n }\n audioSent ||= chunk.length > 0;\n if (chunk.length > 0) {\n return sendWithBackpressure(\n socket,\n Math.ceil((chunk.byteLength * 4) / 3) + 128,\n abortSignal,\n \"openai\",\n () =>\n sendEvent(socket, {\n type: \"input_audio_buffer.append\",\n audio: encodePcm16(chunk),\n }),\n );\n }\n },\n finish() {\n if (closed || finishing) {\n return;\n }\n finishing = true;\n if (!audioSent) {\n finishCleanly();\n return;\n }\n if (!vadCommitPending) {\n manualCommitPending = true;\n sendEvent(socket, {\n type: \"input_audio_buffer.commit\",\n event_id: manualCommitEventId,\n });\n }\n maybeFinish();\n },\n abort,\n };\n}\n\nfunction waitForOpen(\n socket: WebSocket,\n abortSignal: AbortSignal,\n): Promise<void> {\n if (socket.readyState === 1) {\n return Promise.resolve();\n }\n return new Promise((resolve, reject) => {\n const cleanup = (): void => {\n socket.removeEventListener(\"open\", handleOpen);\n socket.removeEventListener(\"error\", handleError);\n abortSignal.removeEventListener(\"abort\", handleAbort);\n };\n const handleOpen = (): void => {\n cleanup();\n resolve();\n };\n const handleError = (event: Event): void => {\n cleanup();\n reject(\n new VoiceInputError({\n code: \"network-error\",\n message: \"Unable to open the OpenAI Realtime connection.\",\n provider: \"openai\",\n retryable: true,\n cause: event,\n }),\n );\n };\n const handleAbort = (): void => {\n cleanup();\n reject(abortSignal.reason);\n };\n socket.addEventListener(\"open\", handleOpen, { once: true });\n socket.addEventListener(\"error\", handleError, { once: true });\n abortSignal.addEventListener(\"abort\", handleAbort, { once: true });\n });\n}\n\nfunction sendEvent(socket: WebSocket, event: Record<string, unknown>): void {\n try {\n socket.send(JSON.stringify(event));\n } catch (cause) {\n throw new VoiceInputError({\n code: \"network-error\",\n message: \"Unable to send data to OpenAI Realtime.\",\n provider: \"openai\",\n retryable: true,\n cause,\n });\n }\n}\n\nfunction encodePcm16(chunk: Int16Array): string {\n const bytes = new Uint8Array(\n chunk.buffer,\n chunk.byteOffset,\n chunk.byteLength,\n );\n let binary = \"\";\n for (let offset = 0; offset < bytes.length; offset += 8_192) {\n binary += String.fromCharCode(...bytes.subarray(offset, offset + 8_192));\n }\n return btoa(binary);\n}\n\nfunction normalizeRealtimeError(\n value: Record<string, unknown>,\n): VoiceInputError {\n const error = isRecord(value[\"error\"]) ? value[\"error\"] : value;\n const code = typeof error[\"code\"] === \"string\" ? error[\"code\"] : \"\";\n const message =\n typeof error[\"message\"] === \"string\"\n ? error[\"message\"]\n : \"OpenAI Realtime reported an error.\";\n const rateLimited = code.includes(\"rate_limit\");\n return new VoiceInputError({\n code: rateLimited ? \"rate-limited\" : \"provider-error\",\n message,\n provider: \"openai\",\n retryable: rateLimited || code.includes(\"server_error\"),\n cause: value,\n });\n}\n\nfunction normalizeTranscriptionFailure(\n value: Record<string, unknown>,\n): VoiceInputError {\n const error = isRecord(value[\"error\"]) ? value[\"error\"] : {};\n return new VoiceInputError({\n code: \"provider-error\",\n message:\n typeof error[\"message\"] === \"string\"\n ? error[\"message\"]\n : \"OpenAI could not transcribe an audio turn.\",\n provider: \"openai\",\n retryable:\n typeof error[\"code\"] === \"string\" &&\n error[\"code\"].includes(\"server_error\"),\n cause: value,\n });\n}\n\nfunction isExpectedEmptyCommitError(\n value: Record<string, unknown>,\n eventId: string,\n): boolean {\n const error = isRecord(value[\"error\"]) ? value[\"error\"] : {};\n return (\n error[\"code\"] === \"input_audio_buffer_commit_empty\" &&\n error[\"event_id\"] === eventId\n );\n}\n\nfunction readString(value: Record<string, unknown>, key: string): string {\n const result = value[key];\n if (typeof result !== \"string\") {\n throw new TypeError(`${key} must be a string.`);\n }\n return result;\n}\n\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (value === null) {\n return undefined;\n }\n const seconds = Number(value);\n if (Number.isFinite(seconds) && seconds >= 0) {\n return Math.ceil(seconds * 1_000);\n }\n const date = Date.parse(value);\n return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());\n}\n\nfunction throwIfAborted(signal: AbortSignal): void {\n if (signal.aborted) {\n throw signal.reason;\n }\n}\n\nfunction validateFactoryString(value: string, name: string): string {\n if (value.trim().length === 0) {\n throw new VoiceInputError({\n code: \"invalid-configuration\",\n message: `${name} must be a non-empty string.`,\n provider: \"openai\",\n });\n }\n return value;\n}\n\nfunction unsupportedBrowserFeature(feature: string): VoiceInputError {\n return new VoiceInputError({\n code: \"unsupported-browser\",\n message: `OpenAI voice input requires browser ${feature} support.`,\n provider: \"openai\",\n });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport { OPENAI_DEFAULT_MODEL } from \"./session-config.js\";\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { VoiceInputProviderV1 } from "@voiceinput/provider";
|
|
2
|
+
//#region src/session-config.d.ts
|
|
3
|
+
declare const OPENAI_DEFAULT_MODEL = "gpt-transcribe";
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
interface OpenAIVoiceInputProviderOptions {
|
|
7
|
+
readonly tokenEndpoint: string | URL;
|
|
8
|
+
readonly model?: string;
|
|
9
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
10
|
+
readonly webSocket?: typeof globalThis.WebSocket;
|
|
11
|
+
readonly realtimeUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
declare function openai(options: OpenAIVoiceInputProviderOptions): VoiceInputProviderV1;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { OPENAI_DEFAULT_MODEL, OpenAIVoiceInputProviderOptions, openai };
|
|
16
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/session-config.ts","../src/index.ts"],"mappings":";;cAMa;;;UCcI;WACN,wBAAwB;WACxB;WACA,eAAe,WAAW;WAC1B,mBAAmB,WAAW;WAC9B;;iBAcK,OACd,SAAS,kCACR"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { VoiceInputProviderV1 } from "@voiceinput/provider";
|
|
2
|
+
//#region src/session-config.d.ts
|
|
3
|
+
declare const OPENAI_DEFAULT_MODEL = "gpt-transcribe";
|
|
4
|
+
//#endregion
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
interface OpenAIVoiceInputProviderOptions {
|
|
7
|
+
readonly tokenEndpoint: string | URL;
|
|
8
|
+
readonly model?: string;
|
|
9
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
10
|
+
readonly webSocket?: typeof globalThis.WebSocket;
|
|
11
|
+
readonly realtimeUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
declare function openai(options: OpenAIVoiceInputProviderOptions): VoiceInputProviderV1;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { OPENAI_DEFAULT_MODEL, OpenAIVoiceInputProviderOptions, openai };
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/session-config.ts","../src/index.ts"],"mappings":";;cAMa;;;UCcI;WACN,wBAAwB;WACxB;WACA,eAAe,WAAW;WAC1B,mBAAmB,WAAW;WAC9B;;iBAcK,OACd,SAAS,kCACR"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
import { i as validateOpenAITokenRequest, n as OPENAI_SAMPLE_RATE, r as createOpenAITranscriptionSession, t as OPENAI_DEFAULT_MODEL } from "./session-config-CuDb4m9Q.js";
|
|
2
|
+
import { sendWithBackpressure } from "@voiceinput/provider/transport";
|
|
3
|
+
import { VoiceInputError } from "@voiceinput/provider";
|
|
4
|
+
//#region src/index.ts
|
|
5
|
+
const DEFAULT_REALTIME_URL = "wss://api.openai.com/v1/realtime";
|
|
6
|
+
function openai(options) {
|
|
7
|
+
const model = validateFactoryString(options.model ?? "gpt-transcribe", "model");
|
|
8
|
+
const tokenEndpoint = validateFactoryString(String(options.tokenEndpoint), "tokenEndpoint");
|
|
9
|
+
const realtimeUrl = validateFactoryString(options.realtimeUrl ?? DEFAULT_REALTIME_URL, "realtimeUrl");
|
|
10
|
+
const fetchImplementation = options.fetch ?? globalThis.fetch;
|
|
11
|
+
const WebSocketImplementation = options.webSocket ?? globalThis.WebSocket;
|
|
12
|
+
const validateProviderOptions = (transcriptionOptions) => validateOptions(transcriptionOptions, model);
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
specificationVersion: "v1",
|
|
15
|
+
provider: "openai",
|
|
16
|
+
modelId: model,
|
|
17
|
+
sampleRate: OPENAI_SAMPLE_RATE,
|
|
18
|
+
validateOptions: validateProviderOptions,
|
|
19
|
+
async doOpen(callOptions) {
|
|
20
|
+
validateProviderOptions(callOptions);
|
|
21
|
+
return await openSession({
|
|
22
|
+
callOptions,
|
|
23
|
+
fetchImplementation,
|
|
24
|
+
model,
|
|
25
|
+
realtimeUrl,
|
|
26
|
+
tokenEndpoint,
|
|
27
|
+
WebSocketImplementation
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function validateOptions(options, model) {
|
|
33
|
+
try {
|
|
34
|
+
validateOpenAITokenRequest({
|
|
35
|
+
model,
|
|
36
|
+
...options
|
|
37
|
+
});
|
|
38
|
+
} catch (cause) {
|
|
39
|
+
if (VoiceInputError.isInstance(cause)) throw cause;
|
|
40
|
+
throw new VoiceInputError({
|
|
41
|
+
code: "invalid-configuration",
|
|
42
|
+
message: cause instanceof Error ? cause.message : "Invalid OpenAI transcription options.",
|
|
43
|
+
provider: "openai",
|
|
44
|
+
cause
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function openSession(options) {
|
|
49
|
+
const { abortSignal } = options.callOptions;
|
|
50
|
+
throwIfAborted(abortSignal);
|
|
51
|
+
if (typeof options.fetchImplementation !== "function") throw unsupportedBrowserFeature("fetch");
|
|
52
|
+
if (typeof options.WebSocketImplementation !== "function") throw unsupportedBrowserFeature("WebSocket");
|
|
53
|
+
const tokenRequest = validateOpenAITokenRequest({
|
|
54
|
+
model: options.model,
|
|
55
|
+
...options.callOptions
|
|
56
|
+
});
|
|
57
|
+
const credential = await requestCredential(options.fetchImplementation, options.tokenEndpoint, tokenRequest, abortSignal);
|
|
58
|
+
throwIfAborted(abortSignal);
|
|
59
|
+
return await createSession(new options.WebSocketImplementation(options.realtimeUrl, ["realtime", `openai-insecure-api-key.${credential.value}`]), tokenRequest, abortSignal);
|
|
60
|
+
}
|
|
61
|
+
async function requestCredential(fetchImplementation, tokenEndpoint, tokenRequest, abortSignal) {
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
response = await fetchImplementation(tokenEndpoint, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "Content-Type": "application/json" },
|
|
67
|
+
body: JSON.stringify(tokenRequest),
|
|
68
|
+
credentials: "same-origin",
|
|
69
|
+
signal: abortSignal
|
|
70
|
+
});
|
|
71
|
+
} catch (cause) {
|
|
72
|
+
throwIfAborted(abortSignal);
|
|
73
|
+
throw new VoiceInputError({
|
|
74
|
+
code: "network-error",
|
|
75
|
+
message: "Unable to reach the OpenAI token endpoint.",
|
|
76
|
+
provider: "openai",
|
|
77
|
+
retryable: true,
|
|
78
|
+
cause
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (!response.ok) throw await tokenResponseError(response);
|
|
82
|
+
let value;
|
|
83
|
+
try {
|
|
84
|
+
value = await response.json();
|
|
85
|
+
} catch (cause) {
|
|
86
|
+
throw new VoiceInputError({
|
|
87
|
+
code: "token-error",
|
|
88
|
+
message: "The OpenAI token endpoint returned invalid JSON.",
|
|
89
|
+
provider: "openai",
|
|
90
|
+
cause
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (!isRecord(value) || typeof value["value"] !== "string" || value["value"].length === 0 || typeof value["expires_at"] !== "number" || !Number.isFinite(value["expires_at"])) throw new VoiceInputError({
|
|
94
|
+
code: "token-error",
|
|
95
|
+
message: "The OpenAI token endpoint returned an invalid credential.",
|
|
96
|
+
provider: "openai"
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
value: value["value"],
|
|
100
|
+
expires_at: value["expires_at"]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function tokenResponseError(response) {
|
|
104
|
+
const retryAfterMs = parseRetryAfter(response.headers.get("Retry-After"));
|
|
105
|
+
if (response.status === 401 || response.status === 403) return new VoiceInputError({
|
|
106
|
+
code: "unauthorized",
|
|
107
|
+
message: "The OpenAI token endpoint rejected this request.",
|
|
108
|
+
provider: "openai"
|
|
109
|
+
});
|
|
110
|
+
if (response.status === 429) return new VoiceInputError({
|
|
111
|
+
code: "rate-limited",
|
|
112
|
+
message: "The OpenAI token endpoint rate limit was exceeded.",
|
|
113
|
+
provider: "openai",
|
|
114
|
+
retryable: true,
|
|
115
|
+
...retryAfterMs === void 0 ? {} : { retryAfterMs }
|
|
116
|
+
});
|
|
117
|
+
const safeError = await readSafeTokenError(response);
|
|
118
|
+
if (safeError !== void 0) return new VoiceInputError({
|
|
119
|
+
...safeError,
|
|
120
|
+
provider: "openai"
|
|
121
|
+
});
|
|
122
|
+
return new VoiceInputError({
|
|
123
|
+
code: "token-error",
|
|
124
|
+
message: "The OpenAI token endpoint did not issue a credential.",
|
|
125
|
+
provider: "openai",
|
|
126
|
+
retryable: response.status >= 500
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async function readSafeTokenError(response) {
|
|
130
|
+
if (response.status !== 400 || response.headers.get("X-VoiceInput-Error") !== "1" || response.headers.get("Content-Type")?.split(";", 1)[0]?.trim() !== "application/json") return;
|
|
131
|
+
const text = await readBoundedErrorText(response);
|
|
132
|
+
if (text === void 0) return void 0;
|
|
133
|
+
try {
|
|
134
|
+
const value = JSON.parse(text);
|
|
135
|
+
const error = isRecord(value) ? value["error"] : void 0;
|
|
136
|
+
if (!isRecord(error) || error["code"] !== "invalid-configuration" && error["code"] !== "unsupported-feature" || typeof error["message"] !== "string" || error["message"].length === 0 || error["message"].length > 1e3) return;
|
|
137
|
+
return {
|
|
138
|
+
code: error["code"],
|
|
139
|
+
message: error["message"]
|
|
140
|
+
};
|
|
141
|
+
} catch {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function readBoundedErrorText(response) {
|
|
146
|
+
const reader = response.body?.getReader();
|
|
147
|
+
if (reader === void 0) return void 0;
|
|
148
|
+
const decoder = new TextDecoder();
|
|
149
|
+
let bytesRead = 0;
|
|
150
|
+
let text = "";
|
|
151
|
+
try {
|
|
152
|
+
while (true) {
|
|
153
|
+
const { done, value } = await reader.read();
|
|
154
|
+
if (done) return text + decoder.decode();
|
|
155
|
+
bytesRead += value.byteLength;
|
|
156
|
+
if (bytesRead > 4096) {
|
|
157
|
+
await reader.cancel().catch(() => {});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
text += decoder.decode(value, { stream: true });
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function createSession(socket, tokenRequest, abortSignal) {
|
|
167
|
+
let streamController;
|
|
168
|
+
const stream = new ReadableStream({ start(controller) {
|
|
169
|
+
streamController = controller;
|
|
170
|
+
} });
|
|
171
|
+
if (streamController === void 0) throw new VoiceInputError({
|
|
172
|
+
code: "provider-error",
|
|
173
|
+
message: "Unable to initialize the OpenAI transcript stream.",
|
|
174
|
+
provider: "openai"
|
|
175
|
+
});
|
|
176
|
+
const transcripts = /* @__PURE__ */ new Map();
|
|
177
|
+
const transcriptOrder = [];
|
|
178
|
+
const closedItems = /* @__PURE__ */ new Set();
|
|
179
|
+
let audioSent = false;
|
|
180
|
+
let closed = false;
|
|
181
|
+
let failed = false;
|
|
182
|
+
let finishing = false;
|
|
183
|
+
let manualCommitPending = false;
|
|
184
|
+
let vadCommitPending = false;
|
|
185
|
+
const manualCommitEventId = "voiceinput-finish";
|
|
186
|
+
const closeStream = () => {
|
|
187
|
+
if (closed) return;
|
|
188
|
+
closed = true;
|
|
189
|
+
abortSignal.removeEventListener("abort", abort);
|
|
190
|
+
streamController?.close();
|
|
191
|
+
};
|
|
192
|
+
const closeSocket = (reason) => {
|
|
193
|
+
if (socket.readyState === 1 || socket.readyState === 0) socket.close(1e3, reason);
|
|
194
|
+
};
|
|
195
|
+
const finishCleanly = () => {
|
|
196
|
+
closeStream();
|
|
197
|
+
closeSocket("finished");
|
|
198
|
+
};
|
|
199
|
+
const fail = (error) => {
|
|
200
|
+
if (closed || failed) return;
|
|
201
|
+
failed = true;
|
|
202
|
+
streamController?.enqueue({
|
|
203
|
+
type: "error",
|
|
204
|
+
error
|
|
205
|
+
});
|
|
206
|
+
closeStream();
|
|
207
|
+
closeSocket("aborted");
|
|
208
|
+
};
|
|
209
|
+
const maybeFinish = () => {
|
|
210
|
+
if (finishing && !manualCommitPending && !vadCommitPending && transcripts.size === 0) finishCleanly();
|
|
211
|
+
};
|
|
212
|
+
const emitInterim = (state) => {
|
|
213
|
+
if (state.delta && state.delta !== state.lastEmittedInterim) {
|
|
214
|
+
state.lastEmittedInterim = state.delta;
|
|
215
|
+
streamController?.enqueue({
|
|
216
|
+
type: "interim",
|
|
217
|
+
text: state.delta,
|
|
218
|
+
segmentId: transcriptOrder[0]
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
const flushFinals = () => {
|
|
223
|
+
while (transcriptOrder.length > 0) {
|
|
224
|
+
const itemId = transcriptOrder[0];
|
|
225
|
+
const state = transcripts.get(itemId);
|
|
226
|
+
if (state?.final === void 0) {
|
|
227
|
+
if (state !== void 0) emitInterim(state);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
transcriptOrder.shift();
|
|
231
|
+
transcripts.delete(itemId);
|
|
232
|
+
closedItems.add(itemId);
|
|
233
|
+
streamController?.enqueue({
|
|
234
|
+
type: "final",
|
|
235
|
+
text: state.final,
|
|
236
|
+
segmentId: itemId
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
maybeFinish();
|
|
240
|
+
};
|
|
241
|
+
const ensureTranscript = (itemId) => {
|
|
242
|
+
let state = transcripts.get(itemId);
|
|
243
|
+
if (state === void 0) {
|
|
244
|
+
state = { delta: "" };
|
|
245
|
+
transcripts.set(itemId, state);
|
|
246
|
+
transcriptOrder.push(itemId);
|
|
247
|
+
}
|
|
248
|
+
return state;
|
|
249
|
+
};
|
|
250
|
+
const handleMessage = (event) => {
|
|
251
|
+
if (closed) return;
|
|
252
|
+
try {
|
|
253
|
+
const value = JSON.parse(String(event.data));
|
|
254
|
+
if (!isRecord(value) || typeof value["type"] !== "string") throw new TypeError("OpenAI sent an invalid Realtime event.");
|
|
255
|
+
const type = value["type"];
|
|
256
|
+
if (type === "input_audio_buffer.speech_started") {
|
|
257
|
+
streamController?.enqueue({ type: "speech-start" });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (type === "input_audio_buffer.speech_stopped") {
|
|
261
|
+
vadCommitPending = true;
|
|
262
|
+
streamController?.enqueue({ type: "speech-end" });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (type === "input_audio_buffer.committed") {
|
|
266
|
+
const itemId = readString(value, "item_id");
|
|
267
|
+
if (closedItems.has(itemId)) return;
|
|
268
|
+
manualCommitPending = false;
|
|
269
|
+
vadCommitPending = false;
|
|
270
|
+
ensureTranscript(itemId);
|
|
271
|
+
flushFinals();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (type === "conversation.item.input_audio_transcription.delta") {
|
|
275
|
+
const itemId = readString(value, "item_id");
|
|
276
|
+
if (closedItems.has(itemId)) return;
|
|
277
|
+
const state = ensureTranscript(itemId);
|
|
278
|
+
state.delta += readString(value, "delta");
|
|
279
|
+
if (transcriptOrder[0] === itemId) emitInterim(state);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (type === "conversation.item.input_audio_transcription.completed") {
|
|
283
|
+
const itemId = readString(value, "item_id");
|
|
284
|
+
if (closedItems.has(itemId)) return;
|
|
285
|
+
ensureTranscript(itemId).final = readString(value, "transcript");
|
|
286
|
+
flushFinals();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (type === "conversation.item.input_audio_transcription.failed") {
|
|
290
|
+
fail(normalizeTranscriptionFailure(value));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (type === "error") {
|
|
294
|
+
if (isExpectedEmptyCommitError(value, manualCommitEventId)) {
|
|
295
|
+
manualCommitPending = false;
|
|
296
|
+
maybeFinish();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
fail(normalizeRealtimeError(value));
|
|
300
|
+
}
|
|
301
|
+
} catch (cause) {
|
|
302
|
+
fail(new VoiceInputError({
|
|
303
|
+
code: "provider-error",
|
|
304
|
+
message: "OpenAI sent an invalid Realtime event.",
|
|
305
|
+
provider: "openai",
|
|
306
|
+
cause
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const handleClose = (event) => {
|
|
311
|
+
if (closed) return;
|
|
312
|
+
if (event.code === 1e3) closeStream();
|
|
313
|
+
else fail(new VoiceInputError({
|
|
314
|
+
code: "network-error",
|
|
315
|
+
message: "The OpenAI Realtime connection closed unexpectedly.",
|
|
316
|
+
provider: "openai",
|
|
317
|
+
retryable: true,
|
|
318
|
+
cause: event
|
|
319
|
+
}));
|
|
320
|
+
};
|
|
321
|
+
const abort = () => {
|
|
322
|
+
if (closed) return;
|
|
323
|
+
closeStream();
|
|
324
|
+
closeSocket("aborted");
|
|
325
|
+
};
|
|
326
|
+
socket.addEventListener("message", handleMessage);
|
|
327
|
+
socket.addEventListener("close", handleClose);
|
|
328
|
+
socket.addEventListener("error", () => {
|
|
329
|
+
fail(new VoiceInputError({
|
|
330
|
+
code: "network-error",
|
|
331
|
+
message: "The OpenAI Realtime connection failed.",
|
|
332
|
+
provider: "openai",
|
|
333
|
+
retryable: true
|
|
334
|
+
}));
|
|
335
|
+
});
|
|
336
|
+
abortSignal.addEventListener("abort", abort, { once: true });
|
|
337
|
+
await waitForOpen(socket, abortSignal);
|
|
338
|
+
sendEvent(socket, {
|
|
339
|
+
type: "session.update",
|
|
340
|
+
session: createOpenAITranscriptionSession(tokenRequest)
|
|
341
|
+
});
|
|
342
|
+
return {
|
|
343
|
+
stream,
|
|
344
|
+
sendAudio(chunk) {
|
|
345
|
+
if (closed || finishing) return;
|
|
346
|
+
audioSent ||= chunk.length > 0;
|
|
347
|
+
if (chunk.length > 0) return sendWithBackpressure(socket, Math.ceil(chunk.byteLength * 4 / 3) + 128, abortSignal, "openai", () => sendEvent(socket, {
|
|
348
|
+
type: "input_audio_buffer.append",
|
|
349
|
+
audio: encodePcm16(chunk)
|
|
350
|
+
}));
|
|
351
|
+
},
|
|
352
|
+
finish() {
|
|
353
|
+
if (closed || finishing) return;
|
|
354
|
+
finishing = true;
|
|
355
|
+
if (!audioSent) {
|
|
356
|
+
finishCleanly();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!vadCommitPending) {
|
|
360
|
+
manualCommitPending = true;
|
|
361
|
+
sendEvent(socket, {
|
|
362
|
+
type: "input_audio_buffer.commit",
|
|
363
|
+
event_id: manualCommitEventId
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
maybeFinish();
|
|
367
|
+
},
|
|
368
|
+
abort
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function waitForOpen(socket, abortSignal) {
|
|
372
|
+
if (socket.readyState === 1) return Promise.resolve();
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
const cleanup = () => {
|
|
375
|
+
socket.removeEventListener("open", handleOpen);
|
|
376
|
+
socket.removeEventListener("error", handleError);
|
|
377
|
+
abortSignal.removeEventListener("abort", handleAbort);
|
|
378
|
+
};
|
|
379
|
+
const handleOpen = () => {
|
|
380
|
+
cleanup();
|
|
381
|
+
resolve();
|
|
382
|
+
};
|
|
383
|
+
const handleError = (event) => {
|
|
384
|
+
cleanup();
|
|
385
|
+
reject(new VoiceInputError({
|
|
386
|
+
code: "network-error",
|
|
387
|
+
message: "Unable to open the OpenAI Realtime connection.",
|
|
388
|
+
provider: "openai",
|
|
389
|
+
retryable: true,
|
|
390
|
+
cause: event
|
|
391
|
+
}));
|
|
392
|
+
};
|
|
393
|
+
const handleAbort = () => {
|
|
394
|
+
cleanup();
|
|
395
|
+
reject(abortSignal.reason);
|
|
396
|
+
};
|
|
397
|
+
socket.addEventListener("open", handleOpen, { once: true });
|
|
398
|
+
socket.addEventListener("error", handleError, { once: true });
|
|
399
|
+
abortSignal.addEventListener("abort", handleAbort, { once: true });
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function sendEvent(socket, event) {
|
|
403
|
+
try {
|
|
404
|
+
socket.send(JSON.stringify(event));
|
|
405
|
+
} catch (cause) {
|
|
406
|
+
throw new VoiceInputError({
|
|
407
|
+
code: "network-error",
|
|
408
|
+
message: "Unable to send data to OpenAI Realtime.",
|
|
409
|
+
provider: "openai",
|
|
410
|
+
retryable: true,
|
|
411
|
+
cause
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function encodePcm16(chunk) {
|
|
416
|
+
const bytes = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
417
|
+
let binary = "";
|
|
418
|
+
for (let offset = 0; offset < bytes.length; offset += 8192) binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));
|
|
419
|
+
return btoa(binary);
|
|
420
|
+
}
|
|
421
|
+
function normalizeRealtimeError(value) {
|
|
422
|
+
const error = isRecord(value["error"]) ? value["error"] : value;
|
|
423
|
+
const code = typeof error["code"] === "string" ? error["code"] : "";
|
|
424
|
+
const message = typeof error["message"] === "string" ? error["message"] : "OpenAI Realtime reported an error.";
|
|
425
|
+
const rateLimited = code.includes("rate_limit");
|
|
426
|
+
return new VoiceInputError({
|
|
427
|
+
code: rateLimited ? "rate-limited" : "provider-error",
|
|
428
|
+
message,
|
|
429
|
+
provider: "openai",
|
|
430
|
+
retryable: rateLimited || code.includes("server_error"),
|
|
431
|
+
cause: value
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
function normalizeTranscriptionFailure(value) {
|
|
435
|
+
const error = isRecord(value["error"]) ? value["error"] : {};
|
|
436
|
+
return new VoiceInputError({
|
|
437
|
+
code: "provider-error",
|
|
438
|
+
message: typeof error["message"] === "string" ? error["message"] : "OpenAI could not transcribe an audio turn.",
|
|
439
|
+
provider: "openai",
|
|
440
|
+
retryable: typeof error["code"] === "string" && error["code"].includes("server_error"),
|
|
441
|
+
cause: value
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function isExpectedEmptyCommitError(value, eventId) {
|
|
445
|
+
const error = isRecord(value["error"]) ? value["error"] : {};
|
|
446
|
+
return error["code"] === "input_audio_buffer_commit_empty" && error["event_id"] === eventId;
|
|
447
|
+
}
|
|
448
|
+
function readString(value, key) {
|
|
449
|
+
const result = value[key];
|
|
450
|
+
if (typeof result !== "string") throw new TypeError(`${key} must be a string.`);
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
function parseRetryAfter(value) {
|
|
454
|
+
if (value === null) return;
|
|
455
|
+
const seconds = Number(value);
|
|
456
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1e3);
|
|
457
|
+
const date = Date.parse(value);
|
|
458
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - Date.now());
|
|
459
|
+
}
|
|
460
|
+
function throwIfAborted(signal) {
|
|
461
|
+
if (signal.aborted) throw signal.reason;
|
|
462
|
+
}
|
|
463
|
+
function validateFactoryString(value, name) {
|
|
464
|
+
if (value.trim().length === 0) throw new VoiceInputError({
|
|
465
|
+
code: "invalid-configuration",
|
|
466
|
+
message: `${name} must be a non-empty string.`,
|
|
467
|
+
provider: "openai"
|
|
468
|
+
});
|
|
469
|
+
return value;
|
|
470
|
+
}
|
|
471
|
+
function unsupportedBrowserFeature(feature) {
|
|
472
|
+
return new VoiceInputError({
|
|
473
|
+
code: "unsupported-browser",
|
|
474
|
+
message: `OpenAI voice input requires browser ${feature} support.`,
|
|
475
|
+
provider: "openai"
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
function isRecord(value) {
|
|
479
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
480
|
+
}
|
|
481
|
+
//#endregion
|
|
482
|
+
export { OPENAI_DEFAULT_MODEL, openai };
|
|
483
|
+
|
|
484
|
+
//# sourceMappingURL=index.js.map
|