11import { RealtimeEventHandler } from './event_handler.js' ;
22import { RealtimeUtils } from './utils.js' ;
3+ import { RealtimeTransportType } from './transport.js' ;
4+ import { RealtimeTransportWebRTC } from './transport_webrtc.js' ;
5+ import { RealtimeTransportWebSocket } from './transport_websocket.js' ;
36
47export class RealtimeAPI extends RealtimeEventHandler {
58 /**
69 * Create a new RealtimeAPI instance
7- * @param {{url?: string, apiKey?: string, dangerouslyAllowAPIKeyInBrowser?: boolean, debug?: boolean} } [settings]
10+ * @param {{transportType?: string, url?: string, apiKey?: string, dangerouslyAllowAPIKeyInBrowser?: boolean, debug?: boolean} } [settings]
811 * @returns {RealtimeAPI }
912 */
10- constructor ( { url, apiKey, dangerouslyAllowAPIKeyInBrowser, debug } = { } ) {
13+ constructor ( { transportType , url, apiKey, dangerouslyAllowAPIKeyInBrowser, debug } = { } ) {
1114 super ( ) ;
12- this . defaultUrl = 'wss://api.openai.com/v1/realtime' ;
13- this . url = url || this . defaultUrl ;
14- this . apiKey = apiKey || null ;
1515 this . debug = ! ! debug ;
16- this . ws = null ;
17- if ( globalThis . document && this . apiKey ) {
18- if ( ! dangerouslyAllowAPIKeyInBrowser ) {
19- throw new Error (
20- `Can not provide API key in the browser without "dangerouslyAllowAPIKeyInBrowser" set to true` ,
21- ) ;
16+ transportType = transportType ?. toUpperCase ( ) || RealtimeTransportType . WEBRTC ;
17+ switch ( transportType ) {
18+ case RealtimeTransportType . WEBRTC : {
19+ this . transport = new RealtimeTransportWebRTC ( { url, apiKey, dangerouslyAllowAPIKeyInBrowser, debug } ) ;
20+ break ;
21+ }
22+ case RealtimeTransportType . WEBSOCKET : {
23+ this . transport = new RealtimeTransportWebSocket ( { url, apiKey, dangerouslyAllowAPIKeyInBrowser, debug } ) ;
24+ break ;
25+ }
26+ default : {
27+ throw new Error ( `Invalid transportType: "${ transportType } "` ) ;
2228 }
2329 }
30+ this . transport . on ( 'close' , ( data ) => {
31+ this . disconnect ( ) ;
32+ this . dispatch ( 'close' , data ) ;
33+ } ) ;
34+ this . transport . on ( 'message' , ( event ) => {
35+ const message = JSON . parse ( event . data ) ;
36+ this . _receive ( message . type , message )
37+ } ) ;
38+ }
39+
40+ get transportType ( ) {
41+ return this . transport . transportType ;
2442 }
2543
2644 /**
27- * Tells us whether or not the WebSocket is connected
45+ * Tells us whether or not the Realtime API server is connected
2846 * @returns {boolean }
2947 */
30- isConnected ( ) {
31- return ! ! this . ws ;
48+ get isConnected ( ) {
49+ return this . transport . isConnected ;
3250 }
3351
3452 /**
35- * Writes WebSocket logs to console
53+ * Writes log to console
3654 * @param {...any } args
3755 * @returns {true }
3856 */
3957 log ( ...args ) {
4058 const date = new Date ( ) . toISOString ( ) ;
41- const logs = [ `[Websocket /${ date } ]` ] . concat ( args ) . map ( ( arg ) => {
59+ const logs = [ `[RealtimeAPI /${ date } ]` ] . concat ( args ) . map ( ( arg ) => {
4260 if ( typeof arg === 'object' && arg !== null ) {
4361 return JSON . stringify ( arg , null , 2 ) ;
4462 } else {
@@ -52,142 +70,51 @@ export class RealtimeAPI extends RealtimeEventHandler {
5270 }
5371
5472 /**
55- * Connects to Realtime API Websocket Server
56- * @param {{model ?: string } } [settings]
73+ * Connects to Realtime API Server
74+ * @param {{sessionConfig ?: SessionConfig, setAudioOutputCallback?: Function, getMicrophoneCallback?: Function } } [settings]
5775 * @returns {Promise<true> }
5876 */
59- async connect ( { model } = { model : 'gpt-4o-realtime-preview-2024-10-01' } ) {
60- if ( ! this . apiKey && this . url === this . defaultUrl ) {
61- console . warn ( `No apiKey provided for connection to "${ this . url } "` ) ;
62- }
63- if ( this . isConnected ( ) ) {
64- throw new Error ( `Already connected` ) ;
65- }
66- if ( globalThis . WebSocket ) {
67- /**
68- * Web browser
69- */
70- if ( globalThis . document && this . apiKey ) {
71- console . warn (
72- 'Warning: Connecting using API key in the browser, this is not recommended' ,
73- ) ;
74- }
75- const WebSocket = globalThis . WebSocket ;
76- const ws = new WebSocket ( `${ this . url } ${ model ? `?model=${ model } ` : '' } ` , [
77- 'realtime' ,
78- `openai-insecure-api-key.${ this . apiKey } ` ,
79- 'openai-beta.realtime-v1' ,
80- ] ) ;
81- ws . addEventListener ( 'message' , ( event ) => {
82- const message = JSON . parse ( event . data ) ;
83- this . receive ( message . type , message ) ;
84- } ) ;
85- return new Promise ( ( resolve , reject ) => {
86- const connectionErrorHandler = ( ) => {
87- this . disconnect ( ws ) ;
88- reject ( new Error ( `Could not connect to "${ this . url } "` ) ) ;
89- } ;
90- ws . addEventListener ( 'error' , connectionErrorHandler ) ;
91- ws . addEventListener ( 'open' , ( ) => {
92- this . log ( `Connected to "${ this . url } "` ) ;
93- ws . removeEventListener ( 'error' , connectionErrorHandler ) ;
94- ws . addEventListener ( 'error' , ( ) => {
95- this . disconnect ( ws ) ;
96- this . log ( `Error, disconnected from "${ this . url } "` ) ;
97- this . dispatch ( 'close' , { error : true } ) ;
98- } ) ;
99- ws . addEventListener ( 'close' , ( ) => {
100- this . disconnect ( ws ) ;
101- this . log ( `Disconnected from "${ this . url } "` ) ;
102- this . dispatch ( 'close' , { error : false } ) ;
103- } ) ;
104- this . ws = ws ;
105- resolve ( true ) ;
106- } ) ;
107- } ) ;
108- } else {
109- /**
110- * Node.js
111- */
112- const moduleName = 'ws' ;
113- const wsModule = await import ( /* webpackIgnore: true */ moduleName ) ;
114- const WebSocket = wsModule . default ;
115- const ws = new WebSocket (
116- 'wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview-2024-10-01' ,
117- [ ] ,
118- {
119- finishRequest : ( request ) => {
120- // Auth
121- request . setHeader ( 'Authorization' , `Bearer ${ this . apiKey } ` ) ;
122- request . setHeader ( 'OpenAI-Beta' , 'realtime=v1' ) ;
123- request . end ( ) ;
124- } ,
125- } ,
126- ) ;
127- ws . on ( 'message' , ( data ) => {
128- const message = JSON . parse ( data . toString ( ) ) ;
129- this . receive ( message . type , message ) ;
130- } ) ;
131- return new Promise ( ( resolve , reject ) => {
132- const connectionErrorHandler = ( ) => {
133- this . disconnect ( ws ) ;
134- reject ( new Error ( `Could not connect to "${ this . url } "` ) ) ;
135- } ;
136- ws . on ( 'error' , connectionErrorHandler ) ;
137- ws . on ( 'open' , ( ) => {
138- this . log ( `Connected to "${ this . url } "` ) ;
139- ws . removeListener ( 'error' , connectionErrorHandler ) ;
140- ws . on ( 'error' , ( ) => {
141- this . disconnect ( ws ) ;
142- this . log ( `Error, disconnected from "${ this . url } "` ) ;
143- this . dispatch ( 'close' , { error : true } ) ;
144- } ) ;
145- ws . on ( 'close' , ( ) => {
146- this . disconnect ( ws ) ;
147- this . log ( `Disconnected from "${ this . url } "` ) ;
148- this . dispatch ( 'close' , { error : false } ) ;
149- } ) ;
150- this . ws = ws ;
151- resolve ( true ) ;
152- } ) ;
153- } ) ;
154- }
77+ async connect ( { sessionConfig, setAudioOutputCallback, getMicrophoneCallback } ) {
78+ return this . transport . connect ( { sessionConfig, setAudioOutputCallback, getMicrophoneCallback } ) ;
15579 }
15680
15781 /**
15882 * Disconnects from Realtime API server
159- * @param {WebSocket } [ws]
16083 * @returns {true }
16184 */
162- disconnect ( ws ) {
163- if ( ! ws || this . ws === ws ) {
164- this . ws && this . ws . close ( ) ;
165- this . ws = null ;
166- return true ;
167- }
85+ async disconnect ( ) {
86+ await this . transport . disconnect ( ) ;
87+ return true ;
16888 }
16989
17090 /**
171- * Receives an event from WebSocket and dispatches as "server.{eventName}" and "server.*" events
91+ * Receives an event from Realtime API server and dispatches as "server.{eventName}" and "server.*" events
17292 * @param {string } eventName
17393 * @param {{[key: string]: any} } event
17494 * @returns {true }
17595 */
176- receive ( eventName , event ) {
177- this . log ( `received:` , eventName , event ) ;
96+ _receive ( eventName , event ) {
97+ if ( this . debug ) {
98+ if ( eventName === 'response.audio.delta' ) {
99+ const delta = event . delta ;
100+ this . log ( `received:` , eventName , { ...event , delta : delta . slice ( 0 , 10 ) + '...' + delta . slice ( - 10 ) } ) ;
101+ } else {
102+ this . log ( `received:` , eventName , event ) ;
103+ }
104+ }
178105 this . dispatch ( `server.${ eventName } ` , event ) ;
179106 this . dispatch ( 'server.*' , event ) ;
180107 return true ;
181108 }
182109
183110 /**
184- * Sends an event to WebSocket and dispatches as "client.{eventName}" and "client.*" events
111+ * Sends an event to Realtime API server and dispatches as "client.{eventName}" and "client.*" events
185112 * @param {string } eventName
186113 * @param {{[key: string]: any} } event
187114 * @returns {true }
188115 */
189- send ( eventName , data ) {
190- if ( ! this . isConnected ( ) ) {
116+ async send ( eventName , data ) {
117+ if ( ! this . isConnected ) {
191118 throw new Error ( `RealtimeAPI is not connected` ) ;
192119 }
193120 data = data || { } ;
@@ -201,8 +128,15 @@ export class RealtimeAPI extends RealtimeEventHandler {
201128 } ;
202129 this . dispatch ( `client.${ eventName } ` , event ) ;
203130 this . dispatch ( 'client.*' , event ) ;
204- this . log ( `sent:` , eventName , event ) ;
205- this . ws . send ( JSON . stringify ( event ) ) ;
131+ if ( this . debug ) {
132+ if ( eventName === 'input_audio_buffer.append' ) {
133+ const audio = event . audio ;
134+ this . log ( `sending:` , eventName , { ...event , audio : audio . slice ( 0 , 10 ) + '...' + audio . slice ( - 10 ) } ) ;
135+ } else {
136+ this . log ( `sending:` , eventName , event ) ;
137+ }
138+ }
139+ await this . transport . send ( event ) ;
206140 return true ;
207141 }
208142}
0 commit comments