@@ -151,6 +151,11 @@ import {
151151 parseEnvInt ,
152152 parseFailoverMode ,
153153} from "./lib/request/failover-config.js" ;
154+ import {
155+ buildStreamFailoverCandidateOrder ,
156+ capStreamFailoverMax ,
157+ computeOutboundRequestAttemptBudget ,
158+ } from "./lib/request/request-attempt-budget.js" ;
154159import {
155160 evaluateFailurePolicy ,
156161 type FailoverMode ,
@@ -732,8 +737,10 @@ let sessionAffinityWriteVersion = 0;
732737 ) ;
733738 const streamFailoverMax = Math . max (
734739 0 ,
735- parseEnvInt ( process . env . CODEX_AUTH_STREAM_FAILOVER_MAX ) ??
736- STREAM_FAILOVER_MAX_BY_MODE [ failoverMode ] ,
740+ capStreamFailoverMax (
741+ parseEnvInt ( process . env . CODEX_AUTH_STREAM_FAILOVER_MAX ) ??
742+ STREAM_FAILOVER_MAX_BY_MODE [ failoverMode ] ,
743+ ) ,
737744 ) ;
738745 const streamFailoverSoftTimeoutMs = Math . max (
739746 1_000 ,
@@ -919,11 +926,36 @@ let sessionAffinityWriteVersion = 0;
919926 ) ;
920927 runtimeMetrics . lastRequestAt = Date . now ( ) ;
921928
922- const abortSignal = requestInit ?. signal ?? init ?. signal ?? null ;
923- const sleep = createAbortableSleep ( abortSignal ) ;
929+ const abortSignal = requestInit ?. signal ?? init ?. signal ?? null ;
930+ const sleep = createAbortableSleep ( abortSignal ) ;
931+ const maxOutboundRequestAttempts =
932+ computeOutboundRequestAttemptBudget ( {
933+ accountCount : accountManager . getAccountCount ( ) ,
934+ maxSameAccountRetries,
935+ emptyResponseMaxRetries,
936+ streamFailoverMax,
937+ } ) ;
938+ let outboundRequestAttemptsRemaining =
939+ maxOutboundRequestAttempts ;
940+ const tryConsumeOutboundRequestAttempt = (
941+ reason : "primary" | "stream-failover" ,
942+ accountIndex : number ,
943+ ) : boolean => {
944+ if ( outboundRequestAttemptsRemaining <= 0 ) {
945+ runtimeMetrics . lastError =
946+ `Request attempt budget exhausted after ${ maxOutboundRequestAttempts } outbound request(s)` ;
947+ logWarn (
948+ `Stopping ${ reason } replay after exhausting ${ maxOutboundRequestAttempts } outbound request(s) on account ${ accountIndex + 1 } .` ,
949+ ) ;
950+ return false ;
951+ }
952+
953+ outboundRequestAttemptsRemaining -= 1 ;
954+ return true ;
955+ } ;
924956
925- let allRateLimitedRetries = 0 ;
926- let emptyResponseRetries = 0 ;
957+ let allRateLimitedRetries = 0 ;
958+ let emptyResponseRetries = 0 ;
927959 const attemptedUnsupportedFallbackModels = new Set < string > ( ) ;
928960 if ( model ) {
929961 attemptedUnsupportedFallbackModels . add ( model ) ;
@@ -1293,6 +1325,28 @@ let sessionAffinityWriteVersion = 0;
12931325 let successAccountForResponse = account ;
12941326 let successEntitlementAccountKey = entitlementAccountKey ;
12951327 while ( true ) {
1328+ if (
1329+ ! tryConsumeOutboundRequestAttempt (
1330+ "primary" ,
1331+ account . index ,
1332+ )
1333+ ) {
1334+ runtimeMetrics . failedRequests ++ ;
1335+ const lastErrorDetail = runtimeMetrics . lastError ;
1336+ const message = lastErrorDetail
1337+ ? `${ lastErrorDetail } . Try again after the current retries settle.`
1338+ : "Request attempt budget exhausted. Try again shortly." ;
1339+ return new Response (
1340+ JSON . stringify ( { error : { message } } ) ,
1341+ {
1342+ status : 503 ,
1343+ headers : {
1344+ "content-type" : "application/json; charset=utf-8" ,
1345+ } ,
1346+ } ,
1347+ ) ;
1348+ }
1349+
12961350 let response : Response ;
12971351 const fetchStart = performance . now ( ) ;
12981352
@@ -2002,13 +2056,13 @@ let sessionAffinityWriteVersion = 0;
20022056 runtimeMetrics . cumulativeLatencyMs += fetchLatencyMs ;
20032057 let responseForSuccess = response ;
20042058 if ( isStreaming ) {
2005- const streamFallbackCandidateOrder = [
2006- account . index ,
2007- ... accountManager
2008- . getAccountsSnapshot ( )
2009- . map ( ( candidate ) => candidate . index )
2010- . filter ( ( index ) => index !== account . index ) ,
2011- ] ;
2059+ const streamFallbackCandidateOrder =
2060+ buildStreamFailoverCandidateOrder (
2061+ account . index ,
2062+ accountSnapshotList . map (
2063+ ( candidate ) => candidate . index ,
2064+ ) ,
2065+ ) ;
20122066 responseForSuccess = withStreamingFailover (
20132067 response ,
20142068 async ( failoverAttempt , emittedBytes ) => {
@@ -2125,6 +2179,14 @@ let sessionAffinityWriteVersion = 0;
21252179 ) {
21262180 continue ;
21272181 }
2182+ if (
2183+ ! tryConsumeOutboundRequestAttempt (
2184+ "stream-failover" ,
2185+ fallbackAccount . index ,
2186+ )
2187+ ) {
2188+ return null ;
2189+ }
21282190 fallbackAccount . accountId = fallbackAccountId ;
21292191 if (
21302192 ! hadFallbackAccountId &&
0 commit comments