@@ -40,6 +40,7 @@ type StreamState = {
4040// endpoint. OR finalizes generation records asynchronously; 500ms is enough
4141// in practice and keeps the delay off the client response path.
4242const GENERATION_LOOKUP_DELAY_MS = 500
43+ const DISCONNECTED_STREAM_DRAIN_TIMEOUT_MS = 2 * 60 * 1000
4344
4445// Extended timeout for deep-thinking models (e.g., gpt-5) that can take
4546// a long time to start streaming.
@@ -363,6 +364,7 @@ export async function handleOpenRouterStream({
363364 billed : false ,
364365 }
365366 let clientDisconnected = false
367+ let disconnectedStreamDrainTimeout : NodeJS . Timeout | null = null
366368
367369 // Runs once on any stream-exit path. If we didn't bill through the normal
368370 // path (stream ended without a usage chunk, got a provider error chunk,
@@ -488,12 +490,41 @@ export async function handleOpenRouterStream({
488490 }
489491 await ensureBilled ( )
490492 } finally {
493+ if ( disconnectedStreamDrainTimeout ) {
494+ clearTimeout ( disconnectedStreamDrainTimeout )
495+ }
491496 clearInterval ( heartbeatInterval )
492497 }
493498 } ,
494499 cancel ( ) {
495500 clearInterval ( heartbeatInterval )
496501 clientDisconnected = true
502+ disconnectedStreamDrainTimeout = setTimeout ( ( ) => {
503+ const stateSummary = {
504+ clientDisconnected,
505+ responseTextLength : state . responseText . length ,
506+ reasoningTextLength : state . reasoningText . length ,
507+ generationId : state . generationId ,
508+ billed : state . billed ,
509+ }
510+ if ( ! state . billed && ! state . generationId ) {
511+ logger . warn (
512+ stateSummary ,
513+ 'Disconnected OpenRouter stream exceeded drain timeout before fallback billing was possible; continuing to drain' ,
514+ )
515+ return
516+ }
517+ logger . warn (
518+ stateSummary ,
519+ 'Cancelling disconnected OpenRouter stream after drain timeout' ,
520+ )
521+ reader . cancel ( 'client disconnected drain timeout' ) . catch ( ( error ) => {
522+ logger . warn (
523+ { error } ,
524+ 'Failed to cancel disconnected OpenRouter stream' ,
525+ )
526+ } )
527+ } , DISCONNECTED_STREAM_DRAIN_TIMEOUT_MS )
497528 // Log truncated state to prevent OOM during logging (state can be up to 2MB)
498529 logger . warn (
499530 {
0 commit comments