@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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"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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"}
@@ -0,0 +1,165 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_session_config = require("./session-config-CmoDDiY6.cjs");
3
+ let _voiceinput_provider = require("@voiceinput/provider");
4
+ //#region src/server.ts
5
+ const DEFAULT_CLIENT_SECRET_URL = "https://api.openai.com/v1/realtime/client_secrets";
6
+ const MAX_TOKEN_REQUEST_BYTES = 16384;
7
+ function createOpenAITokenHandler(options) {
8
+ const defaultModel = validateNonEmpty(options.model ?? "gpt-transcribe", "model");
9
+ const allowedModels = new Set(options.allowedModels ?? [defaultModel]);
10
+ if (!allowedModels.has(defaultModel)) throw new TypeError("allowedModels must contain the default model.");
11
+ if (typeof options.authorize !== "function") throw new TypeError("authorize must be a function.");
12
+ const apiKey = validateNonEmpty(options.apiKey, "apiKey");
13
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
14
+ const clientSecretUrl = options.clientSecretUrl ?? DEFAULT_CLIENT_SECRET_URL;
15
+ return async (request) => {
16
+ if (request.method !== "POST") return jsonError(405, "invalid-request", "Method not allowed.", { Allow: "POST" });
17
+ try {
18
+ const requestBody = await readRequestBody(request);
19
+ const authorization = await options.authorize(copyRequest(request, requestBody));
20
+ if (authorization === null) return jsonError(401, "unauthorized", "Unauthorized.");
21
+ const subject = validateNonEmpty(authorization.subject, "subject");
22
+ const tokenRequest = readTokenRequest(requestBody, defaultModel);
23
+ if (!allowedModels.has(tokenRequest.model)) return jsonError(400, "invalid-configuration", "The requested OpenAI model is not allowed.");
24
+ const createContext = () => ({
25
+ request: copyRequest(request, requestBody),
26
+ subject,
27
+ model: tokenRequest.model
28
+ });
29
+ const rateLimit = await options.rateLimit?.(createContext());
30
+ if (rateLimit?.allowed === false) {
31
+ const retryAfter = normalizeRetryAfter(rateLimit.retryAfterSeconds);
32
+ return jsonError(429, "rate-limited", "Rate limit exceeded.", retryAfter === void 0 ? {} : { "Retry-After": String(retryAfter) });
33
+ }
34
+ const safetyIdentifier = await options.safetyIdentifier?.(createContext());
35
+ const response = await fetchImplementation(clientSecretUrl, {
36
+ method: "POST",
37
+ headers: {
38
+ Authorization: `Bearer ${apiKey}`,
39
+ "Content-Type": "application/json",
40
+ ...options.organization === void 0 ? {} : { "OpenAI-Organization": options.organization },
41
+ ...options.project === void 0 ? {} : { "OpenAI-Project": options.project },
42
+ ...safetyIdentifier === void 0 ? {} : { "OpenAI-Safety-Identifier": validateNonEmpty(safetyIdentifier, "safetyIdentifier") }
43
+ },
44
+ body: JSON.stringify({ session: require_session_config.createOpenAITranscriptionSession(tokenRequest) }),
45
+ signal: request.signal
46
+ });
47
+ if (!response.ok) {
48
+ if (response.status === 429) return jsonError(429, "rate-limited", "OpenAI rate limit exceeded.", copyRetryAfter(response.headers));
49
+ return jsonError(502, "token-error", "OpenAI did not issue a client credential.");
50
+ }
51
+ const credential = validateCredential(await response.json());
52
+ await options.onTokenIssued?.({
53
+ provider: "openai",
54
+ subject,
55
+ model: tokenRequest.model,
56
+ expiresAt: credential.expires_at
57
+ });
58
+ return Response.json(credential, { headers: { "Cache-Control": "no-store" } });
59
+ } catch (error) {
60
+ if (request.signal.aborted) return jsonError(499, "network-error", "The request was cancelled.");
61
+ if (error instanceof InvalidTokenRequestError) return jsonError(error.status, error.code, error.message);
62
+ return jsonError(500, "token-error", "Unable to issue an OpenAI client credential.");
63
+ }
64
+ };
65
+ }
66
+ async function readRequestBody(request) {
67
+ if (request.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") throw new InvalidTokenRequestError("Content-Type must be application/json.", "invalid-request", 415);
68
+ if (Number(request.headers.get("Content-Length")) > MAX_TOKEN_REQUEST_BYTES) throw requestTooLarge();
69
+ const reader = request.body?.getReader();
70
+ if (reader === void 0) return "";
71
+ const decoder = new TextDecoder("utf-8", { fatal: true });
72
+ let bytesRead = 0;
73
+ let text = "";
74
+ while (true) {
75
+ const { done, value } = await reader.read();
76
+ if (done) break;
77
+ bytesRead += value.byteLength;
78
+ if (bytesRead > MAX_TOKEN_REQUEST_BYTES) {
79
+ await reader.cancel().catch(() => {});
80
+ throw requestTooLarge();
81
+ }
82
+ try {
83
+ text += decoder.decode(value, { stream: true });
84
+ } catch {
85
+ await reader.cancel().catch(() => {});
86
+ throw invalidUtf8();
87
+ }
88
+ }
89
+ try {
90
+ text += decoder.decode();
91
+ } catch {
92
+ throw invalidUtf8();
93
+ }
94
+ return text;
95
+ }
96
+ function readTokenRequest(body, defaultModel) {
97
+ try {
98
+ return require_session_config.validateOpenAITokenRequest(body.length === 0 ? {} : JSON.parse(body), defaultModel);
99
+ } catch (cause) {
100
+ if (cause instanceof SyntaxError || cause instanceof TypeError) throw new InvalidTokenRequestError(cause.message);
101
+ if (_voiceinput_provider.VoiceInputError.isInstance(cause) && (cause.code === "invalid-configuration" || cause.code === "unsupported-feature")) throw new InvalidTokenRequestError(cause.message, cause.code);
102
+ throw cause;
103
+ }
104
+ }
105
+ function copyRequest(request, body) {
106
+ return new Request(request, {
107
+ method: "POST",
108
+ body,
109
+ referrer: request.referrer,
110
+ referrerPolicy: request.referrerPolicy
111
+ });
112
+ }
113
+ var InvalidTokenRequestError = class extends Error {
114
+ code;
115
+ status;
116
+ constructor(message, code = "invalid-configuration", status = 400) {
117
+ super(message);
118
+ this.code = code;
119
+ this.status = status;
120
+ this.name = "InvalidTokenRequestError";
121
+ }
122
+ };
123
+ function requestTooLarge() {
124
+ return new InvalidTokenRequestError(`Token request body exceeds ${MAX_TOKEN_REQUEST_BYTES} bytes.`, "invalid-request", 413);
125
+ }
126
+ function invalidUtf8() {
127
+ return new InvalidTokenRequestError("Token request body must be valid UTF-8.", "invalid-request");
128
+ }
129
+ function validateCredential(value) {
130
+ if (!isRecord(value) || typeof value["value"] !== "string" || value["value"].length === 0 || typeof value["expires_at"] !== "number" || !Number.isFinite(value["expires_at"])) throw new Error("OpenAI returned an invalid client credential.");
131
+ return {
132
+ value: value["value"],
133
+ expires_at: value["expires_at"]
134
+ };
135
+ }
136
+ function jsonError(status, code, message, headers = {}) {
137
+ const responseHeaders = new Headers(headers);
138
+ responseHeaders.set("Cache-Control", "no-store");
139
+ responseHeaders.set("X-VoiceInput-Error", "1");
140
+ return Response.json({ error: {
141
+ code,
142
+ message
143
+ } }, {
144
+ status,
145
+ headers: responseHeaders
146
+ });
147
+ }
148
+ function copyRetryAfter(headers) {
149
+ const retryAfter = headers.get("Retry-After");
150
+ return retryAfter === null ? {} : { "Retry-After": retryAfter };
151
+ }
152
+ function normalizeRetryAfter(value) {
153
+ return value === void 0 || !Number.isFinite(value) || value <= 0 ? void 0 : Math.ceil(value);
154
+ }
155
+ function validateNonEmpty(value, name) {
156
+ if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${name} must be a non-empty string.`);
157
+ return value;
158
+ }
159
+ function isRecord(value) {
160
+ return typeof value === "object" && value !== null && !Array.isArray(value);
161
+ }
162
+ //#endregion
163
+ exports.createOpenAITokenHandler = createOpenAITokenHandler;
164
+
165
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.cjs","names":["createOpenAITranscriptionSession","validateOpenAITokenRequest","VoiceInputError"],"sources":["../src/server.ts"],"sourcesContent":["import { VoiceInputError } from \"@voiceinput/provider\";\n\nimport {\n OPENAI_DEFAULT_MODEL,\n createOpenAITranscriptionSession,\n validateOpenAITokenRequest,\n} from \"./session-config.js\";\n\nconst DEFAULT_CLIENT_SECRET_URL =\n \"https://api.openai.com/v1/realtime/client_secrets\";\nconst MAX_TOKEN_REQUEST_BYTES = 16 * 1024;\n\nexport interface OpenAIAuthorization {\n readonly subject: string;\n}\n\nexport type OpenAIRateLimitResult =\n | { readonly allowed: true }\n | { readonly allowed: false; readonly retryAfterSeconds?: number };\n\nexport interface OpenAITokenHandlerContext {\n readonly request: Request;\n readonly subject: string;\n readonly model: string;\n}\n\nexport interface OpenAITokenIssuedMetadata {\n readonly provider: \"openai\";\n readonly subject: string;\n readonly model: string;\n readonly expiresAt: number;\n}\n\nexport interface CreateOpenAITokenHandlerOptions {\n readonly apiKey: string;\n readonly authorize: (\n request: Request,\n ) => PromiseLike<OpenAIAuthorization | null> | OpenAIAuthorization | null;\n readonly model?: string;\n readonly allowedModels?: readonly string[];\n readonly organization?: string;\n readonly project?: string;\n readonly safetyIdentifier?: (\n context: OpenAITokenHandlerContext,\n ) => PromiseLike<string | undefined> | string | undefined;\n readonly rateLimit?: (\n context: OpenAITokenHandlerContext,\n ) => PromiseLike<OpenAIRateLimitResult> | OpenAIRateLimitResult;\n readonly onTokenIssued?: (\n metadata: OpenAITokenIssuedMetadata,\n ) => PromiseLike<void> | void;\n readonly fetch?: typeof globalThis.fetch;\n readonly clientSecretUrl?: string;\n}\n\nexport function createOpenAITokenHandler(\n options: CreateOpenAITokenHandlerOptions,\n): (request: Request) => Promise<Response> {\n const defaultModel = validateNonEmpty(\n options.model ?? OPENAI_DEFAULT_MODEL,\n \"model\",\n );\n const allowedModels = new Set(options.allowedModels ?? [defaultModel]);\n if (!allowedModels.has(defaultModel)) {\n throw new TypeError(\"allowedModels must contain the default model.\");\n }\n if (typeof options.authorize !== \"function\") {\n throw new TypeError(\"authorize must be a function.\");\n }\n const apiKey = validateNonEmpty(options.apiKey, \"apiKey\");\n const fetchImplementation = options.fetch ?? globalThis.fetch;\n const clientSecretUrl = options.clientSecretUrl ?? DEFAULT_CLIENT_SECRET_URL;\n\n return async (request) => {\n if (request.method !== \"POST\") {\n return jsonError(405, \"invalid-request\", \"Method not allowed.\", {\n Allow: \"POST\",\n });\n }\n\n try {\n const requestBody = await readRequestBody(request);\n const authorization = await options.authorize(\n copyRequest(request, requestBody),\n );\n if (authorization === null) {\n return jsonError(401, \"unauthorized\", \"Unauthorized.\");\n }\n const subject = validateNonEmpty(authorization.subject, \"subject\");\n const tokenRequest = readTokenRequest(requestBody, defaultModel);\n if (!allowedModels.has(tokenRequest.model)) {\n return jsonError(\n 400,\n \"invalid-configuration\",\n \"The requested OpenAI model is not allowed.\",\n );\n }\n const createContext = (): OpenAITokenHandlerContext => ({\n request: copyRequest(request, requestBody),\n subject,\n model: tokenRequest.model,\n });\n const rateLimit = await options.rateLimit?.(createContext());\n if (rateLimit?.allowed === false) {\n const retryAfter = normalizeRetryAfter(rateLimit.retryAfterSeconds);\n return jsonError(\n 429,\n \"rate-limited\",\n \"Rate limit exceeded.\",\n retryAfter === undefined ? {} : { \"Retry-After\": String(retryAfter) },\n );\n }\n\n const safetyIdentifier =\n await options.safetyIdentifier?.(createContext());\n const response = await fetchImplementation(clientSecretUrl, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n ...(options.organization === undefined\n ? {}\n : { \"OpenAI-Organization\": options.organization }),\n ...(options.project === undefined\n ? {}\n : { \"OpenAI-Project\": options.project }),\n ...(safetyIdentifier === undefined\n ? {}\n : {\n \"OpenAI-Safety-Identifier\": validateNonEmpty(\n safetyIdentifier,\n \"safetyIdentifier\",\n ),\n }),\n },\n body: JSON.stringify({\n session: createOpenAITranscriptionSession(tokenRequest),\n }),\n signal: request.signal,\n });\n\n if (!response.ok) {\n if (response.status === 429) {\n return jsonError(\n 429,\n \"rate-limited\",\n \"OpenAI rate limit exceeded.\",\n copyRetryAfter(response.headers),\n );\n }\n return jsonError(\n 502,\n \"token-error\",\n \"OpenAI did not issue a client credential.\",\n );\n }\n\n const credential = validateCredential(await response.json());\n await options.onTokenIssued?.({\n provider: \"openai\",\n subject,\n model: tokenRequest.model,\n expiresAt: credential.expires_at,\n });\n\n return Response.json(credential, {\n headers: { \"Cache-Control\": \"no-store\" },\n });\n } catch (error) {\n if (request.signal.aborted) {\n return jsonError(499, \"network-error\", \"The request was cancelled.\");\n }\n if (error instanceof InvalidTokenRequestError) {\n return jsonError(error.status, error.code, error.message);\n }\n return jsonError(\n 500,\n \"token-error\",\n \"Unable to issue an OpenAI client credential.\",\n );\n }\n };\n}\n\nasync function readRequestBody(request: Request): Promise<string> {\n const contentType = request.headers\n .get(\"Content-Type\")\n ?.split(\";\", 1)[0]\n ?.trim()\n .toLowerCase();\n if (contentType !== \"application/json\") {\n throw new InvalidTokenRequestError(\n \"Content-Type must be application/json.\",\n \"invalid-request\",\n 415,\n );\n }\n const declaredLength = Number(request.headers.get(\"Content-Length\"));\n if (declaredLength > MAX_TOKEN_REQUEST_BYTES) {\n throw requestTooLarge();\n }\n const reader = request.body?.getReader();\n if (reader === undefined) return \"\";\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n let bytesRead = 0;\n let text = \"\";\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesRead += value.byteLength;\n if (bytesRead > MAX_TOKEN_REQUEST_BYTES) {\n await reader.cancel().catch(() => {});\n throw requestTooLarge();\n }\n try {\n text += decoder.decode(value, { stream: true });\n } catch {\n await reader.cancel().catch(() => {});\n throw invalidUtf8();\n }\n }\n try {\n text += decoder.decode();\n } catch {\n throw invalidUtf8();\n }\n return text;\n}\n\nfunction readTokenRequest(\n body: string,\n defaultModel: string,\n): ReturnType<typeof validateOpenAITokenRequest> {\n try {\n return validateOpenAITokenRequest(\n body.length === 0 ? {} : JSON.parse(body),\n defaultModel,\n );\n } catch (cause) {\n if (cause instanceof SyntaxError || cause instanceof TypeError) {\n throw new InvalidTokenRequestError(cause.message);\n }\n if (\n VoiceInputError.isInstance(cause) &&\n (cause.code === \"invalid-configuration\" ||\n cause.code === \"unsupported-feature\")\n ) {\n throw new InvalidTokenRequestError(cause.message, cause.code);\n }\n throw cause;\n }\n}\n\nfunction copyRequest(request: Request, body: string): Request {\n return new Request(request, {\n method: \"POST\",\n body,\n referrer: request.referrer,\n referrerPolicy: request.referrerPolicy,\n });\n}\n\nclass InvalidTokenRequestError extends Error {\n constructor(\n message: string,\n readonly code:\n | \"invalid-configuration\"\n | \"invalid-request\"\n | \"unsupported-feature\" = \"invalid-configuration\",\n readonly status = 400,\n ) {\n super(message);\n this.name = \"InvalidTokenRequestError\";\n }\n}\n\nfunction requestTooLarge(): InvalidTokenRequestError {\n return new InvalidTokenRequestError(\n `Token request body exceeds ${MAX_TOKEN_REQUEST_BYTES} bytes.`,\n \"invalid-request\",\n 413,\n );\n}\n\nfunction invalidUtf8(): InvalidTokenRequestError {\n return new InvalidTokenRequestError(\n \"Token request body must be valid UTF-8.\",\n \"invalid-request\",\n );\n}\n\nfunction validateCredential(value: unknown): {\n value: string;\n expires_at: number;\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 Error(\"OpenAI returned an invalid client credential.\");\n }\n return {\n value: value[\"value\"],\n expires_at: value[\"expires_at\"],\n };\n}\n\nfunction jsonError(\n status: number,\n code: string,\n message: string,\n headers: HeadersInit = {},\n): Response {\n const responseHeaders = new Headers(headers);\n responseHeaders.set(\"Cache-Control\", \"no-store\");\n responseHeaders.set(\"X-VoiceInput-Error\", \"1\");\n return Response.json(\n { error: { code, message } },\n {\n status,\n headers: responseHeaders,\n },\n );\n}\n\nfunction copyRetryAfter(headers: Headers): HeadersInit {\n const retryAfter = headers.get(\"Retry-After\");\n return retryAfter === null ? {} : { \"Retry-After\": retryAfter };\n}\n\nfunction normalizeRetryAfter(value: number | undefined): number | undefined {\n return value === undefined || !Number.isFinite(value) || value <= 0\n ? undefined\n : Math.ceil(value);\n}\n\nfunction validateNonEmpty(value: string, name: string): string {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`${name} must be a non-empty string.`);\n }\n return value;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ //#region src/server.d.ts
2
+ interface OpenAIAuthorization {
3
+ readonly subject: string;
4
+ }
5
+ type OpenAIRateLimitResult = {
6
+ readonly allowed: true;
7
+ } | {
8
+ readonly allowed: false;
9
+ readonly retryAfterSeconds?: number;
10
+ };
11
+ interface OpenAITokenHandlerContext {
12
+ readonly request: Request;
13
+ readonly subject: string;
14
+ readonly model: string;
15
+ }
16
+ interface OpenAITokenIssuedMetadata {
17
+ readonly provider: "openai";
18
+ readonly subject: string;
19
+ readonly model: string;
20
+ readonly expiresAt: number;
21
+ }
22
+ interface CreateOpenAITokenHandlerOptions {
23
+ readonly apiKey: string;
24
+ readonly authorize: (request: Request) => PromiseLike<OpenAIAuthorization | null> | OpenAIAuthorization | null;
25
+ readonly model?: string;
26
+ readonly allowedModels?: readonly string[];
27
+ readonly organization?: string;
28
+ readonly project?: string;
29
+ readonly safetyIdentifier?: (context: OpenAITokenHandlerContext) => PromiseLike<string | undefined> | string | undefined;
30
+ readonly rateLimit?: (context: OpenAITokenHandlerContext) => PromiseLike<OpenAIRateLimitResult> | OpenAIRateLimitResult;
31
+ readonly onTokenIssued?: (metadata: OpenAITokenIssuedMetadata) => PromiseLike<void> | void;
32
+ readonly fetch?: typeof globalThis.fetch;
33
+ readonly clientSecretUrl?: string;
34
+ }
35
+ declare function createOpenAITokenHandler(options: CreateOpenAITokenHandlerOptions): (request: Request) => Promise<Response>;
36
+ //#endregion
37
+ export { CreateOpenAITokenHandlerOptions, OpenAIAuthorization, OpenAIRateLimitResult, OpenAITokenHandlerContext, OpenAITokenIssuedMetadata, createOpenAITokenHandler };
38
+ //# sourceMappingURL=server.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.cts","names":[],"sources":["../src/server.ts"],"mappings":";UAYiB;WACN;;KAGC;WACG;;WACA;WAAyB;;UAEvB;WACN,SAAS;WACT;WACA;;UAGM;WACN;WACA;WACA;WACA;;UAGM;WACN;WACA,YACP,SAAS,YACN,YAAY,8BAA8B;WACtC;WACA;WACA;WACA;WACA,oBACP,SAAS,8BACN;WACI,aACP,SAAS,8BACN,YAAY,yBAAyB;WACjC,iBACP,UAAU,8BACP;WACI,eAAe,WAAW;WAC1B;;iBAGK,yBACd,SAAS,mCACP,SAAS,YAAY,QAAQ"}
@@ -0,0 +1,38 @@
1
+ //#region src/server.d.ts
2
+ interface OpenAIAuthorization {
3
+ readonly subject: string;
4
+ }
5
+ type OpenAIRateLimitResult = {
6
+ readonly allowed: true;
7
+ } | {
8
+ readonly allowed: false;
9
+ readonly retryAfterSeconds?: number;
10
+ };
11
+ interface OpenAITokenHandlerContext {
12
+ readonly request: Request;
13
+ readonly subject: string;
14
+ readonly model: string;
15
+ }
16
+ interface OpenAITokenIssuedMetadata {
17
+ readonly provider: "openai";
18
+ readonly subject: string;
19
+ readonly model: string;
20
+ readonly expiresAt: number;
21
+ }
22
+ interface CreateOpenAITokenHandlerOptions {
23
+ readonly apiKey: string;
24
+ readonly authorize: (request: Request) => PromiseLike<OpenAIAuthorization | null> | OpenAIAuthorization | null;
25
+ readonly model?: string;
26
+ readonly allowedModels?: readonly string[];
27
+ readonly organization?: string;
28
+ readonly project?: string;
29
+ readonly safetyIdentifier?: (context: OpenAITokenHandlerContext) => PromiseLike<string | undefined> | string | undefined;
30
+ readonly rateLimit?: (context: OpenAITokenHandlerContext) => PromiseLike<OpenAIRateLimitResult> | OpenAIRateLimitResult;
31
+ readonly onTokenIssued?: (metadata: OpenAITokenIssuedMetadata) => PromiseLike<void> | void;
32
+ readonly fetch?: typeof globalThis.fetch;
33
+ readonly clientSecretUrl?: string;
34
+ }
35
+ declare function createOpenAITokenHandler(options: CreateOpenAITokenHandlerOptions): (request: Request) => Promise<Response>;
36
+ //#endregion
37
+ export { CreateOpenAITokenHandlerOptions, OpenAIAuthorization, OpenAIRateLimitResult, OpenAITokenHandlerContext, OpenAITokenIssuedMetadata, createOpenAITokenHandler };
38
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";UAYiB;WACN;;KAGC;WACG;;WACA;WAAyB;;UAEvB;WACN,SAAS;WACT;WACA;;UAGM;WACN;WACA;WACA;WACA;;UAGM;WACN;WACA,YACP,SAAS,YACN,YAAY,8BAA8B;WACtC;WACA;WACA;WACA;WACA,oBACP,SAAS,8BACN;WACI,aACP,SAAS,8BACN,YAAY,yBAAyB;WACjC,iBACP,UAAU,8BACP;WACI,eAAe,WAAW;WAC1B;;iBAGK,yBACd,SAAS,mCACP,SAAS,YAAY,QAAQ"}
package/dist/server.js ADDED
@@ -0,0 +1,164 @@
1
+ import { i as validateOpenAITokenRequest, r as createOpenAITranscriptionSession } from "./session-config-CuDb4m9Q.js";
2
+ import { VoiceInputError } from "@voiceinput/provider";
3
+ //#region src/server.ts
4
+ const DEFAULT_CLIENT_SECRET_URL = "https://api.openai.com/v1/realtime/client_secrets";
5
+ const MAX_TOKEN_REQUEST_BYTES = 16384;
6
+ function createOpenAITokenHandler(options) {
7
+ const defaultModel = validateNonEmpty(options.model ?? "gpt-transcribe", "model");
8
+ const allowedModels = new Set(options.allowedModels ?? [defaultModel]);
9
+ if (!allowedModels.has(defaultModel)) throw new TypeError("allowedModels must contain the default model.");
10
+ if (typeof options.authorize !== "function") throw new TypeError("authorize must be a function.");
11
+ const apiKey = validateNonEmpty(options.apiKey, "apiKey");
12
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
13
+ const clientSecretUrl = options.clientSecretUrl ?? DEFAULT_CLIENT_SECRET_URL;
14
+ return async (request) => {
15
+ if (request.method !== "POST") return jsonError(405, "invalid-request", "Method not allowed.", { Allow: "POST" });
16
+ try {
17
+ const requestBody = await readRequestBody(request);
18
+ const authorization = await options.authorize(copyRequest(request, requestBody));
19
+ if (authorization === null) return jsonError(401, "unauthorized", "Unauthorized.");
20
+ const subject = validateNonEmpty(authorization.subject, "subject");
21
+ const tokenRequest = readTokenRequest(requestBody, defaultModel);
22
+ if (!allowedModels.has(tokenRequest.model)) return jsonError(400, "invalid-configuration", "The requested OpenAI model is not allowed.");
23
+ const createContext = () => ({
24
+ request: copyRequest(request, requestBody),
25
+ subject,
26
+ model: tokenRequest.model
27
+ });
28
+ const rateLimit = await options.rateLimit?.(createContext());
29
+ if (rateLimit?.allowed === false) {
30
+ const retryAfter = normalizeRetryAfter(rateLimit.retryAfterSeconds);
31
+ return jsonError(429, "rate-limited", "Rate limit exceeded.", retryAfter === void 0 ? {} : { "Retry-After": String(retryAfter) });
32
+ }
33
+ const safetyIdentifier = await options.safetyIdentifier?.(createContext());
34
+ const response = await fetchImplementation(clientSecretUrl, {
35
+ method: "POST",
36
+ headers: {
37
+ Authorization: `Bearer ${apiKey}`,
38
+ "Content-Type": "application/json",
39
+ ...options.organization === void 0 ? {} : { "OpenAI-Organization": options.organization },
40
+ ...options.project === void 0 ? {} : { "OpenAI-Project": options.project },
41
+ ...safetyIdentifier === void 0 ? {} : { "OpenAI-Safety-Identifier": validateNonEmpty(safetyIdentifier, "safetyIdentifier") }
42
+ },
43
+ body: JSON.stringify({ session: createOpenAITranscriptionSession(tokenRequest) }),
44
+ signal: request.signal
45
+ });
46
+ if (!response.ok) {
47
+ if (response.status === 429) return jsonError(429, "rate-limited", "OpenAI rate limit exceeded.", copyRetryAfter(response.headers));
48
+ return jsonError(502, "token-error", "OpenAI did not issue a client credential.");
49
+ }
50
+ const credential = validateCredential(await response.json());
51
+ await options.onTokenIssued?.({
52
+ provider: "openai",
53
+ subject,
54
+ model: tokenRequest.model,
55
+ expiresAt: credential.expires_at
56
+ });
57
+ return Response.json(credential, { headers: { "Cache-Control": "no-store" } });
58
+ } catch (error) {
59
+ if (request.signal.aborted) return jsonError(499, "network-error", "The request was cancelled.");
60
+ if (error instanceof InvalidTokenRequestError) return jsonError(error.status, error.code, error.message);
61
+ return jsonError(500, "token-error", "Unable to issue an OpenAI client credential.");
62
+ }
63
+ };
64
+ }
65
+ async function readRequestBody(request) {
66
+ if (request.headers.get("Content-Type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") throw new InvalidTokenRequestError("Content-Type must be application/json.", "invalid-request", 415);
67
+ if (Number(request.headers.get("Content-Length")) > MAX_TOKEN_REQUEST_BYTES) throw requestTooLarge();
68
+ const reader = request.body?.getReader();
69
+ if (reader === void 0) return "";
70
+ const decoder = new TextDecoder("utf-8", { fatal: true });
71
+ let bytesRead = 0;
72
+ let text = "";
73
+ while (true) {
74
+ const { done, value } = await reader.read();
75
+ if (done) break;
76
+ bytesRead += value.byteLength;
77
+ if (bytesRead > MAX_TOKEN_REQUEST_BYTES) {
78
+ await reader.cancel().catch(() => {});
79
+ throw requestTooLarge();
80
+ }
81
+ try {
82
+ text += decoder.decode(value, { stream: true });
83
+ } catch {
84
+ await reader.cancel().catch(() => {});
85
+ throw invalidUtf8();
86
+ }
87
+ }
88
+ try {
89
+ text += decoder.decode();
90
+ } catch {
91
+ throw invalidUtf8();
92
+ }
93
+ return text;
94
+ }
95
+ function readTokenRequest(body, defaultModel) {
96
+ try {
97
+ return validateOpenAITokenRequest(body.length === 0 ? {} : JSON.parse(body), defaultModel);
98
+ } catch (cause) {
99
+ if (cause instanceof SyntaxError || cause instanceof TypeError) throw new InvalidTokenRequestError(cause.message);
100
+ if (VoiceInputError.isInstance(cause) && (cause.code === "invalid-configuration" || cause.code === "unsupported-feature")) throw new InvalidTokenRequestError(cause.message, cause.code);
101
+ throw cause;
102
+ }
103
+ }
104
+ function copyRequest(request, body) {
105
+ return new Request(request, {
106
+ method: "POST",
107
+ body,
108
+ referrer: request.referrer,
109
+ referrerPolicy: request.referrerPolicy
110
+ });
111
+ }
112
+ var InvalidTokenRequestError = class extends Error {
113
+ code;
114
+ status;
115
+ constructor(message, code = "invalid-configuration", status = 400) {
116
+ super(message);
117
+ this.code = code;
118
+ this.status = status;
119
+ this.name = "InvalidTokenRequestError";
120
+ }
121
+ };
122
+ function requestTooLarge() {
123
+ return new InvalidTokenRequestError(`Token request body exceeds ${MAX_TOKEN_REQUEST_BYTES} bytes.`, "invalid-request", 413);
124
+ }
125
+ function invalidUtf8() {
126
+ return new InvalidTokenRequestError("Token request body must be valid UTF-8.", "invalid-request");
127
+ }
128
+ function validateCredential(value) {
129
+ if (!isRecord(value) || typeof value["value"] !== "string" || value["value"].length === 0 || typeof value["expires_at"] !== "number" || !Number.isFinite(value["expires_at"])) throw new Error("OpenAI returned an invalid client credential.");
130
+ return {
131
+ value: value["value"],
132
+ expires_at: value["expires_at"]
133
+ };
134
+ }
135
+ function jsonError(status, code, message, headers = {}) {
136
+ const responseHeaders = new Headers(headers);
137
+ responseHeaders.set("Cache-Control", "no-store");
138
+ responseHeaders.set("X-VoiceInput-Error", "1");
139
+ return Response.json({ error: {
140
+ code,
141
+ message
142
+ } }, {
143
+ status,
144
+ headers: responseHeaders
145
+ });
146
+ }
147
+ function copyRetryAfter(headers) {
148
+ const retryAfter = headers.get("Retry-After");
149
+ return retryAfter === null ? {} : { "Retry-After": retryAfter };
150
+ }
151
+ function normalizeRetryAfter(value) {
152
+ return value === void 0 || !Number.isFinite(value) || value <= 0 ? void 0 : Math.ceil(value);
153
+ }
154
+ function validateNonEmpty(value, name) {
155
+ if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${name} must be a non-empty string.`);
156
+ return value;
157
+ }
158
+ function isRecord(value) {
159
+ return typeof value === "object" && value !== null && !Array.isArray(value);
160
+ }
161
+ //#endregion
162
+ export { createOpenAITokenHandler };
163
+
164
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/server.ts"],"sourcesContent":["import { VoiceInputError } from \"@voiceinput/provider\";\n\nimport {\n OPENAI_DEFAULT_MODEL,\n createOpenAITranscriptionSession,\n validateOpenAITokenRequest,\n} from \"./session-config.js\";\n\nconst DEFAULT_CLIENT_SECRET_URL =\n \"https://api.openai.com/v1/realtime/client_secrets\";\nconst MAX_TOKEN_REQUEST_BYTES = 16 * 1024;\n\nexport interface OpenAIAuthorization {\n readonly subject: string;\n}\n\nexport type OpenAIRateLimitResult =\n | { readonly allowed: true }\n | { readonly allowed: false; readonly retryAfterSeconds?: number };\n\nexport interface OpenAITokenHandlerContext {\n readonly request: Request;\n readonly subject: string;\n readonly model: string;\n}\n\nexport interface OpenAITokenIssuedMetadata {\n readonly provider: \"openai\";\n readonly subject: string;\n readonly model: string;\n readonly expiresAt: number;\n}\n\nexport interface CreateOpenAITokenHandlerOptions {\n readonly apiKey: string;\n readonly authorize: (\n request: Request,\n ) => PromiseLike<OpenAIAuthorization | null> | OpenAIAuthorization | null;\n readonly model?: string;\n readonly allowedModels?: readonly string[];\n readonly organization?: string;\n readonly project?: string;\n readonly safetyIdentifier?: (\n context: OpenAITokenHandlerContext,\n ) => PromiseLike<string | undefined> | string | undefined;\n readonly rateLimit?: (\n context: OpenAITokenHandlerContext,\n ) => PromiseLike<OpenAIRateLimitResult> | OpenAIRateLimitResult;\n readonly onTokenIssued?: (\n metadata: OpenAITokenIssuedMetadata,\n ) => PromiseLike<void> | void;\n readonly fetch?: typeof globalThis.fetch;\n readonly clientSecretUrl?: string;\n}\n\nexport function createOpenAITokenHandler(\n options: CreateOpenAITokenHandlerOptions,\n): (request: Request) => Promise<Response> {\n const defaultModel = validateNonEmpty(\n options.model ?? OPENAI_DEFAULT_MODEL,\n \"model\",\n );\n const allowedModels = new Set(options.allowedModels ?? [defaultModel]);\n if (!allowedModels.has(defaultModel)) {\n throw new TypeError(\"allowedModels must contain the default model.\");\n }\n if (typeof options.authorize !== \"function\") {\n throw new TypeError(\"authorize must be a function.\");\n }\n const apiKey = validateNonEmpty(options.apiKey, \"apiKey\");\n const fetchImplementation = options.fetch ?? globalThis.fetch;\n const clientSecretUrl = options.clientSecretUrl ?? DEFAULT_CLIENT_SECRET_URL;\n\n return async (request) => {\n if (request.method !== \"POST\") {\n return jsonError(405, \"invalid-request\", \"Method not allowed.\", {\n Allow: \"POST\",\n });\n }\n\n try {\n const requestBody = await readRequestBody(request);\n const authorization = await options.authorize(\n copyRequest(request, requestBody),\n );\n if (authorization === null) {\n return jsonError(401, \"unauthorized\", \"Unauthorized.\");\n }\n const subject = validateNonEmpty(authorization.subject, \"subject\");\n const tokenRequest = readTokenRequest(requestBody, defaultModel);\n if (!allowedModels.has(tokenRequest.model)) {\n return jsonError(\n 400,\n \"invalid-configuration\",\n \"The requested OpenAI model is not allowed.\",\n );\n }\n const createContext = (): OpenAITokenHandlerContext => ({\n request: copyRequest(request, requestBody),\n subject,\n model: tokenRequest.model,\n });\n const rateLimit = await options.rateLimit?.(createContext());\n if (rateLimit?.allowed === false) {\n const retryAfter = normalizeRetryAfter(rateLimit.retryAfterSeconds);\n return jsonError(\n 429,\n \"rate-limited\",\n \"Rate limit exceeded.\",\n retryAfter === undefined ? {} : { \"Retry-After\": String(retryAfter) },\n );\n }\n\n const safetyIdentifier =\n await options.safetyIdentifier?.(createContext());\n const response = await fetchImplementation(clientSecretUrl, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${apiKey}`,\n \"Content-Type\": \"application/json\",\n ...(options.organization === undefined\n ? {}\n : { \"OpenAI-Organization\": options.organization }),\n ...(options.project === undefined\n ? {}\n : { \"OpenAI-Project\": options.project }),\n ...(safetyIdentifier === undefined\n ? {}\n : {\n \"OpenAI-Safety-Identifier\": validateNonEmpty(\n safetyIdentifier,\n \"safetyIdentifier\",\n ),\n }),\n },\n body: JSON.stringify({\n session: createOpenAITranscriptionSession(tokenRequest),\n }),\n signal: request.signal,\n });\n\n if (!response.ok) {\n if (response.status === 429) {\n return jsonError(\n 429,\n \"rate-limited\",\n \"OpenAI rate limit exceeded.\",\n copyRetryAfter(response.headers),\n );\n }\n return jsonError(\n 502,\n \"token-error\",\n \"OpenAI did not issue a client credential.\",\n );\n }\n\n const credential = validateCredential(await response.json());\n await options.onTokenIssued?.({\n provider: \"openai\",\n subject,\n model: tokenRequest.model,\n expiresAt: credential.expires_at,\n });\n\n return Response.json(credential, {\n headers: { \"Cache-Control\": \"no-store\" },\n });\n } catch (error) {\n if (request.signal.aborted) {\n return jsonError(499, \"network-error\", \"The request was cancelled.\");\n }\n if (error instanceof InvalidTokenRequestError) {\n return jsonError(error.status, error.code, error.message);\n }\n return jsonError(\n 500,\n \"token-error\",\n \"Unable to issue an OpenAI client credential.\",\n );\n }\n };\n}\n\nasync function readRequestBody(request: Request): Promise<string> {\n const contentType = request.headers\n .get(\"Content-Type\")\n ?.split(\";\", 1)[0]\n ?.trim()\n .toLowerCase();\n if (contentType !== \"application/json\") {\n throw new InvalidTokenRequestError(\n \"Content-Type must be application/json.\",\n \"invalid-request\",\n 415,\n );\n }\n const declaredLength = Number(request.headers.get(\"Content-Length\"));\n if (declaredLength > MAX_TOKEN_REQUEST_BYTES) {\n throw requestTooLarge();\n }\n const reader = request.body?.getReader();\n if (reader === undefined) return \"\";\n const decoder = new TextDecoder(\"utf-8\", { fatal: true });\n let bytesRead = 0;\n let text = \"\";\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n bytesRead += value.byteLength;\n if (bytesRead > MAX_TOKEN_REQUEST_BYTES) {\n await reader.cancel().catch(() => {});\n throw requestTooLarge();\n }\n try {\n text += decoder.decode(value, { stream: true });\n } catch {\n await reader.cancel().catch(() => {});\n throw invalidUtf8();\n }\n }\n try {\n text += decoder.decode();\n } catch {\n throw invalidUtf8();\n }\n return text;\n}\n\nfunction readTokenRequest(\n body: string,\n defaultModel: string,\n): ReturnType<typeof validateOpenAITokenRequest> {\n try {\n return validateOpenAITokenRequest(\n body.length === 0 ? {} : JSON.parse(body),\n defaultModel,\n );\n } catch (cause) {\n if (cause instanceof SyntaxError || cause instanceof TypeError) {\n throw new InvalidTokenRequestError(cause.message);\n }\n if (\n VoiceInputError.isInstance(cause) &&\n (cause.code === \"invalid-configuration\" ||\n cause.code === \"unsupported-feature\")\n ) {\n throw new InvalidTokenRequestError(cause.message, cause.code);\n }\n throw cause;\n }\n}\n\nfunction copyRequest(request: Request, body: string): Request {\n return new Request(request, {\n method: \"POST\",\n body,\n referrer: request.referrer,\n referrerPolicy: request.referrerPolicy,\n });\n}\n\nclass InvalidTokenRequestError extends Error {\n constructor(\n message: string,\n readonly code:\n | \"invalid-configuration\"\n | \"invalid-request\"\n | \"unsupported-feature\" = \"invalid-configuration\",\n readonly status = 400,\n ) {\n super(message);\n this.name = \"InvalidTokenRequestError\";\n }\n}\n\nfunction requestTooLarge(): InvalidTokenRequestError {\n return new InvalidTokenRequestError(\n `Token request body exceeds ${MAX_TOKEN_REQUEST_BYTES} bytes.`,\n \"invalid-request\",\n 413,\n );\n}\n\nfunction invalidUtf8(): InvalidTokenRequestError {\n return new InvalidTokenRequestError(\n \"Token request body must be valid UTF-8.\",\n \"invalid-request\",\n );\n}\n\nfunction validateCredential(value: unknown): {\n value: string;\n expires_at: number;\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 Error(\"OpenAI returned an invalid client credential.\");\n }\n return {\n value: value[\"value\"],\n expires_at: value[\"expires_at\"],\n };\n}\n\nfunction jsonError(\n status: number,\n code: string,\n message: string,\n headers: HeadersInit = {},\n): Response {\n const responseHeaders = new Headers(headers);\n responseHeaders.set(\"Cache-Control\", \"no-store\");\n responseHeaders.set(\"X-VoiceInput-Error\", \"1\");\n return Response.json(\n { error: { code, message } },\n {\n status,\n headers: responseHeaders,\n },\n );\n}\n\nfunction copyRetryAfter(headers: Headers): HeadersInit {\n const retryAfter = headers.get(\"Retry-After\");\n return retryAfter === null ? {} : { \"Retry-After\": retryAfter };\n}\n\nfunction normalizeRetryAfter(value: number | undefined): number | undefined {\n return value === undefined || !Number.isFinite(value) || value <= 0\n ? undefined\n : Math.ceil(value);\n}\n\nfunction validateNonEmpty(value: string, name: string): string {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new TypeError(`${name} must be a non-empty string.`);\n }\n return value;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n"],"mappings":"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"}