@@ -135,13 +135,6 @@ interface RequestBody {
135135// Retry Helpers
136136// ============================================================================
137137
138- /**
139- * Marks a provider response the retry loop already classified as non-retryable, so the
140- * surrounding catch (which treats unknown failures as retryable network errors) rethrows
141- * it untouched instead of resending the request.
142- */
143- class NonRetryableCodexResponseError extends Error { }
144-
145138function isRetryableError ( status : number , errorText : string ) : boolean {
146139 if ( status === 429 || status === 500 || status === 502 || status === 503 || status === 504 ) {
147140 return true ;
@@ -151,6 +144,32 @@ function isRetryableError(status: number, errorText: string): boolean {
151144 ) ;
152145}
153146
147+ function resolveHttpRetryDelayMs ( response : Response , attempt : number ) : number {
148+ const fallbackMs = BASE_DELAY_MS * 2 ** attempt ;
149+ const retryAfterMs = response . headers . get ( "retry-after-ms" ) ;
150+ if ( retryAfterMs !== null ) {
151+ const trimmed = retryAfterMs . trim ( ) ;
152+ const millis = Number ( trimmed ) ;
153+ return / ^ \d + (?: \. \d + ) ? $ / . test ( trimmed ) && Number . isFinite ( millis )
154+ ? ( clampTimerTimeoutMs ( millis , 0 ) ?? fallbackMs )
155+ : fallbackMs ;
156+ }
157+
158+ const retryAfter = response . headers . get ( "retry-after" ) ;
159+ if ( ! retryAfter ) {
160+ return fallbackMs ;
161+ }
162+ const trimmed = retryAfter . trim ( ) ;
163+ const seconds = Number ( trimmed ) ;
164+ if ( / ^ \d + $ / . test ( trimmed ) && Number . isFinite ( seconds ) ) {
165+ return clampTimerTimeoutMs ( seconds * 1000 , 0 ) ?? fallbackMs ;
166+ }
167+ const retryAt = parseRetryAfterHttpDateMs ( trimmed ) ;
168+ return retryAt === undefined
169+ ? fallbackMs
170+ : ( clampTimerTimeoutMs ( retryAt - Date . now ( ) , 0 ) ?? fallbackMs ) ;
171+ }
172+
154173function resolveRequestTimeoutMs ( options ?: OpenAICodexResponsesOptions ) : number | undefined {
155174 const timeoutMs = options ?. timeoutMs ;
156175 return typeof timeoutMs === "number" && Number . isFinite ( timeoutMs ) && timeoutMs > 0
@@ -380,64 +399,26 @@ export const streamOpenAICodexResponses: StreamFunction<
380399 throw new Error ( "Request was aborted" ) ;
381400 }
382401
402+ let attemptResponse : Response ;
403+ let errorText : string ;
383404 try {
384- response = await fetch ( resolveCodexUrl ( model . baseUrl ) , {
405+ attemptResponse = await fetch ( resolveCodexUrl ( model . baseUrl ) , {
385406 method : "POST" ,
386407 headers : sseHeaders ,
387408 body : sseBody ,
388409 signal : activeSignal ,
389410 } ) ;
411+ response = attemptResponse ;
390412 await options ?. onResponse ?.(
391- { status : response . status , headers : headersToRecord ( response . headers ) } ,
413+ { status : attemptResponse . status , headers : headersToRecord ( attemptResponse . headers ) } ,
392414 model ,
393415 ) ;
394416
395- if ( response . ok ) {
417+ if ( attemptResponse . ok ) {
396418 break ;
397419 }
398-
399- const errorText = await readChatGptResponsesErrorTextLimited ( response ) ;
400- if ( attempt < maxRetries && isRetryableError ( response . status , errorText ) ) {
401- let delayMs = BASE_DELAY_MS * 2 ** attempt ;
402-
403- const retryAfterMs = response . headers . get ( "retry-after-ms" ) ;
404- if ( retryAfterMs !== null ) {
405- const trimmedRetryAfterMs = retryAfterMs . trim ( ) ;
406- const millis = Number ( trimmedRetryAfterMs ) ;
407- if ( / ^ \d + (?: \. \d + ) ? $ / . test ( trimmedRetryAfterMs ) && Number . isFinite ( millis ) ) {
408- delayMs = clampTimerTimeoutMs ( millis , 0 ) ?? delayMs ;
409- }
410- } else {
411- const retryAfter = response . headers . get ( "retry-after" ) ;
412- if ( retryAfter ) {
413- const trimmedRetryAfter = retryAfter . trim ( ) ;
414- const seconds = Number ( trimmedRetryAfter ) ;
415- if ( / ^ \d + $ / . test ( trimmedRetryAfter ) && Number . isFinite ( seconds ) ) {
416- delayMs = clampTimerTimeoutMs ( seconds * 1000 , 0 ) ?? delayMs ;
417- } else {
418- const retryAt = parseRetryAfterHttpDateMs ( trimmedRetryAfter ) ;
419- if ( retryAt !== undefined ) {
420- delayMs = clampTimerTimeoutMs ( retryAt - Date . now ( ) , 0 ) ?? delayMs ;
421- }
422- }
423- }
424- }
425-
426- await sleepWithAbort ( delayMs , activeSignal ) ;
427- continue ;
428- }
429-
430- // Parse error for friendly message on final attempt or non-retryable error
431- const fakeResponse = new Response ( errorText , {
432- status : response . status ,
433- statusText : response . statusText ,
434- } ) ;
435- const info = await parseErrorResponse ( fakeResponse ) ;
436- throw new NonRetryableCodexResponseError ( info . friendlyMessage || info . message ) ;
420+ errorText = await readChatGptResponsesErrorTextLimited ( attemptResponse ) ;
437421 } catch ( error ) {
438- if ( error instanceof NonRetryableCodexResponseError ) {
439- throw error ;
440- }
441422 if ( error instanceof Error ) {
442423 if (
443424 isRequestTimeoutError (
@@ -466,6 +447,18 @@ export const streamOpenAICodexResponses: StreamFunction<
466447 }
467448 throw lastError ;
468449 }
450+
451+ if ( attempt < maxRetries && isRetryableError ( attemptResponse . status , errorText ) ) {
452+ await sleepWithAbort ( resolveHttpRetryDelayMs ( attemptResponse , attempt ) , activeSignal ) ;
453+ continue ;
454+ }
455+
456+ const info = parseErrorResponseText (
457+ errorText ,
458+ attemptResponse . status ,
459+ attemptResponse . statusText ,
460+ ) ;
461+ throw new Error ( info . friendlyMessage || info . message ) ;
469462 }
470463
471464 if ( ! response ?. ok ) {
@@ -1589,11 +1582,12 @@ async function readChatGptResponsesErrorTextLimited(response: Response): Promise
15891582 return text ;
15901583}
15911584
1592- async function parseErrorResponse (
1593- response : Response ,
1594- ) : Promise < { message : string ; friendlyMessage ?: string } > {
1595- const raw = await readChatGptResponsesErrorTextLimited ( response ) ;
1596- let message = raw || response . statusText || "Request failed" ;
1585+ function parseErrorResponseText (
1586+ raw : string ,
1587+ status : number ,
1588+ statusText : string ,
1589+ ) : { message : string ; friendlyMessage ?: string } {
1590+ let message = raw || statusText || "Request failed" ;
15971591 let friendlyMessage : string | undefined ;
15981592
15991593 try {
@@ -1611,7 +1605,7 @@ async function parseErrorResponse(
16111605 const code = err . code || err . type || "" ;
16121606 if (
16131607 / u s a g e _ l i m i t _ r e a c h e d | u s a g e _ n o t _ i n c l u d e d | r a t e _ l i m i t _ e x c e e d e d / i. test ( code ) ||
1614- response . status === 429
1608+ status === 429
16151609 ) {
16161610 const plan = err . plan_type ? ` (${ err . plan_type . toLowerCase ( ) } plan)` : "" ;
16171611 const mins = err . resets_at
0 commit comments