@@ -133,6 +133,52 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
133133 } ) ;
134134}
135135
136+ function resolveRequestTimeoutMs ( options ?: OpenAICodexResponsesOptions ) : number | undefined {
137+ const timeoutMs = options ?. timeoutMs ;
138+ return typeof timeoutMs === "number" && Number . isFinite ( timeoutMs ) && timeoutMs > 0
139+ ? Math . floor ( timeoutMs )
140+ : undefined ;
141+ }
142+
143+ function buildRequestSignal (
144+ baseSignal : AbortSignal | undefined ,
145+ timeoutMs : number | undefined ,
146+ ) : AbortSignal | undefined {
147+ if ( timeoutMs === undefined ) {
148+ return baseSignal ;
149+ }
150+ const timeoutSignal = AbortSignal . timeout ( timeoutMs ) ;
151+ if ( ! baseSignal ) {
152+ return timeoutSignal ;
153+ }
154+ return AbortSignal . any ( [ baseSignal , timeoutSignal ] ) ;
155+ }
156+
157+ function isRequestTimeoutError (
158+ error : unknown ,
159+ callerSignal : AbortSignal | undefined ,
160+ requestSignal : AbortSignal | undefined ,
161+ timeoutMs : number | undefined ,
162+ ) : boolean {
163+ if ( timeoutMs === undefined || callerSignal ?. aborted || ! requestSignal ?. aborted ) {
164+ return false ;
165+ }
166+ if ( ! ( error instanceof Error ) ) {
167+ return false ;
168+ }
169+ return (
170+ error . name === "AbortError" ||
171+ error . name === "TimeoutError" ||
172+ error . message === "Request was aborted"
173+ ) ;
174+ }
175+
176+ function formatRequestTimeoutError ( timeoutMs : number , cause : unknown ) : Error {
177+ return new Error ( `Request timed out after ${ timeoutMs } ms` , {
178+ cause : cause instanceof Error ? cause : undefined ,
179+ } ) ;
180+ }
181+
136182// ============================================================================
137183// Main Stream Function
138184// ============================================================================
@@ -148,6 +194,8 @@ export const streamOpenAICodexResponses: StreamFunction<
148194 const stream = new AssistantMessageEventStream ( ) ;
149195
150196 void ( async ( ) => {
197+ let requestTimeoutMs : number | undefined ;
198+ let activeSignal : AbortSignal | undefined ;
151199 const output : AssistantMessage = {
152200 role : "assistant" ,
153201 content : [ ] ,
@@ -194,6 +242,10 @@ export const streamOpenAICodexResponses: StreamFunction<
194242 websocketRequestId ,
195243 ) ;
196244 const bodyJson = JSON . stringify ( body ) ;
245+ requestTimeoutMs = resolveRequestTimeoutMs ( options ) ;
246+ activeSignal = buildRequestSignal ( options ?. signal , requestTimeoutMs ) ;
247+ const requestOptions =
248+ activeSignal === options ?. signal ? options : { ...options , signal : activeSignal } ;
197249 const transport = options ?. transport || "auto" ;
198250 const websocketDisabledForSession =
199251 transport === "auto" && isWebSocketSseFallbackActive ( options ?. sessionId ) ;
@@ -214,10 +266,10 @@ export const streamOpenAICodexResponses: StreamFunction<
214266 ( ) => {
215267 websocketStarted = true ;
216268 } ,
217- options ,
269+ requestOptions ,
218270 ) ;
219271
220- if ( options ?. signal ?. aborted ) {
272+ if ( activeSignal ?. aborted ) {
221273 throw new Error ( "Request was aborted" ) ;
222274 }
223275 stream . push ( {
@@ -228,7 +280,7 @@ export const streamOpenAICodexResponses: StreamFunction<
228280 stream . end ( ) ;
229281 return ;
230282 } catch ( error ) {
231- const aborted = options ?. signal ?. aborted ;
283+ const aborted = activeSignal ?. aborted ;
232284 if ( aborted || isCodexNonTransportError ( error ) ) {
233285 throw error ;
234286 }
@@ -259,7 +311,7 @@ export const streamOpenAICodexResponses: StreamFunction<
259311 let lastError : Error | undefined ;
260312
261313 for ( let attempt = 0 ; attempt <= MAX_RETRIES ; attempt ++ ) {
262- if ( options ?. signal ?. aborted ) {
314+ if ( activeSignal ?. aborted ) {
263315 throw new Error ( "Request was aborted" ) ;
264316 }
265317
@@ -268,7 +320,7 @@ export const streamOpenAICodexResponses: StreamFunction<
268320 method : "POST" ,
269321 headers : sseHeaders ,
270322 body : bodyJson ,
271- signal : options ?. signal ,
323+ signal : activeSignal ,
272324 } ) ;
273325 await options ?. onResponse ?.(
274326 { status : response . status , headers : headersToRecord ( response . headers ) } ,
@@ -304,7 +356,7 @@ export const streamOpenAICodexResponses: StreamFunction<
304356 }
305357 }
306358
307- await sleep ( delayMs , options ?. signal ) ;
359+ await sleep ( delayMs , activeSignal ) ;
308360 continue ;
309361 }
310362
@@ -317,15 +369,24 @@ export const streamOpenAICodexResponses: StreamFunction<
317369 throw new Error ( info . friendlyMessage || info . message ) ;
318370 } catch ( error ) {
319371 if ( error instanceof Error ) {
372+ if (
373+ isRequestTimeoutError ( error , options ?. signal , activeSignal , requestTimeoutMs ) &&
374+ requestTimeoutMs !== undefined
375+ ) {
376+ throw formatRequestTimeoutError ( requestTimeoutMs , error ) ;
377+ }
320378 if ( error . name === "AbortError" || error . message === "Request was aborted" ) {
321379 throw new Error ( "Request was aborted" , { cause : error } ) ;
322380 }
381+ if ( error . name === "TimeoutError" && requestTimeoutMs !== undefined ) {
382+ throw new Error ( `Request timed out after ${ requestTimeoutMs } ms` , { cause : error } ) ;
383+ }
323384 }
324385 lastError = error instanceof Error ? error : new Error ( String ( error ) ) ;
325386 // Network errors are retryable
326387 if ( attempt < MAX_RETRIES && ! lastError . message . includes ( "usage limit" ) ) {
327388 const delayMs = BASE_DELAY_MS * 2 ** attempt ;
328- await sleep ( delayMs , options ?. signal ) ;
389+ await sleep ( delayMs , activeSignal ) ;
329390 continue ;
330391 }
331392 throw lastError ;
@@ -343,7 +404,7 @@ export const streamOpenAICodexResponses: StreamFunction<
343404 stream . push ( { type : "start" , partial : output } ) ;
344405 await processStream ( response , output , stream , model , options ) ;
345406
346- if ( options ?. signal ?. aborted ) {
407+ if ( activeSignal ?. aborted ) {
347408 throw new Error ( "Request was aborted" ) ;
348409 }
349410
@@ -354,12 +415,18 @@ export const streamOpenAICodexResponses: StreamFunction<
354415 } ) ;
355416 stream . end ( ) ;
356417 } catch ( error ) {
418+ const normalizedError =
419+ isRequestTimeoutError ( error , options ?. signal , activeSignal , requestTimeoutMs ) &&
420+ requestTimeoutMs !== undefined
421+ ? formatRequestTimeoutError ( requestTimeoutMs , error )
422+ : error ;
357423 for ( const block of output . content ) {
358424 // partialJson is only a streaming scratch buffer; never persist it.
359425 delete ( block as { partialJson ?: string } ) . partialJson ;
360426 }
361427 output . stopReason = options ?. signal ?. aborted ? "aborted" : "error" ;
362- output . errorMessage = error instanceof Error ? error . message : String ( error ) ;
428+ output . errorMessage =
429+ normalizedError instanceof Error ? normalizedError . message : String ( normalizedError ) ;
363430 stream . push ( { type : "error" , reason : output . stopReason , error : output } ) ;
364431 stream . end ( ) ;
365432 }
@@ -975,6 +1042,11 @@ async function connectWebSocket(
9751042 signal ?. removeEventListener ( "abort" , onAbort ) ;
9761043 } ;
9771044
1045+ if ( signal ?. aborted ) {
1046+ onAbort ( ) ;
1047+ return ;
1048+ }
1049+
9781050 socket . addEventListener ( "open" , onOpen ) ;
9791051 socket . addEventListener ( "error" , onError ) ;
9801052 socket . addEventListener ( "close" , onClose ) ;
@@ -1374,6 +1446,9 @@ async function processWebSocketStream(
13741446 }
13751447 }
13761448 try {
1449+ if ( options ?. signal ?. aborted ) {
1450+ throw new Error ( "Request was aborted" ) ;
1451+ }
13771452 socket . send ( JSON . stringify ( { type : "response.create" , ...requestBody } ) ) ;
13781453 await processResponsesStream (
13791454 startWebSocketOutputOnFirstEvent (
0 commit comments