@@ -31,6 +31,17 @@ export type OpenAiNativeModel = ReturnType<OpenAiNativeHandler["getModel"]>
3131// Constants for model identification
3232const GPT5_MODEL_PREFIX = "gpt-5"
3333
34+ // Marker for terminal background-mode failures so we don't attempt resume/poll fallbacks
35+ function createTerminalBackgroundError ( message : string ) : Error {
36+ const err = new Error ( message )
37+ ; ( err as any ) . isTerminalBackgroundError = true
38+ err . name = "TerminalBackgroundError"
39+ return err
40+ }
41+ function isTerminalBackgroundError ( err : any ) : boolean {
42+ return ! ! ( err && ( err as any ) . isTerminalBackgroundError )
43+ }
44+
3445export class OpenAiNativeHandler extends BaseProvider implements SingleCompletionHandler {
3546 protected options : ApiHandlerOptions
3647 private client : OpenAI
@@ -338,6 +349,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
338349 }
339350 }
340351 } catch ( iterErr ) {
352+ // If terminal failure, propagate and do not attempt resume/poll
353+ if ( isTerminalBackgroundError ( iterErr ) ) {
354+ throw iterErr
355+ }
341356 // Stream dropped mid-flight; attempt resume for background requests
342357 if ( canAttemptResume ( ) ) {
343358 for await ( const chunk of this . attemptResumeOrPoll (
@@ -352,6 +367,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
352367 throw iterErr
353368 }
354369 } catch ( sdkErr : any ) {
370+ // Propagate terminal background failures without fallback
371+ if ( isTerminalBackgroundError ( sdkErr ) ) {
372+ throw sdkErr
373+ }
355374 // Check if this is a 400 error about previous_response_id not found
356375 const errorMessage = sdkErr ?. message || sdkErr ?. error ?. message || ""
357376 const is400Error = sdkErr ?. status === 400 || sdkErr ?. response ?. status === 400
@@ -412,6 +431,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
412431 }
413432 return
414433 } catch ( iterErr ) {
434+ if ( isTerminalBackgroundError ( iterErr ) ) {
435+ throw iterErr
436+ }
415437 if ( canAttemptResume ( ) ) {
416438 for await ( const chunk of this . attemptResumeOrPoll (
417439 this . lastResponseId ! ,
@@ -425,6 +447,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
425447 throw iterErr
426448 }
427449 } catch ( retryErr ) {
450+ if ( isTerminalBackgroundError ( retryErr ) ) {
451+ throw retryErr
452+ }
428453 // If retry also fails, fall back to SSE
429454 try {
430455 yield * this . makeGpt5ResponsesAPIRequest (
@@ -436,6 +461,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
436461 )
437462 return
438463 } catch ( fallbackErr ) {
464+ if ( isTerminalBackgroundError ( fallbackErr ) ) {
465+ throw fallbackErr
466+ }
439467 if ( canAttemptResume ( ) ) {
440468 for await ( const chunk of this . attemptResumeOrPoll (
441469 this . lastResponseId ! ,
@@ -456,6 +484,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
456484 yield * this . makeGpt5ResponsesAPIRequest ( requestBody , model , metadata , systemPrompt , messages )
457485 } catch ( fallbackErr ) {
458486 // If SSE fallback fails mid-stream and we can resume, try that
487+ if ( isTerminalBackgroundError ( fallbackErr ) ) {
488+ throw fallbackErr
489+ }
459490 if ( canAttemptResume ( ) ) {
460491 for await ( const chunk of this . attemptResumeOrPoll (
461492 this . lastResponseId ! ,
@@ -1058,9 +1089,20 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
10581089 else if ( parsed . type === "response.error" || parsed . type === "error" ) {
10591090 // Error event from the API
10601091 if ( parsed . error || parsed . message ) {
1061- throw new Error (
1062- `Responses API error: ${ parsed . error ?. message || parsed . message || "Unknown error" } ` ,
1063- )
1092+ const errMsg = `Responses API error: ${ parsed . error ?. message || parsed . message || "Unknown error" } `
1093+ // For background mode, treat as terminal to avoid futile resume attempts
1094+ if ( this . currentRequestIsBackground ) {
1095+ // Surface a failed status for UI lifecycle before terminating
1096+ yield {
1097+ type : "status" ,
1098+ mode : "background" ,
1099+ status : "failed" ,
1100+ ...( parsed . response ?. id ? { responseId : parsed . response . id } : { } ) ,
1101+ }
1102+ throw createTerminalBackgroundError ( errMsg )
1103+ }
1104+ // Non-background: propagate as a standard error
1105+ throw new Error ( errMsg )
10641106 }
10651107 }
10661108 // Handle incomplete event
@@ -1096,7 +1138,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
10961138 }
10971139 // Response failed
10981140 if ( parsed . error || parsed . message ) {
1099- throw new Error (
1141+ throw createTerminalBackgroundError (
11001142 `Response failed: ${ parsed . error ?. message || parsed . message || "Unknown failure" } ` ,
11011143 )
11021144 }
@@ -1227,6 +1269,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
12271269 // This can happen in certain edge cases and shouldn't break the flow
12281270 } catch ( error ) {
12291271 if ( error instanceof Error ) {
1272+ // Preserve terminal background errors so callers can avoid resume attempts
1273+ if ( ( error as any ) . isTerminalBackgroundError ) {
1274+ throw error
1275+ }
12301276 throw new Error ( `Error processing response stream: ${ error . message } ` )
12311277 }
12321278 throw new Error ( "Unexpected error processing response stream" )
@@ -1264,25 +1310,25 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
12641310 } ,
12651311 } )
12661312
1267- if ( ! res . ok || ! res . body ) {
1313+ if ( ! res . ok ) {
12681314 throw new Error ( `Resume request failed (${ res . status } )` )
12691315 }
1316+ if ( ! res . body ) {
1317+ throw new Error ( "Resume request failed (no body)" )
1318+ }
12701319
12711320 this . resumeCutoffSequence = lastSeq
12721321
1273- let emittedInProgress = false
1322+ // Handshake accepted: immediately switch UI from reconnecting -> in_progress
1323+ yield {
1324+ type : "status" ,
1325+ mode : "background" ,
1326+ status : "in_progress" ,
1327+ responseId,
1328+ }
1329+
12741330 try {
12751331 for await ( const chunk of this . handleStreamResponse ( res . body , model ) ) {
1276- // After the handshake and first accepted chunk, emit in_progress once
1277- if ( ! emittedInProgress ) {
1278- emittedInProgress = true
1279- yield {
1280- type : "status" ,
1281- mode : "background" ,
1282- status : "in_progress" ,
1283- responseId,
1284- }
1285- }
12861332 // Avoid double-emitting in_progress if the inner handler surfaces it
12871333 if ( chunk . type === "status" && ( chunk as any ) . status === "in_progress" ) {
12881334 continue
@@ -1297,9 +1343,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
12971343 this . resumeCutoffSequence = undefined
12981344 throw e
12991345 }
1300- } catch {
1301- // Wait with backoff before next attempt
1346+ } catch ( err ) {
1347+ // If terminal error, don't keep retrying resume; fall back to polling immediately
13021348 const delay = resumeBaseDelayMs * Math . pow ( 2 , attempt )
1349+ if ( isTerminalBackgroundError ( err ) ) {
1350+ break
1351+ }
1352+ // Otherwise retry with backoff
13031353 if ( delay > 0 ) {
13041354 await new Promise ( ( r ) => setTimeout ( r , delay ) )
13051355 }
@@ -1413,10 +1463,21 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
14131463 }
14141464
14151465 if ( status === "failed" || status === "canceled" ) {
1416- throw new Error ( `Response ${ status } : ${ respId || responseId } ` )
1466+ const detail : string | undefined = resp ?. error ?. message ?? raw ?. error ?. message
1467+ const msg = detail ? `Response ${ status } : ${ detail } ` : `Response ${ status } : ${ respId || responseId } `
1468+ throw createTerminalBackgroundError ( msg )
1469+ }
1470+ } catch ( err ) {
1471+ // If we've already emitted a terminal status, propagate to consumer to stop polling.
1472+ if ( lastEmittedStatus === "failed" || lastEmittedStatus === "canceled" ) {
1473+ throw err
14171474 }
1418- } catch {
1419- // ignore transient poll errors
1475+ // Otherwise ignore transient poll errors
1476+ }
1477+
1478+ // Stop polling immediately on terminal background statuses
1479+ if ( lastEmittedStatus === "failed" || lastEmittedStatus === "canceled" ) {
1480+ throw new Error ( `Background polling terminated with status=${ lastEmittedStatus } for ${ responseId } ` )
14201481 }
14211482
14221483 await new Promise ( ( r ) => setTimeout ( r , pollIntervalMs ) )
@@ -1463,6 +1524,11 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
14631524 if ( mappedStatus === "completed" || mappedStatus === "failed" || mappedStatus === "canceled" ) {
14641525 this . currentRequestIsBackground = undefined
14651526 }
1527+ // Throw terminal error to integrate with standard failure path (surfaced in UI)
1528+ if ( mappedStatus === "failed" || mappedStatus === "canceled" ) {
1529+ const msg = ( event as any ) ?. error ?. message || ( event as any ) ?. message || `Response ${ mappedStatus } `
1530+ throw createTerminalBackgroundError ( msg )
1531+ }
14661532 // Do not return; allow further handling (e.g., usage on done/completed)
14671533 }
14681534
0 commit comments