@@ -43,6 +43,16 @@ type OpenAIHttpOptions = {
4343 timeoutMs ?: number ;
4444} ;
4545
46+ type OpenAIRealtimeBrowserResponseReader = (
47+ response : Response ,
48+ label : string ,
49+ maxBytes : number ,
50+ ) => Promise < string > ;
51+
52+ type OpenAIWebRtcSmokeGlobal = typeof globalThis & {
53+ openclawReadBoundedRealtimeResponseText ?: OpenAIRealtimeBrowserResponseReader ;
54+ } ;
55+
4656function getEnv ( name : string ) : string | undefined {
4757 const value = process . env [ name ] ?. trim ( ) ;
4858 return value ? value : undefined ;
@@ -114,6 +124,63 @@ function compareStrings(left: string | undefined, right: string | undefined): nu
114124 return ( left ?? "" ) . localeCompare ( right ?? "" ) ;
115125}
116126
127+ async function readOpenAIRealtimeBrowserResponseText (
128+ response : Response ,
129+ label : string ,
130+ maxBytes : number ,
131+ ) : Promise < string > {
132+ const responseBodyTooLargeError = ( errorLabel : string , errorMaxBytes : number ) : Error =>
133+ new Error ( `${ errorLabel } response body exceeded ${ errorMaxBytes } bytes` ) ;
134+ const rawContentLength = response . headers . get ( "content-length" ) ;
135+ if ( rawContentLength && / ^ \d + $ / u. test ( rawContentLength ) ) {
136+ const contentLength = Number ( rawContentLength ) ;
137+ if ( ! Number . isSafeInteger ( contentLength ) || contentLength > maxBytes ) {
138+ await response . body ?. cancel ( ) . catch ( ( ) => undefined ) ;
139+ throw responseBodyTooLargeError ( label , maxBytes ) ;
140+ }
141+ }
142+ if ( ! response . body ) {
143+ return "" ;
144+ }
145+
146+ const reader = response . body . getReader ( ) ;
147+ const decoder = new TextDecoder ( ) ;
148+ const chunks : string [ ] = [ ] ;
149+ let totalBytes = 0 ;
150+ let canceled = false ;
151+
152+ try {
153+ for ( ; ; ) {
154+ const { done, value } = await reader . read ( ) ;
155+ if ( done ) {
156+ const tail = decoder . decode ( ) ;
157+ if ( tail ) {
158+ chunks . push ( tail ) ;
159+ }
160+ break ;
161+ }
162+
163+ totalBytes += value . byteLength ;
164+ if ( totalBytes > maxBytes ) {
165+ canceled = true ;
166+ await reader . cancel ( ) . catch ( ( ) => undefined ) ;
167+ throw responseBodyTooLargeError ( label , maxBytes ) ;
168+ }
169+ chunks . push ( decoder . decode ( value , { stream : true } ) ) ;
170+ }
171+ } finally {
172+ if ( ! canceled ) {
173+ reader . releaseLock ( ) ;
174+ }
175+ }
176+
177+ return chunks . join ( "" ) ;
178+ }
179+
180+ function openAIRealtimeBrowserResponseReaderInitScript ( ) : string {
181+ return `globalThis.openclawReadBoundedRealtimeResponseText = ${ readOpenAIRealtimeBrowserResponseText . toString ( ) } ;` ;
182+ }
183+
117184async function createOpenAIClientSecret (
118185 apiKey : string ,
119186 options : OpenAIHttpOptions = { } ,
@@ -219,57 +286,14 @@ async function smokeOpenAIWebRtc(browser: Browser, apiKey: string): Promise<Smok
219286 try {
220287 const page = await context . newPage ( ) ;
221288 await page . evaluate ( "globalThis.__name = (fn) => fn" ) ;
289+ await page . evaluate ( openAIRealtimeBrowserResponseReaderInitScript ( ) ) ;
222290 const result = await page . evaluate (
223291 async ( { clientSecret : secret , sdpAnswerMaxBytes, timeoutMs } ) => {
224- const responseBodyTooLargeError = ( label : string , maxBytes : number ) : Error =>
225- new Error ( `${ label } response body exceeded ${ maxBytes } bytes` ) ;
226- const readBoundedTextLocal = async (
227- response : Response ,
228- label : string ,
229- maxBytes : number ,
230- ) : Promise < string > => {
231- const contentLength = Number ( response . headers . get ( "content-length" ) ?? "" ) ;
232- if ( Number . isSafeInteger ( contentLength ) && contentLength > maxBytes ) {
233- await response . body ?. cancel ( ) . catch ( ( ) => undefined ) ;
234- throw responseBodyTooLargeError ( label , maxBytes ) ;
235- }
236- if ( ! response . body ) {
237- return "" ;
238- }
239-
240- const reader = response . body . getReader ( ) ;
241- const decoder = new TextDecoder ( ) ;
242- const chunks : string [ ] = [ ] ;
243- let totalBytes = 0 ;
244- let canceled = false ;
245-
246- try {
247- for ( ; ; ) {
248- const { done, value } = await reader . read ( ) ;
249- if ( done ) {
250- const tail = decoder . decode ( ) ;
251- if ( tail ) {
252- chunks . push ( tail ) ;
253- }
254- break ;
255- }
256-
257- totalBytes += value . byteLength ;
258- if ( totalBytes > maxBytes ) {
259- canceled = true ;
260- await reader . cancel ( ) . catch ( ( ) => undefined ) ;
261- throw responseBodyTooLargeError ( label , maxBytes ) ;
262- }
263- chunks . push ( decoder . decode ( value , { stream : true } ) ) ;
264- }
265- } finally {
266- if ( ! canceled ) {
267- reader . releaseLock ( ) ;
268- }
269- }
270-
271- return chunks . join ( "" ) ;
272- } ;
292+ const readBoundedTextLocal = ( globalThis as OpenAIWebRtcSmokeGlobal )
293+ . openclawReadBoundedRealtimeResponseText ;
294+ if ( ! readBoundedTextLocal ) {
295+ throw new Error ( "OpenAI Realtime bounded response reader was not installed" ) ;
296+ }
273297 const withBrowserTimeout = async < T > (
274298 label : string ,
275299 run : ( signal : AbortSignal ) => Promise < T > ,
@@ -327,12 +351,16 @@ async function smokeOpenAIWebRtc(browser: Browser, apiKey: string): Promise<Smok
327351 } ) ;
328352 const offer = await peer . createOffer ( ) ;
329353 await peer . setLocalDescription ( offer ) ;
354+ const offerSdp = offer . sdp ;
355+ if ( ! offerSdp ) {
356+ throw new Error ( "OpenAI Realtime SDP offer did not include SDP" ) ;
357+ }
330358 const answer = await withBrowserTimeout (
331359 "OpenAI Realtime SDP offer request" ,
332360 async ( signal ) => {
333361 const response = await fetch ( "https://api.openai.com/v1/realtime/calls" , {
334362 method : "POST" ,
335- body : offer . sdp ,
363+ body : offerSdp ,
336364 headers : {
337365 Authorization : `Bearer ${ secret } ` ,
338366 "Content-Type" : "application/sdp" ,
@@ -761,6 +789,7 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
761789export const testing = {
762790 OPENAI_HTTP_RESPONSE_MAX_BYTES ,
763791 createOpenAIClientSecret,
792+ readOpenAIRealtimeBrowserResponseText,
764793 readBoundedText,
765794 resolveOpenAIHttpTimeoutMs,
766795} ;
0 commit comments