@@ -228,6 +228,140 @@ async function shouldRetryCodexPoolAccountModel400(
228228 }
229229}
230230
231+ /** Pre-stream quota/billing rejections that warrant one alternate-account attempt (#584). */
232+ function shouldRetryCodexPoolAccountQuota ( response : Response ) : boolean {
233+ return response . status === 429 || response . status === 402 ;
234+ }
235+
236+ interface CodexPoolAccountRetryArgs {
237+ req : Request ;
238+ config : OcxConfig ;
239+ route : { providerName : string ; modelId : string ; provider : OcxProviderConfig } ;
240+ parsed : OcxParsedRequest ;
241+ logCtx : RequestLogContext ;
242+ options : {
243+ abortSignal ?: AbortSignal ;
244+ onCodexAuthContextResolved ?: ( ctx : CodexAuthContext ) => void ;
245+ } ;
246+ firstAuthCtx : Extract < CodexAuthContext , { kind : "pool" | "main-pool" } > ;
247+ firstResponse : Response ;
248+ outcomeStatus : number ;
249+ upstream : AbortController ;
250+ connectMs : number ;
251+ passthroughEstimate ?: number ;
252+ stream : boolean ;
253+ }
254+
255+ type CodexPoolAccountRetryResult =
256+ | {
257+ kind : "retried" ;
258+ authCtx : Extract < CodexAuthContext , { kind : "pool" | "main-pool" } > ;
259+ request : Awaited < ReturnType < ReturnType < typeof resolveAdapter > [ "buildRequest" ] > > ;
260+ upstreamResponse : Response ;
261+ selectedForwardHeaders : Headers ;
262+ }
263+ | { kind : "no-alternate" }
264+ | {
265+ kind : "transport" ;
266+ error : unknown ;
267+ authCtx : Extract < CodexAuthContext , { kind : "pool" | "main-pool" } > ;
268+ } ;
269+
270+ /**
271+ * One bounded alternate-account retry for Codex pool auth. Used for allow-listed
272+ * model-400 and for pre-stream 429/402 quota failures (#584).
273+ */
274+ async function retryCodexPoolOnAlternateAccount (
275+ args : CodexPoolAccountRetryArgs ,
276+ ) : Promise < CodexPoolAccountRetryResult > {
277+ const {
278+ req, config, route, parsed, logCtx, options, firstAuthCtx, firstResponse,
279+ outcomeStatus, upstream, connectMs, passthroughEstimate, stream,
280+ } = args ;
281+ let retryAuthCtx : CodexAuthContext | undefined ;
282+ try {
283+ retryAuthCtx = await resolveCodexAuthContext (
284+ req . headers ,
285+ config ,
286+ "pool" ,
287+ { excludeAccountId : firstAuthCtx . accountId } ,
288+ ) ;
289+ } catch ( error ) {
290+ if (
291+ ! ( error instanceof CodexPoolAuthenticationError )
292+ && ! ( error instanceof CodexAuthContextError )
293+ && ! ( error instanceof CodexAccountCooldownError )
294+ ) throw error ;
295+ }
296+ if ( retryAuthCtx ?. kind !== "pool" && retryAuthCtx ?. kind !== "main-pool" ) {
297+ return { kind : "no-alternate" } ;
298+ }
299+
300+ const retryAfterRaw = firstResponse . headers . get ( "retry-after" ) ;
301+ if ( outcomeStatus === 429 || outcomeStatus === 402 ) {
302+ const { applyAccountQuotaFromUpstreamHeaders } = await import ( "../../codex/auth-api" ) ;
303+ applyAccountQuotaFromUpstreamHeaders ( firstAuthCtx . accountId , firstResponse . headers ) ;
304+ }
305+ recordCodexUpstreamOutcome ( config , firstAuthCtx . accountId , outcomeStatus , {
306+ retryAfter : retryAfterRaw ,
307+ resetAt : [
308+ firstResponse . headers . get ( "x-codex-primary-reset-at" ) ,
309+ firstResponse . headers . get ( "x-codex-secondary-reset-at" ) ,
310+ firstResponse . headers . get ( "x-codex-tertiary-reset-at" ) ,
311+ ] . filter ( Boolean ) ,
312+ threadId : req . headers . get ( "x-codex-parent-thread-id" ) ,
313+ probeLeaseId : codexProbeLeaseId ( firstAuthCtx ) ,
314+ } ) ;
315+
316+ const retryHeaders = headersForCodexAuthContext ( req . headers , retryAuthCtx ) ;
317+ const retryProvider = applyCodexAuthContextToProvider (
318+ stripCodexRuntimeProviderFields ( route . provider ) ,
319+ retryAuthCtx ,
320+ "pool" ,
321+ ) ;
322+ const retryAdapter = resolveAdapter (
323+ resolveWireProtocolOverride ( route . providerName , route . modelId , retryProvider ) ,
324+ config . cacheRetention ,
325+ ) ;
326+ const request = await retryAdapter . buildRequest ( parsed , { headers : retryHeaders } ) ;
327+ recordAdapterReasoning ( logCtx , request ) ;
328+
329+ await firstResponse . body ?. cancel ( ) . catch ( ( ) => undefined ) ;
330+ options . onCodexAuthContextResolved ?.( retryAuthCtx ) ;
331+ route . provider = retryProvider ;
332+ logCtx . provider = formatCodexProviderForLog (
333+ route . providerName ,
334+ retryAuthCtx . accountId ,
335+ config ,
336+ ) ;
337+
338+ noteAttemptSend ( logCtx . activeAttempt , passthroughEstimate ) ;
339+ try {
340+ const upstreamResponse = await fetchWithHeaderTimeout (
341+ request . url ,
342+ {
343+ method : request . method ,
344+ headers : request . headers ,
345+ body : request . body ,
346+ } ,
347+ upstream . signal ,
348+ connectMs ,
349+ stream ,
350+ providerFetch ( route . provider ) ,
351+ ) ;
352+ return {
353+ kind : "retried" ,
354+ authCtx : retryAuthCtx ,
355+ request,
356+ upstreamResponse,
357+ selectedForwardHeaders : retryHeaders ,
358+ } ;
359+ } catch ( error ) {
360+ // Attribute the transport failure to the alternate account (already selected).
361+ return { kind : "transport" , error, authCtx : retryAuthCtx } ;
362+ }
363+ }
364+
231365
232366
233367export function codexForwardTerminalOutcomeRecorder (
@@ -1273,77 +1407,46 @@ export async function handleResponses(
12731407 return transportFailureResponse ( err ) ;
12741408 }
12751409
1276- if (
1277- usesCodexForwardPoolAuth ( authCtx , route . provider )
1278- && await shouldRetryCodexPoolAccountModel400 (
1410+ if ( usesCodexForwardPoolAuth ( authCtx , route . provider ) ) {
1411+ let poolRetryOutcome : number | undefined ;
1412+ if ( await shouldRetryCodexPoolAccountModel400 (
12791413 upstreamResponse ,
12801414 route . modelId ,
12811415 options . abortSignal ,
1282- )
1283- ) {
1284- const firstAuthCtx = authCtx ;
1285- let retryAuthCtx : CodexAuthContext | undefined ;
1286- try {
1287- retryAuthCtx = await resolveCodexAuthContext (
1288- req . headers ,
1289- config ,
1290- "pool" ,
1291- { excludeAccountId : firstAuthCtx . accountId } ,
1292- ) ;
1293- } catch ( error ) {
1294- if (
1295- ! ( error instanceof CodexPoolAuthenticationError )
1296- && ! ( error instanceof CodexAuthContextError )
1297- && ! ( error instanceof CodexAccountCooldownError )
1298- ) throw error ;
1416+ ) ) {
1417+ poolRetryOutcome = 400 ;
1418+ } else if ( shouldRetryCodexPoolAccountQuota ( upstreamResponse ) ) {
1419+ // Pre-stream only: once SSE has begun, mid-stream quota stays terminal.
1420+ poolRetryOutcome = upstreamResponse . status ;
12991421 }
13001422
1301- if ( retryAuthCtx ?. kind === "pool" || retryAuthCtx ?. kind === "main-pool" ) {
1302- recordCodexUpstreamOutcome ( config , firstAuthCtx . accountId , 400 , {
1303- threadId : req . headers . get ( "x-codex-parent-thread-id" ) ,
1304- probeLeaseId : codexProbeLeaseId ( firstAuthCtx ) ,
1305- } ) ;
1306-
1307- const retryHeaders = headersForCodexAuthContext ( req . headers , retryAuthCtx ) ;
1308- const retryProvider = applyCodexAuthContextToProvider (
1309- stripCodexRuntimeProviderFields ( route . provider ) ,
1310- retryAuthCtx ,
1311- "pool" ,
1312- ) ;
1313- const retryAdapter = resolveAdapter (
1314- resolveWireProtocolOverride ( route . providerName , route . modelId , retryProvider ) ,
1315- config . cacheRetention ,
1316- ) ;
1317- request = await retryAdapter . buildRequest ( parsed , { headers : retryHeaders } ) ;
1318- recordAdapterReasoning ( logCtx , request ) ;
1319-
1320- await upstreamResponse . body ?. cancel ( ) . catch ( ( ) => undefined ) ;
1321- authCtx = retryAuthCtx ;
1322- options . onCodexAuthContextResolved ?.( retryAuthCtx ) ;
1323- selectedForwardHeaders = retryHeaders ;
1324- route . provider = retryProvider ;
1325- logCtx . provider = formatCodexProviderForLog (
1326- route . providerName ,
1327- retryAuthCtx . accountId ,
1423+ if ( poolRetryOutcome !== undefined ) {
1424+ const retry = await retryCodexPoolOnAlternateAccount ( {
1425+ req,
13281426 config,
1329- ) ;
1330-
1331- noteAttemptSend ( logCtx . activeAttempt , passthroughEstimate ) ;
1332- try {
1333- upstreamResponse = await fetchWithHeaderTimeout (
1334- request . url ,
1335- {
1336- method : request . method ,
1337- headers : request . headers ,
1338- body : request . body ,
1339- } ,
1340- upstream . signal ,
1341- connectMs ,
1342- parsed . stream ,
1343- providerFetch ( route . provider ) ,
1344- ) ;
1345- } catch ( err ) {
1346- return transportFailureResponse ( err ) ;
1427+ route,
1428+ parsed,
1429+ logCtx,
1430+ options,
1431+ firstAuthCtx : authCtx ,
1432+ firstResponse : upstreamResponse ,
1433+ outcomeStatus : poolRetryOutcome ,
1434+ upstream,
1435+ connectMs,
1436+ passthroughEstimate,
1437+ stream : parsed . stream ,
1438+ } ) ;
1439+ if ( retry . kind === "transport" ) {
1440+ authCtx = retry . authCtx ;
1441+ return transportFailureResponse ( retry . error ) ;
1442+ }
1443+ if ( retry . kind === "retried" ) {
1444+ authCtx = retry . authCtx ;
1445+ request = retry . request ;
1446+ upstreamResponse = retry . upstreamResponse ;
1447+ selectedForwardHeaders = retry . selectedForwardHeaders ;
1448+ // Keep subagent quota-failure health keyed to the account that actually served.
1449+ subagentFallbackAccountId = retry . authCtx . accountId ;
13471450 }
13481451 }
13491452 }
0 commit comments