@ai-sdk/openai
Advanced tools
+4
-4
| { | ||
| "name": "@ai-sdk/openai", | ||
| "version": "4.0.11", | ||
| "version": "4.0.13", | ||
| "type": "module", | ||
@@ -39,3 +39,3 @@ "license": "Apache-2.0", | ||
| "@ai-sdk/provider": "4.0.3", | ||
| "@ai-sdk/provider-utils": "5.0.7" | ||
| "@ai-sdk/provider-utils": "5.0.9" | ||
| }, | ||
@@ -47,4 +47,4 @@ "devDependencies": { | ||
| "zod": "3.25.76", | ||
| "@ai-sdk/test-server": "2.0.0", | ||
| "@vercel/ai-tsconfig": "0.0.0" | ||
| "@vercel/ai-tsconfig": "0.0.0", | ||
| "@ai-sdk/test-server": "2.0.0" | ||
| }, | ||
@@ -51,0 +51,0 @@ "peerDependencies": { |
@@ -16,2 +16,3 @@ import type { | ||
| loadOptionalSetting, | ||
| validateBaseURL, | ||
| withoutTrailingSlash, | ||
@@ -179,6 +180,8 @@ withUserAgentSuffix, | ||
| withoutTrailingSlash( | ||
| loadOptionalSetting({ | ||
| settingValue: options.baseURL, | ||
| environmentVariableName: 'OPENAI_BASE_URL', | ||
| }), | ||
| validateBaseURL( | ||
| loadOptionalSetting({ | ||
| settingValue: options.baseURL, | ||
| environmentVariableName: 'OPENAI_BASE_URL', | ||
| }), | ||
| ), | ||
| ) ?? 'https://api.openai.com/v1'; | ||
@@ -185,0 +188,0 @@ |
@@ -13,7 +13,6 @@ import { | ||
| createJsonResponseHandler, | ||
| getWebSocketConstructor, | ||
| connectToWebSocket, | ||
| mediaTypeToExtension, | ||
| parseProviderOptions, | ||
| postFormDataToApi, | ||
| readWebSocketMessageText, | ||
| safeParseJSON, | ||
@@ -24,2 +23,5 @@ serializeModelOptions, | ||
| WORKFLOW_SERIALIZE, | ||
| waitForWebSocketBufferDrain, | ||
| type WebSocketConnection, | ||
| type WebSocketLike, | ||
| } from '@ai-sdk/provider-utils'; | ||
@@ -395,18 +397,17 @@ import type { OpenAIConfig } from '../openai-config'; | ||
| start: controller => { | ||
| const WebSocketConstructor = getWebSocketConstructor(webSocket); | ||
| const ws = new WebSocketConstructor( | ||
| url, | ||
| getOpenAIRealtimeProtocols(headers), | ||
| { headers }, | ||
| ); | ||
| const realtimeConnection = getOpenAIRealtimeConnection(headers); | ||
| let audioReader: | ||
| | ReadableStreamDefaultReader<Uint8Array | string> | ||
| | undefined; | ||
| let connection: WebSocketConnection | undefined; | ||
| cleanup = (closeCode?: number) => { | ||
| abortSignal?.removeEventListener('abort', abort); | ||
| void audioReader?.cancel().catch(() => {}); | ||
| try { | ||
| ws.close(closeCode); | ||
| } catch {} | ||
| if (audioReader != null) { | ||
| void audioReader.cancel().catch(() => {}); | ||
| } else { | ||
| // pre-open failure or abort: cancel the caller's audio stream so an | ||
| // upstream producer piping into it does not hang: | ||
| void audio.cancel().catch(() => {}); | ||
| } | ||
| connection?.close(closeCode); | ||
| }; | ||
@@ -437,12 +438,3 @@ | ||
| const abort = () => { | ||
| finishWithError(abortSignal?.reason ?? new Error('Aborted')); | ||
| }; | ||
| if (abortSignal?.aborted) { | ||
| abort(); | ||
| return; | ||
| } | ||
| abortSignal?.addEventListener('abort', abort, { once: true }); | ||
| const sendAudio = async () => { | ||
| const sendAudio = async (socket: WebSocketLike) => { | ||
| audioReader = audio.getReader(); | ||
@@ -453,3 +445,3 @@ try { | ||
| if (done || finished) break; | ||
| ws.send( | ||
| socket.send( | ||
| JSON.stringify({ | ||
@@ -460,64 +452,70 @@ type: 'input_audio_buffer.append', | ||
| ); | ||
| // backpressure: pause reads while the socket buffer is full | ||
| await waitForWebSocketBufferDrain(socket); | ||
| } | ||
| } finally { | ||
| audioReader.releaseLock(); | ||
| // unlocked again: cleanup must cancel `audio`, not the reader | ||
| audioReader = undefined; | ||
| } | ||
| if (!finished) { | ||
| ws.send(JSON.stringify({ type: 'input_audio_buffer.commit' })); | ||
| socket.send(JSON.stringify({ type: 'input_audio_buffer.commit' })); | ||
| } | ||
| }; | ||
| ws.onopen = () => { | ||
| controller.enqueue({ type: 'stream-start', warnings }); | ||
| ws.send(JSON.stringify(sessionUpdate)); | ||
| void sendAudio().catch(finishWithError); | ||
| }; | ||
| connection = connectToWebSocket({ | ||
| url, | ||
| protocols: realtimeConnection.protocols, | ||
| headers: realtimeConnection.headers, | ||
| webSocket, | ||
| abortSignal, | ||
| onAbort: finishWithError, | ||
| onProcessingError: finishWithError, | ||
| onOpen: socket => { | ||
| controller.enqueue({ type: 'stream-start', warnings }); | ||
| socket.send(JSON.stringify(sessionUpdate)); | ||
| void sendAudio(socket).catch(finishWithError); | ||
| }, | ||
| onMessageText: async text => { | ||
| const parsed = await safeParseJSON({ text }); | ||
| if (!parsed.success) return; | ||
| const raw = parsed.value as OpenAIRealtimeTranscriptionEvent; | ||
| ws.onmessage = event => { | ||
| void readWebSocketMessageText(event.data) | ||
| .then(async text => { | ||
| const parsed = await safeParseJSON({ text }); | ||
| if (!parsed.success) return; | ||
| const raw = parsed.value as OpenAIRealtimeTranscriptionEvent; | ||
| if (includeRawChunks) { | ||
| controller.enqueue({ type: 'raw', rawValue: raw }); | ||
| } | ||
| if (includeRawChunks) { | ||
| controller.enqueue({ type: 'raw', rawValue: raw }); | ||
| switch (raw.type) { | ||
| case 'conversation.item.input_audio_transcription.delta': { | ||
| controller.enqueue({ | ||
| type: 'transcript-delta', | ||
| id: raw.item_id, | ||
| delta: raw.delta ?? '', | ||
| }); | ||
| break; | ||
| } | ||
| switch (raw.type) { | ||
| case 'conversation.item.input_audio_transcription.delta': { | ||
| controller.enqueue({ | ||
| type: 'transcript-delta', | ||
| id: raw.item_id, | ||
| delta: raw.delta ?? '', | ||
| }); | ||
| break; | ||
| } | ||
| case 'conversation.item.input_audio_transcription.completed': { | ||
| finish(raw.transcript ?? '', raw.item_id); | ||
| break; | ||
| } | ||
| case 'conversation.item.input_audio_transcription.completed': { | ||
| finish(raw.transcript ?? '', raw.item_id); | ||
| break; | ||
| } | ||
| case 'error': { | ||
| finishWithError( | ||
| new Error(raw.error?.message ?? 'OpenAI realtime error'), | ||
| ); | ||
| break; | ||
| } | ||
| case 'error': { | ||
| finishWithError( | ||
| new Error(raw.error?.message ?? 'OpenAI realtime error'), | ||
| ); | ||
| break; | ||
| } | ||
| }) | ||
| .catch(finishWithError); | ||
| }; | ||
| ws.onerror = () => { | ||
| finishWithError(new Error('OpenAI realtime transcription error')); | ||
| }; | ||
| ws.onclose = () => { | ||
| if (finished) return; | ||
| finished = true; | ||
| cleanup(); | ||
| controller.close(); | ||
| }; | ||
| } | ||
| }, | ||
| onSocketError: () => { | ||
| finishWithError(new Error('OpenAI realtime transcription error')); | ||
| }, | ||
| onClose: () => { | ||
| if (finished) return; | ||
| finished = true; | ||
| cleanup(); | ||
| controller.close(); | ||
| }, | ||
| }); | ||
| }, | ||
@@ -573,13 +571,34 @@ | ||
| function getOpenAIRealtimeProtocols( | ||
| // The bearer token rides the `openai-insecure-api-key` subprotocol (native | ||
| // `WebSocket` cannot send headers) and the Authorization header is stripped: | ||
| // OpenAI rejects handshakes that send both auth channels. | ||
| function getOpenAIRealtimeConnection( | ||
| headers: Record<string, string | undefined>, | ||
| ): string[] { | ||
| const authorization = headers.Authorization ?? headers.authorization; | ||
| const token = authorization?.startsWith('Bearer ') | ||
| ? authorization.slice('Bearer '.length) | ||
| : undefined; | ||
| ): { | ||
| protocols: string[]; | ||
| headers: Record<string, string | undefined>; | ||
| } { | ||
| // last case-variant wins: combineHeaders keeps case-distinct keys and | ||
| // spreads per-call headers after configuration headers | ||
| let authorization: string | undefined; | ||
| for (const [key, value] of Object.entries(headers)) { | ||
| if (key.toLowerCase() === 'authorization' && value != null) { | ||
| authorization = value; | ||
| } | ||
| } | ||
| // the HTTP auth scheme is case-insensitive | ||
| const token = authorization?.match(/^bearer\s+(.+)$/i)?.[1]; | ||
| return token == null | ||
| ? ['realtime'] | ||
| : ['realtime', `openai-insecure-api-key.${token}`]; | ||
| if (token == null) { | ||
| return { protocols: ['realtime'], headers }; | ||
| } | ||
| return { | ||
| protocols: ['realtime', `openai-insecure-api-key.${token}`], | ||
| headers: Object.fromEntries( | ||
| Object.entries(headers).filter( | ||
| ([key]) => key.toLowerCase() !== 'authorization', | ||
| ), | ||
| ), | ||
| }; | ||
| } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
2491874
0.23%31324
0.17%1
-50%+ Added
- Removed
Updated