@@ -24,6 +24,8 @@ const COOLDOWN_MS = {
2424 auth : 2 * 60_000 ,
2525 transient : 30_000 ,
2626} ;
27+ const TRANSPORT_RETRY_DELAY_MS = 10_000 ;
28+ const TRANSPORT_RETRY_ATTEMPTS = 2 ;
2729const PREOUTPUT_INSPECTION_BYTES = 64 * 1024 ;
2830const CLAUDE_CODE_CANONICAL_ROUTES = new Map ( [
2931 [ "claude-fable-5" , "fable" ] ,
@@ -746,6 +748,66 @@ function isAbortError(err) {
746748 return / a b o r t e d | t i m e o u t | T i m e o u t E r r o r / i. test ( msg ) ;
747749}
748750
751+ function transportErrorMessage ( error ) {
752+ const parts = [ ] ;
753+ const seen = new Set ( ) ;
754+ let current = error ;
755+ for ( let depth = 0 ; current && depth < 4 ; depth += 1 ) {
756+ if ( seen . has ( current ) ) break ;
757+ seen . add ( current ) ;
758+ const code = typeof current . code === "string" ? current . code . trim ( ) : "" ;
759+ const message = String ( current . message || current ) . trim ( ) ;
760+ const detail = code && ! message . includes ( code ) ? `${ code } : ${ message } ` : message ;
761+ if ( detail && ! parts . includes ( detail ) ) parts . push ( detail ) ;
762+ current = current . cause ;
763+ }
764+ return parts . join ( "; " ) || "Upstream connection failed" ;
765+ }
766+
767+ function isTransportError ( error ) {
768+ const codes = new Set ( [
769+ "EAI_AGAIN" ,
770+ "ECONNABORTED" ,
771+ "ECONNREFUSED" ,
772+ "ECONNRESET" ,
773+ "EHOSTUNREACH" ,
774+ "ENETDOWN" ,
775+ "ENETUNREACH" ,
776+ "ENOTFOUND" ,
777+ "EPIPE" ,
778+ "ETIMEDOUT" ,
779+ "UND_ERR_CONNECT_TIMEOUT" ,
780+ "UND_ERR_HEADERS_TIMEOUT" ,
781+ "UND_ERR_SOCKET" ,
782+ ] ) ;
783+ const seen = new Set ( ) ;
784+ let current = error ;
785+ for ( let depth = 0 ; current && depth < 4 ; depth += 1 ) {
786+ if ( seen . has ( current ) ) break ;
787+ seen . add ( current ) ;
788+ if ( codes . has ( current . code ) ) return true ;
789+ if ( / \b f e t c h f a i l e d \b / i. test ( String ( current . message || "" ) ) ) return true ;
790+ current = current . cause ;
791+ }
792+ return false ;
793+ }
794+
795+ function waitForTransportRetry ( ms , signal ) {
796+ if ( signal ?. aborted ) return Promise . resolve ( false ) ;
797+ if ( ! ms || ms <= 0 ) return Promise . resolve ( true ) ;
798+ return new Promise ( ( resolve ) => {
799+ let timer ;
800+ const finish = ( ready ) => {
801+ if ( timer ) clearTimeout ( timer ) ;
802+ signal ?. removeEventListener ( "abort" , onAbort ) ;
803+ resolve ( ready ) ;
804+ } ;
805+ const onAbort = ( ) => finish ( false ) ;
806+ timer = setTimeout ( ( ) => finish ( true ) , ms ) ;
807+ signal ?. addEventListener ( "abort" , onAbort , { once : true } ) ;
808+ } ) ;
809+ }
810+
749811function memberSignal ( outer , timeoutMs ) {
750812 const ms = timeoutMs ?? REQUEST_TIMEOUT_MS ;
751813 const ctrl = new AbortController ( ) ;
@@ -790,7 +852,16 @@ function memberSignal(outer, timeoutMs) {
790852/**
791853 * @param {{ store, fetchImpl?, requestLog?, timeoutMs?, usage?, logger? } } opts
792854 */
793- function createRouter ( { store, fetchImpl = fetch , requestLog, timeoutMs, usage, logger = appLogger } = { } ) {
855+ function createRouter ( {
856+ store,
857+ fetchImpl = fetch ,
858+ requestLog,
859+ timeoutMs,
860+ usage,
861+ logger = appLogger ,
862+ transportRetryDelayMs = TRANSPORT_RETRY_DELAY_MS ,
863+ transportRetryAttempts = TRANSPORT_RETRY_ATTEMPTS ,
864+ } = { } ) {
794865 const rrState = new Map ( ) ;
795866 const log = requestLog || createRequestLog ( ) ;
796867 let totalRequests = 0 ;
@@ -1158,17 +1229,29 @@ function createRouter({ store, fetchImpl = fetch, requestLog, timeoutMs, usage,
11581229 return {
11591230 ok : false ,
11601231 status : 408 ,
1161- error : e . message || "Upstream request timeout" ,
1162- cooldownEligible : true ,
1163- failureKind : "transient" ,
1164- defaultCooldownMs : COOLDOWN_MS . transient ,
1232+ error : transportErrorMessage ( e ) || "Upstream request timeout" ,
1233+ cooldownEligible : false ,
1234+ failureKind : "transport" ,
1235+ defaultCooldownMs : 0 ,
1236+ transportError : false ,
11651237 } ;
11661238 }
1167- const error = e . message || String ( e ) ;
1239+ const error = transportErrorMessage ( e ) ;
11681240 const classification = classifyFailure ( 502 , error ) ;
1241+ if ( classification . kind !== "request" && isTransportError ( e ) ) {
1242+ return {
1243+ ok : false ,
1244+ status : 502 ,
1245+ error,
1246+ cooldownEligible : false ,
1247+ failureKind : "transport" ,
1248+ defaultCooldownMs : 0 ,
1249+ transportError : true ,
1250+ } ;
1251+ }
11691252 return {
11701253 ok : false ,
1171- status : classification . kind === "request" ? 400 : 502 ,
1254+ status : 400 ,
11721255 error,
11731256 cooldownEligible : classification . eligible ,
11741257 failureKind : classification . kind ,
@@ -1290,19 +1373,55 @@ function createRouter({ store, fetchImpl = fetch, requestLog, timeoutMs, usage,
12901373 }
12911374 }
12921375 let result ;
1293- try {
1294- result = await tryMember ( attemptMember , body , stream , signal ) ;
1295- } catch ( e ) {
1296- const status = isAbortError ( e ) ? 408 : 500 ;
1297- const classification = classifyFailure ( status , e . message ) ;
1298- result = {
1299- ok : false ,
1300- status,
1301- error : e . message || String ( e ) ,
1302- cooldownEligible : classification . eligible ,
1303- failureKind : classification . kind ,
1304- defaultCooldownMs : classification . defaultCooldownMs ,
1305- } ;
1376+ let transportRetry = 0 ;
1377+ while ( true ) {
1378+ try {
1379+ result = await tryMember ( attemptMember , body , stream , signal ) ;
1380+ } catch ( e ) {
1381+ const status = isAbortError ( e ) ? 408 : 500 ;
1382+ const error = transportErrorMessage ( e ) ;
1383+ const classification = classifyFailure ( status , error ) ;
1384+ const transportError = isTransportError ( e ) ;
1385+ result = {
1386+ ok : false ,
1387+ status,
1388+ error,
1389+ cooldownEligible : transportError ? false : classification . eligible ,
1390+ failureKind : transportError ? "transport" : classification . kind ,
1391+ defaultCooldownMs : transportError ? 0 : classification . defaultCooldownMs ,
1392+ transportError,
1393+ } ;
1394+ }
1395+ if ( ! result . transportError || result . canceled || transportRetry >= transportRetryAttempts ) {
1396+ break ;
1397+ }
1398+ transportRetry += 1 ;
1399+ logger . warn ( "router transport retry scheduled" , {
1400+ event : "transport_retry_scheduled" ,
1401+ routeModel : modelId ,
1402+ upstreamModel : member . upstreamModel ,
1403+ providerId : provider . id ,
1404+ providerType : canonicalProviderType ( provider . type ) ,
1405+ providerName : provider . name ,
1406+ accountAlias : provider . accountAlias || null ,
1407+ status : result . status ,
1408+ error : result . error ,
1409+ retryAttempt : transportRetry ,
1410+ maxRetries : transportRetryAttempts ,
1411+ retryDelayMs : transportRetryDelayMs ,
1412+ } ) ;
1413+ if ( ! ( await waitForTransportRetry ( transportRetryDelayMs , signal ) ) ) {
1414+ result = {
1415+ ok : false ,
1416+ status : 499 ,
1417+ error : "Client disconnected" ,
1418+ cooldownEligible : false ,
1419+ canceled : true ,
1420+ failureKind : "canceled" ,
1421+ defaultCooldownMs : 0 ,
1422+ } ;
1423+ break ;
1424+ }
13061425 }
13071426
13081427 if ( result . ok ) {
@@ -1359,15 +1478,22 @@ function createRouter({ store, fetchImpl = fetch, requestLog, timeoutMs, usage,
13591478 } ;
13601479 }
13611480
1362- const savedLock = persistModelLock ( provider , member . upstreamModel , result ) ;
1481+ const transportCause = result . transportError ? result . error : null ;
1482+ const publicError = result . transportError
1483+ ? `Upstream connection failed after ${ transportRetryAttempts } retries`
1484+ : result . error ;
1485+ const savedLock = persistModelLock ( provider , member . upstreamModel , {
1486+ ...result ,
1487+ error : publicError ,
1488+ } ) ;
13631489 const failed = {
13641490 providerId : provider . id ,
13651491 providerType : canonicalProviderType ( provider . type ) ,
13661492 providerName : provider . name ,
13671493 accountAlias : provider . accountAlias || null ,
13681494 model : member . upstreamModel ,
13691495 status : result . status ,
1370- error : result . error ,
1496+ error : publicError ,
13711497 failureKind : result . failureKind ,
13721498 retryable : true ,
13731499 lockedUntil : savedLock ?. until || null ,
@@ -1378,6 +1504,7 @@ function createRouter({ store, fetchImpl = fetch, requestLog, timeoutMs, usage,
13781504 event : oauthAttempt ? "account_failure" : "route_member_failure" ,
13791505 routeModel : modelId ,
13801506 ...failed ,
1507+ ...( transportCause ? { transportCause } : { } ) ,
13811508 } ) ;
13821509 fallbackFrom = failed ;
13831510 }
0 commit comments