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;
}
}