1
1
import { RealtimeEventHandler } from './event_handler.js' ;
2
2
import { RealtimeUtils } from './utils.js' ;
3
+ import { RealtimeTransportType } from './transport.js' ;
4
+ import { RealtimeTransportWebRTC } from './webrtc.js' ;
5
+ import { RealtimeTransportWebSocket } from './websocket.js' ;
3
6
4
7
export class RealtimeAPI extends RealtimeEventHandler {
5
8
/**
6
9
* Create a new RealtimeAPI instance
7
10
* @param {{url?: string, apiKey?: string, dangerouslyAllowAPIKeyInBrowser?: boolean, debug?: boolean} } [settings]
8
11
* @returns {RealtimeAPI }
9
12
*/
10
- constructor ( { url, apiKey, dangerouslyAllowAPIKeyInBrowser, debug } = { } ) {
13
+ constructor ( { transportType , url, apiKey, dangerouslyAllowAPIKeyInBrowser, debug } = { } ) {
11
14
super ( ) ;
12
- this . defaultUrl = 'wss://api.openai.com/v1/realtime' ;
13
- this . url = url || this . defaultUrl ;
14
- this . apiKey = apiKey || null ;
15
15
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 } "` ) ;
22
28
}
23
29
}
30
+ this . transport . on ( 'close' , ( ) => {
31
+ this . disconnect ( ) ;
32
+ } ) ;
33
+ this . transport . on ( 'message' , ( event ) => {
34
+ const message = JSON . parse ( event . data ) ;
35
+ this . _receive ( message . type , message )
36
+ } ) ;
37
+ }
38
+
39
+ get transportType ( ) {
40
+ return this . transport . transportType ;
24
41
}
25
42
26
43
/**
27
- * Tells us whether or not the WebSocket is connected
44
+ * Tells us whether or not the transport is connected
28
45
* @returns {boolean }
29
46
*/
30
- isConnected ( ) {
31
- return ! ! this . ws ;
47
+ get isConnected ( ) {
48
+ return this . transport . isConnected ;
32
49
}
33
50
34
51
/**
35
- * Writes WebSocket logs to console
52
+ * Writes log to console
36
53
* @param {...any } args
37
54
* @returns {true }
38
55
*/
39
56
log ( ...args ) {
40
57
const date = new Date ( ) . toISOString ( ) ;
41
- const logs = [ `[Websocket /${ date } ]` ] . concat ( args ) . map ( ( arg ) => {
58
+ const logs = [ `[RealtimeAPI /${ date } ]` ] . concat ( args ) . map ( ( arg ) => {
42
59
if ( typeof arg === 'object' && arg !== null ) {
43
60
return JSON . stringify ( arg , null , 2 ) ;
44
61
} else {
@@ -52,142 +69,51 @@ export class RealtimeAPI extends RealtimeEventHandler {
52
69
}
53
70
54
71
/**
55
- * Connects to Realtime API Websocket Server
72
+ * Connects to Realtime API Server
56
73
* @param {{model?: string} } [settings]
57
74
* @returns {Promise<true> }
58
75
*/
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
- }
76
+ async connect ( { sessionConfig, getMicrophoneCallback, setAudioOutputCallback } ) {
77
+ return this . transport . connect ( { sessionConfig, getMicrophoneCallback, setAudioOutputCallback } ) ;
155
78
}
156
79
157
80
/**
158
81
* Disconnects from Realtime API server
159
- * @param {WebSocket } [ws]
160
82
* @returns {true }
161
83
*/
162
- disconnect ( ws ) {
163
- if ( ! ws || this . ws === ws ) {
164
- this . ws && this . ws . close ( ) ;
165
- this . ws = null ;
166
- return true ;
167
- }
84
+ async disconnect ( ) {
85
+ await this . transport . disconnect ( ) ;
86
+ return true ;
168
87
}
169
88
170
89
/**
171
- * Receives an event from WebSocket and dispatches as "server.{eventName}" and "server.*" events
90
+ * Receives an event from transport and dispatches as "server.{eventName}" and "server.*" events
172
91
* @param {string } eventName
173
92
* @param {{[key: string]: any} } event
174
93
* @returns {true }
175
94
*/
176
- receive ( eventName , event ) {
177
- this . log ( `received:` , eventName , event ) ;
95
+ _receive ( eventName , event ) {
96
+ if ( this . debug ) {
97
+ if ( eventName === 'response.audio.delta' ) {
98
+ const delta = event . delta ;
99
+ this . log ( `received:` , eventName , { ...event , delta : delta . slice ( 0 , 10 ) + '...' + delta . slice ( - 10 ) } ) ;
100
+ } else {
101
+ this . log ( `received:` , eventName , event ) ;
102
+ }
103
+ }
178
104
this . dispatch ( `server.${ eventName } ` , event ) ;
179
105
this . dispatch ( 'server.*' , event ) ;
180
106
return true ;
181
107
}
182
108
183
109
/**
184
- * Sends an event to WebSocket and dispatches as "client.{eventName}" and "client.*" events
110
+ * Sends an event to transport and dispatches as "client.{eventName}" and "client.*" events
185
111
* @param {string } eventName
186
112
* @param {{[key: string]: any} } event
187
113
* @returns {true }
188
114
*/
189
- send ( eventName , data ) {
190
- if ( ! this . isConnected ( ) ) {
115
+ async send ( eventName , data ) {
116
+ if ( ! this . isConnected ) {
191
117
throw new Error ( `RealtimeAPI is not connected` ) ;
192
118
}
193
119
data = data || { } ;
@@ -201,8 +127,15 @@ export class RealtimeAPI extends RealtimeEventHandler {
201
127
} ;
202
128
this . dispatch ( `client.${ eventName } ` , event ) ;
203
129
this . dispatch ( 'client.*' , event ) ;
204
- this . log ( `sent:` , eventName , event ) ;
205
- this . ws . send ( JSON . stringify ( event ) ) ;
130
+ if ( this . debug ) {
131
+ if ( eventName === 'input_audio_buffer.append' ) {
132
+ const audio = event . audio ;
133
+ this . log ( `sending:` , eventName , { ...event , audio : audio . slice ( 0 , 10 ) + '...' + audio . slice ( - 10 ) } ) ;
134
+ } else {
135
+ this . log ( `sending:` , eventName , event ) ;
136
+ }
137
+ }
138
+ await this . transport . send ( event ) ;
206
139
return true ;
207
140
}
208
141
}
0 commit comments