@@ -34,6 +34,7 @@ import {
3434 loadRuntimePolicyState ,
3535 type RuntimePolicyDecision ,
3636} from "./policy/runtime-policy.js" ;
37+ import { isWorkspaceDisabledError } from "./request/fetch-helpers.js" ;
3738import { SessionAffinityStore } from "./session-affinity.js" ;
3839import type { OAuthAuthDetails , RequestBody , TokenResult } from "./types.js" ;
3940import { isRecord } from "./utils.js" ;
@@ -91,6 +92,7 @@ type ExhaustionReason =
9192 | "network-error"
9293 | "auth-failure"
9394 | "budget"
95+ | "deactivated"
9496 | "no-account" ;
9597type RuntimeProxyHttpError = Error & {
9698 statusCode : number ;
@@ -637,6 +639,26 @@ async function readErrorBody(response: Response): Promise<string> {
637639 }
638640}
639641
642+ function extractErrorCodeFromBody ( bodyText : string ) : string | null {
643+ if ( ! bodyText . trim ( ) ) return null ;
644+ try {
645+ const parsed = JSON . parse ( bodyText ) as unknown ;
646+ if ( ! isRecord ( parsed ) ) return null ;
647+ const directCode = parsed . code ;
648+ if ( typeof directCode === "string" && directCode . trim ( ) ) {
649+ return directCode . trim ( ) ;
650+ }
651+ const maybeError = parsed . error ;
652+ if ( ! isRecord ( maybeError ) ) return null ;
653+ const nestedCode = maybeError . code ;
654+ return typeof nestedCode === "string" && nestedCode . trim ( )
655+ ? nestedCode . trim ( )
656+ : null ;
657+ } catch {
658+ return null ;
659+ }
660+ }
661+
640662function getQuotaWindowWaitMs ( headers : Headers , prefix : string , now : number ) : number {
641663 const resetAfterSeconds = Number . parseInt (
642664 headers . get ( `${ prefix } -reset-after-seconds` ) ?? "" ,
@@ -1013,12 +1035,18 @@ export async function startRuntimeRotationProxy(
10131035 ) ;
10141036 const attemptedIndexes = new Set < number > ( ) ;
10151037 let exhaustionReason : ExhaustionReason = "no-account" ;
1016- const accountAttemptLimit = Math . max (
1038+ const accountCount = accountManager . getAccountCount ( ) ;
1039+ const transientAttemptLimit = Math . max (
10171040 1 ,
1018- Math . min ( accountManager . getAccountCount ( ) , maxRuntimeAccountAttempts ) ,
1041+ Math . min ( accountCount , maxRuntimeAccountAttempts ) ,
10191042 ) ;
1043+ let transientAttempts = 0 ;
1044+ let transientExhaustionReason : ExhaustionReason | null = null ;
10201045
1021- while ( attemptedIndexes . size < accountAttemptLimit ) {
1046+ while (
1047+ attemptedIndexes . size < accountCount &&
1048+ transientAttempts < transientAttemptLimit
1049+ ) {
10221050 const selected = chooseAccount ( {
10231051 accountManager,
10241052 sessionAffinityStore,
@@ -1049,6 +1077,8 @@ export async function startRuntimeRotationProxy(
10491077 accountManager . refundToken ( selected , context . family , context . model ) ;
10501078 exhaustionReason = "auth-failure" ;
10511079 if ( ! refreshed . retryable ) continue ;
1080+ transientAttempts += 1 ;
1081+ transientExhaustionReason = "auth-failure" ;
10521082 status . retries += 1 ;
10531083 status . rotations += 1 ;
10541084 continue ;
@@ -1064,6 +1094,8 @@ export async function startRuntimeRotationProxy(
10641094 "auth-failure" ,
10651095 ) ;
10661096 exhaustionReason = "auth-failure" ;
1097+ transientAttempts += 1 ;
1098+ transientExhaustionReason = "auth-failure" ;
10671099 status . retries += 1 ;
10681100 status . rotations += 1 ;
10691101 continue ;
@@ -1108,6 +1140,8 @@ export async function startRuntimeRotationProxy(
11081140 ) ;
11091141 accountManager . saveToDiskDebounced ( ) ;
11101142 exhaustionReason = "network-error" ;
1143+ transientAttempts += 1 ;
1144+ transientExhaustionReason = "network-error" ;
11111145 status . retries += 1 ;
11121146 status . rotations += 1 ;
11131147 continue ;
@@ -1131,11 +1165,52 @@ export async function startRuntimeRotationProxy(
11311165 ) ;
11321166 accountManager . saveToDiskDebounced ( ) ;
11331167 exhaustionReason = "rate-limit" ;
1168+ transientAttempts += 1 ;
1169+ transientExhaustionReason = "rate-limit" ;
11341170 status . retries += 1 ;
11351171 status . rotations += 1 ;
11361172 continue ;
11371173 }
11381174
1175+ if ( upstream . status === 402 || upstream . status === HTTP_STATUS . FORBIDDEN ) {
1176+ const bodyText = await readErrorBody ( upstream ) ;
1177+ const errorCode = extractErrorCodeFromBody ( bodyText ) ;
1178+ if ( isWorkspaceDisabledError ( upstream . status , errorCode , bodyText ) ) {
1179+ const accountWasEnabled =
1180+ accountManager . getAccountByIndex ( refreshed . account . index ) ?. enabled !==
1181+ false ;
1182+ accountManager . refundToken (
1183+ refreshed . account ,
1184+ context . family ,
1185+ context . model ,
1186+ ) ;
1187+ if ( accountWasEnabled ) {
1188+ accountManager . recordFailure (
1189+ refreshed . account ,
1190+ context . family ,
1191+ context . model ,
1192+ ) ;
1193+ accountManager . setAccountEnabled ( refreshed . account . index , false ) ;
1194+ accountManager . saveToDiskDebounced ( ) ;
1195+ }
1196+ sessionAffinityStore ?. forgetSession ( context . sessionKey ) ;
1197+ exhaustionReason = "deactivated" ;
1198+ status . retries += 1 ;
1199+ status . rotations += 1 ;
1200+ continue ;
1201+ }
1202+
1203+ res . writeHead ( upstream . status , responseHeadersForClient ( upstream . headers ) ) ;
1204+ res . end ( bodyText ) ;
1205+ await usageRecorder . record ( {
1206+ outcome : "failure" ,
1207+ statusCode : upstream . status ,
1208+ errorCode,
1209+ account : refreshed . account ,
1210+ } ) ;
1211+ return ;
1212+ }
1213+
11391214 if ( upstream . status === HTTP_STATUS . UNAUTHORIZED ) {
11401215 await readErrorBody ( upstream ) ;
11411216 accountManager . refundToken ( refreshed . account , context . family , context . model ) ;
@@ -1147,6 +1222,8 @@ export async function startRuntimeRotationProxy(
11471222 ) ;
11481223 accountManager . saveToDiskDebounced ( ) ;
11491224 exhaustionReason = "auth-failure" ;
1225+ transientAttempts += 1 ;
1226+ transientExhaustionReason = "auth-failure" ;
11501227 status . retries += 1 ;
11511228 status . rotations += 1 ;
11521229 continue ;
@@ -1163,6 +1240,8 @@ export async function startRuntimeRotationProxy(
11631240 ) ;
11641241 accountManager . saveToDiskDebounced ( ) ;
11651242 exhaustionReason = "server-error" ;
1243+ transientAttempts += 1 ;
1244+ transientExhaustionReason = "server-error" ;
11661245 status . retries += 1 ;
11671246 status . rotations += 1 ;
11681247 continue ;
@@ -1283,10 +1362,15 @@ export async function startRuntimeRotationProxy(
12831362 }
12841363
12851364 if (
1286- attemptedIndexes . size >= accountAttemptLimit &&
1287- accountAttemptLimit < accountManager . getAccountCount ( )
1365+ transientAttempts >= transientAttemptLimit &&
1366+ attemptedIndexes . size < accountCount
12881367 ) {
12891368 exhaustionReason = "budget" ;
1369+ } else if (
1370+ exhaustionReason === "deactivated" &&
1371+ transientExhaustionReason
1372+ ) {
1373+ exhaustionReason = transientExhaustionReason ;
12901374 }
12911375
12921376 await usageRecorder ?. record ( {
0 commit comments