lib/api.js (158 lines of code) (raw):

import { RealtimeEventHandler } from './event_handler.js'; import { RealtimeUtils } from './utils.js'; export class RealtimeAPI extends RealtimeEventHandler { /** * Create a new RealtimeAPI instance * @param {{url?: string, apiKey?: string, dangerouslyAllowAPIKeyInBrowser?: boolean, debug?: boolean}} [settings] * @returns {RealtimeAPI} */ constructor({ url, apiKey, dangerouslyAllowAPIKeyInBrowser, debug } = {}) { super(); this.defaultUrl = 'wss://api.openai.com/v1/realtime'; this.url = url || this.defaultUrl; this.apiKey = apiKey || null; this.debug = !!debug; this.ws = null; if (globalThis.document && this.apiKey) { if (!dangerouslyAllowAPIKeyInBrowser) { throw new Error( `Can not provide API key in the browser without "dangerouslyAllowAPIKeyInBrowser" set to true`, ); } } } /** * Tells us whether or not the WebSocket is connected * @returns {boolean} */ isConnected() { return !!this.ws; } /** * Writes WebSocket logs to console * @param {...any} args * @returns {true} */ log(...args) { const date = new Date().toISOString(); const logs = [`[Websocket/${date}]`].concat(args).map((arg) => { if (typeof arg === 'object' && arg !== null) { return JSON.stringify(arg, null, 2); } else { return arg; } }); if (this.debug) { console.log(...logs); } return true; } /** * Connects to Realtime API Websocket Server * @param {{model?: string}} [settings] * @returns {Promise<true>} */ async connect({ model } = { model: 'gpt-4o-realtime-preview-2024-10-01' }) { if (!this.apiKey && this.url === this.defaultUrl) { console.warn(`No apiKey provided for connection to "${this.url}"`); } if (this.isConnected()) { throw new Error(`Already connected`); } if (globalThis.WebSocket) { /** * Web browser */ if (globalThis.document && this.apiKey) { console.warn( 'Warning: Connecting using API key in the browser, this is not recommended', ); } const WebSocket = globalThis.WebSocket; const ws = new WebSocket(`${this.url}${model ? `?model=${model}` : ''}`, [ 'realtime', `openai-insecure-api-key.${this.apiKey}`, 'openai-beta.realtime-v1', ]); ws.addEventListener('message', (event) => { const message = JSON.parse(event.data); this.receive(message.type, message); }); return new Promise((resolve, reject) => { const connectionErrorHandler = () => { this.disconnect(ws); reject(new Error(`Could not connect to "${this.url}"`)); }; ws.addEventListener('error', connectionErrorHandler); ws.addEventListener('open', () => { this.log(`Connected to "${this.url}"`); ws.removeEventListener('error', connectionErrorHandler); ws.addEventListener('error', () => { this.disconnect(ws); this.log(`Error, disconnected from "${this.url}"`); this.dispatch('close', { error: true }); }); ws.addEventListener('close', () => { this.disconnect(ws); this.log(`Disconnected from "${this.url}"`); this.dispatch('close', { error: false }); }); this.ws = ws; resolve(true); }); }); } else { /** * Node.js */ const moduleName = 'ws'; const wsModule = await import(/* webpackIgnore: true */ moduleName); const WebSocket = wsModule.default; const ws = new WebSocket( 'wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01', [], { finishRequest: (request) => { // Auth request.setHeader('Authorization', `Bearer ${this.apiKey}`); request.setHeader('OpenAI-Beta', 'realtime=v1'); request.end(); }, }, ); ws.on('message', (data) => { const message = JSON.parse(data.toString()); this.receive(message.type, message); }); return new Promise((resolve, reject) => { const connectionErrorHandler = () => { this.disconnect(ws); reject(new Error(`Could not connect to "${this.url}"`)); }; ws.on('error', connectionErrorHandler); ws.on('open', () => { this.log(`Connected to "${this.url}"`); ws.removeListener('error', connectionErrorHandler); ws.on('error', () => { this.disconnect(ws); this.log(`Error, disconnected from "${this.url}"`); this.dispatch('close', { error: true }); }); ws.on('close', () => { this.disconnect(ws); this.log(`Disconnected from "${this.url}"`); this.dispatch('close', { error: false }); }); this.ws = ws; resolve(true); }); }); } } /** * Disconnects from Realtime API server * @param {WebSocket} [ws] * @returns {true} */ disconnect(ws) { if (!ws || this.ws === ws) { this.ws && this.ws.close(); this.ws = null; return true; } } /** * Receives an event from WebSocket and dispatches as "server.{eventName}" and "server.*" events * @param {string} eventName * @param {{[key: string]: any}} event * @returns {true} */ receive(eventName, event) { this.log(`received:`, eventName, event); this.dispatch(`server.${eventName}`, event); this.dispatch('server.*', event); return true; } /** * Sends an event to WebSocket and dispatches as "client.{eventName}" and "client.*" events * @param {string} eventName * @param {{[key: string]: any}} event * @returns {true} */ send(eventName, data) { if (!this.isConnected()) { throw new Error(`RealtimeAPI is not connected`); } data = data || {}; if (typeof data !== 'object') { throw new Error(`data must be an object`); } const event = { event_id: RealtimeUtils.generateId('evt_'), type: eventName, ...data, }; this.dispatch(`client.${eventName}`, event); this.dispatch('client.*', event); this.log(`sent:`, eventName, event); this.ws.send(JSON.stringify(event)); return true; } }