codex-multi-auth 2.8.5 → 2.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/lib/accounts.d.ts +7 -0
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/accounts.js +10 -0
- package/dist/lib/codex-manager/commands/best.d.ts +3 -0
- package/dist/lib/codex-manager/commands/best.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/best.js +10 -1
- package/dist/lib/codex-manager/commands/forecast.d.ts +3 -0
- package/dist/lib/codex-manager/commands/forecast.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/forecast.js +18 -1
- package/dist/lib/codex-manager/commands/report.d.ts.map +1 -1
- package/dist/lib/codex-manager/commands/report.js +14 -0
- package/dist/lib/forecast.d.ts +13 -0
- package/dist/lib/forecast.d.ts.map +1 -1
- package/dist/lib/forecast.js +18 -3
- package/dist/lib/request/rate-limit-decision.d.ts +46 -1
- package/dist/lib/request/rate-limit-decision.d.ts.map +1 -1
- package/dist/lib/request/rate-limit-decision.js +150 -3
- package/dist/lib/runtime/account-status.d.ts +67 -0
- package/dist/lib/runtime/account-status.d.ts.map +1 -1
- package/dist/lib/runtime/account-status.js +73 -0
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +90 -2
- package/package.json +1 -1
- package/scripts/codex.js +300 -82
|
@@ -7,7 +7,74 @@ export declare function resolveActiveIndex(storage: {
|
|
|
7
7
|
export declare function getRateLimitResetTimeForFamily(account: {
|
|
8
8
|
rateLimitResetTimes?: Record<string, number | undefined>;
|
|
9
9
|
}, now: number, family: ModelFamily): number | null;
|
|
10
|
+
export interface AccountRecoveryBounds {
|
|
11
|
+
/**
|
|
12
|
+
* The moment the account becomes usable again: `rateLimitAtMs` widened by
|
|
13
|
+
* any active cooldown, because the account stays skipped while ANY gating
|
|
14
|
+
* record is active. Null when nothing bounds recovery.
|
|
15
|
+
*/
|
|
16
|
+
recoveryAtMs: number | null;
|
|
17
|
+
/**
|
|
18
|
+
* The rate-limit records' own contribution to `recoveryAtMs`, cooldowns
|
|
19
|
+
* excluded. The pinned-503 uses it to decide whether the deadline it
|
|
20
|
+
* advertises is in fact the rate limit's own reset — with a breaker or
|
|
21
|
+
* cooldown ending later, the full recovery bound outlives the rate limit
|
|
22
|
+
* and must not be worded as its reset. Null when no rate-limit record
|
|
23
|
+
* gates the request.
|
|
24
|
+
*/
|
|
25
|
+
rateLimitAtMs: number | null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Both recovery bounds for a `family`/`model` request, from ONE pass over
|
|
29
|
+
* `rateLimitResetTimes` against a single `now`.
|
|
30
|
+
*
|
|
31
|
+
* Two deliberate differences from getRateLimitResetTimeForFamily, whose
|
|
32
|
+
* earliest-reset answer feeds wait displays: the latest bound wins, because
|
|
33
|
+
* the earliest reset would send clients back into a 503 — and only the keys
|
|
34
|
+
* selection consults (`family`, plus `family:<model>` when a model is known;
|
|
35
|
+
* see isRateLimitedForFamily) may contribute, because another model's record
|
|
36
|
+
* does not block this request and would overstate its recovery.
|
|
37
|
+
*
|
|
38
|
+
* A caller that needs both bounds must take them from one call. Measuring
|
|
39
|
+
* them separately lets a record expire between the two walks, which reports
|
|
40
|
+
* a rate-limited account as if a cooldown bounded its recovery.
|
|
41
|
+
*/
|
|
42
|
+
export declare function getAccountRecoveryBoundsForFamily(account: {
|
|
43
|
+
rateLimitResetTimes?: Record<string, number | undefined>;
|
|
44
|
+
coolingDownUntil?: number;
|
|
45
|
+
}, now: number, family: ModelFamily, model?: string | null): AccountRecoveryBounds;
|
|
46
|
+
/** The `recoveryAtMs` bound alone; see getAccountRecoveryBoundsForFamily. */
|
|
47
|
+
export declare function getAccountRecoveryTimeForFamily(account: {
|
|
48
|
+
rateLimitResetTimes?: Record<string, number | undefined>;
|
|
49
|
+
coolingDownUntil?: number;
|
|
50
|
+
}, now: number, family: ModelFamily, model?: string | null): number | null;
|
|
51
|
+
/** The `rateLimitAtMs` bound alone; see getAccountRecoveryBoundsForFamily. */
|
|
52
|
+
export declare function getRateLimitRecoveryTimeForFamily(account: {
|
|
53
|
+
rateLimitResetTimes?: Record<string, number | undefined>;
|
|
54
|
+
}, now: number, family: ModelFamily, model?: string | null): number | null;
|
|
10
55
|
export declare function formatRateLimitEntry(account: {
|
|
11
56
|
rateLimitResetTimes?: Record<string, number | undefined>;
|
|
12
57
|
}, now: number, formatWaitTime: (ms: number) => string, family?: ModelFamily): string | null;
|
|
58
|
+
/**
|
|
59
|
+
* When a request for `family`/`model` stops being rate limited: the LATEST
|
|
60
|
+
* active bound among exactly the two keys selection consults — the family-wide
|
|
61
|
+
* key and `family:<model>` (see `isRateLimitedForFamily`). Null when neither is
|
|
62
|
+
* active.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately narrower and later than `getRateLimitResetTimeForFamily`, whose
|
|
65
|
+
* earliest-reset-across-every-`family:*`-key answer feeds wait displays and
|
|
66
|
+
* model-less callers:
|
|
67
|
+
*
|
|
68
|
+
* - narrower, because `markRateLimitedWithReason` keys token/concurrency limits
|
|
69
|
+
* under `family:<model>`, and a sibling model's record does not gate this
|
|
70
|
+
* request — folding it in reports a delay the runtime proxy would not impose;
|
|
71
|
+
* - later, because the account stays skipped while EITHER key is active, so the
|
|
72
|
+
* earliest reset understates the wait when both are set.
|
|
73
|
+
*
|
|
74
|
+
* Requires a model by construction: a caller without one cannot know which
|
|
75
|
+
* model key applies and should keep the family-wide union above.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getRateLimitResetTimeForModel(account: {
|
|
78
|
+
rateLimitResetTimes?: Record<string, number | undefined>;
|
|
79
|
+
}, now: number, family: ModelFamily, model: string): number | null;
|
|
13
80
|
//# sourceMappingURL=account-status.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-status.d.ts","sourceRoot":"","sources":["../../../lib/runtime/account-status.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"account-status.d.ts","sourceRoot":"","sources":["../../../lib/runtime/account-status.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,wBAAgB,kBAAkB,CACjC,OAAO,EAAE;IACR,WAAW,EAAE,MAAM,CAAC;IACpB,mBAAmB,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3D,QAAQ,EAAE,OAAO,EAAE,CAAC;CACpB,EACD,MAAM,GAAE,WAAqB,GAC3B,MAAM,CAOR;AAED,wBAAgB,8BAA8B,CAC7C,OAAO,EAAE;IAAE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,EACrE,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,GACjB,MAAM,GAAG,IAAI,CAgBf;AAeD,MAAM,WAAW,qBAAqB;IACrC;;;;OAIG;IACH,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;;;;OAOG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iCAAiC,CAChD,OAAO,EAAE;IACR,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B,EACD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,qBAAqB,CAevB;AAED,6EAA6E;AAC7E,wBAAgB,+BAA+B,CAC9C,OAAO,EAAE;IACR,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B,EACD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,GAAG,IAAI,CAGf;AAED,8EAA8E;AAC9E,wBAAgB,iCAAiC,CAChD,OAAO,EAAE;IACR,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CACzD,EACD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,GAAG,IAAI,CAGf;AAED,wBAAgB,oBAAoB,CACnC,OAAO,EAAE;IAAE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,EACrE,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,EACtC,MAAM,GAAE,WAAqB,GAC3B,MAAM,GAAG,IAAI,CAMf;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAC5C,OAAO,EAAE;IAAE,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,EACrE,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,GACX,MAAM,GAAG,IAAI,CAGf"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getQuotaKey } from "../accounts/rate-limits.js";
|
|
1
2
|
export function resolveActiveIndex(storage, family = "codex") {
|
|
2
3
|
const total = storage.accounts.length;
|
|
3
4
|
if (total === 0)
|
|
@@ -25,6 +26,55 @@ export function getRateLimitResetTimeForFamily(account, now, family) {
|
|
|
25
26
|
}
|
|
26
27
|
return minReset;
|
|
27
28
|
}
|
|
29
|
+
/** The value when it is still a live bound at `now`, otherwise null. */
|
|
30
|
+
function activeBound(value, now) {
|
|
31
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
32
|
+
return null;
|
|
33
|
+
return value > now ? value : null;
|
|
34
|
+
}
|
|
35
|
+
/** The later of two bounds, treating null as "does not bound". */
|
|
36
|
+
function laterBound(a, b) {
|
|
37
|
+
if (a === null)
|
|
38
|
+
return b;
|
|
39
|
+
if (b === null)
|
|
40
|
+
return a;
|
|
41
|
+
return a > b ? a : b;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Both recovery bounds for a `family`/`model` request, from ONE pass over
|
|
45
|
+
* `rateLimitResetTimes` against a single `now`.
|
|
46
|
+
*
|
|
47
|
+
* Two deliberate differences from getRateLimitResetTimeForFamily, whose
|
|
48
|
+
* earliest-reset answer feeds wait displays: the latest bound wins, because
|
|
49
|
+
* the earliest reset would send clients back into a 503 — and only the keys
|
|
50
|
+
* selection consults (`family`, plus `family:<model>` when a model is known;
|
|
51
|
+
* see isRateLimitedForFamily) may contribute, because another model's record
|
|
52
|
+
* does not block this request and would overstate its recovery.
|
|
53
|
+
*
|
|
54
|
+
* A caller that needs both bounds must take them from one call. Measuring
|
|
55
|
+
* them separately lets a record expire between the two walks, which reports
|
|
56
|
+
* a rate-limited account as if a cooldown bounded its recovery.
|
|
57
|
+
*/
|
|
58
|
+
export function getAccountRecoveryBoundsForFamily(account, now, family, model) {
|
|
59
|
+
const times = account.rateLimitResetTimes;
|
|
60
|
+
const rateLimitAtMs = times
|
|
61
|
+
? laterBound(activeBound(times[getQuotaKey(family)], now), model ? activeBound(times[getQuotaKey(family, model)], now) : null)
|
|
62
|
+
: null;
|
|
63
|
+
return {
|
|
64
|
+
rateLimitAtMs,
|
|
65
|
+
recoveryAtMs: laterBound(rateLimitAtMs, activeBound(account.coolingDownUntil, now)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** The `recoveryAtMs` bound alone; see getAccountRecoveryBoundsForFamily. */
|
|
69
|
+
export function getAccountRecoveryTimeForFamily(account, now, family, model) {
|
|
70
|
+
return getAccountRecoveryBoundsForFamily(account, now, family, model)
|
|
71
|
+
.recoveryAtMs;
|
|
72
|
+
}
|
|
73
|
+
/** The `rateLimitAtMs` bound alone; see getAccountRecoveryBoundsForFamily. */
|
|
74
|
+
export function getRateLimitRecoveryTimeForFamily(account, now, family, model) {
|
|
75
|
+
return getAccountRecoveryBoundsForFamily(account, now, family, model)
|
|
76
|
+
.rateLimitAtMs;
|
|
77
|
+
}
|
|
28
78
|
export function formatRateLimitEntry(account, now, formatWaitTime, family = "codex") {
|
|
29
79
|
const resetAt = getRateLimitResetTimeForFamily(account, now, family);
|
|
30
80
|
if (typeof resetAt !== "number")
|
|
@@ -34,3 +84,26 @@ export function formatRateLimitEntry(account, now, formatWaitTime, family = "cod
|
|
|
34
84
|
return null;
|
|
35
85
|
return `resets in ${formatWaitTime(remaining)}`;
|
|
36
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* When a request for `family`/`model` stops being rate limited: the LATEST
|
|
89
|
+
* active bound among exactly the two keys selection consults — the family-wide
|
|
90
|
+
* key and `family:<model>` (see `isRateLimitedForFamily`). Null when neither is
|
|
91
|
+
* active.
|
|
92
|
+
*
|
|
93
|
+
* Deliberately narrower and later than `getRateLimitResetTimeForFamily`, whose
|
|
94
|
+
* earliest-reset-across-every-`family:*`-key answer feeds wait displays and
|
|
95
|
+
* model-less callers:
|
|
96
|
+
*
|
|
97
|
+
* - narrower, because `markRateLimitedWithReason` keys token/concurrency limits
|
|
98
|
+
* under `family:<model>`, and a sibling model's record does not gate this
|
|
99
|
+
* request — folding it in reports a delay the runtime proxy would not impose;
|
|
100
|
+
* - later, because the account stays skipped while EITHER key is active, so the
|
|
101
|
+
* earliest reset understates the wait when both are set.
|
|
102
|
+
*
|
|
103
|
+
* Requires a model by construction: a caller without one cannot know which
|
|
104
|
+
* model key applies and should keep the family-wide union above.
|
|
105
|
+
*/
|
|
106
|
+
export function getRateLimitResetTimeForModel(account, now, family, model) {
|
|
107
|
+
return getAccountRecoveryBoundsForFamily(account, now, family, model)
|
|
108
|
+
.rateLimitAtMs;
|
|
109
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-rotation-proxy.d.ts","sourceRoot":"","sources":["../../lib/runtime-rotation-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"runtime-rotation-proxy.d.ts","sourceRoot":"","sources":["../../lib/runtime-rotation-proxy.ts"],"names":[],"mappings":"AAGA,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,eAAe,CAAC;AA8BvB,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA4CtE,OAAO,KAAK,EAKX,2BAA2B,EAC3B,0BAA0B,EAE1B,MAAM,oCAAoC,CAAC;AAe5C,YAAY,EACX,2BAA2B,EAC3B,0BAA0B,EAC1B,0BAA0B,GAC1B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EACN,+BAA+B,EAC/B,0BAA0B,GAC1B,MAAM,kCAAkC,CAAC;AAC1C,YAAY,EAAE,0BAA0B,EAAE,MAAM,kCAAkC,CAAC;AACnF,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AACxE,OAAO,EACN,+BAA+B,EAC/B,8BAA8B,EAC9B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,YAAY,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAC;AA8DtE,kFAAkF;AAClF,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,OAAO,GAAG,cAAc,CAAC,GAAG;IACvE,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,EAAE,WAAW,EACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACnB,MAAM,CAmBR;AA8bD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAC1C,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GACvC,MAAM,GAAG,IAAI,CAUf;AAED,wBAAsB,yBAAyB,CAC9C,OAAO,EAAE,2BAA2B,GAClC,OAAO,CAAC,0BAA0B,CAAC,CA4KrC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
|
-
import { AccountManager, extractAccountId, } from "./accounts.js";
|
|
3
|
+
import { AccountManager, AUTH_INVALIDATION_MARKER, extractAccountId, } from "./accounts.js";
|
|
4
4
|
import { withRoutingMutex } from "./routing-mutex.js";
|
|
5
5
|
import { getFetchTimeoutMs, getNetworkErrorCooldownMs, getRetryAllAccountsMaxRetries, getServerErrorCooldownMs, getSessionAffinity, getSessionAffinityMaxEntries, getSessionAffinityTtlMs, getStreamStallTimeoutMs, getMinRotationIntervalMs, getTokenInvalidationCooldownMs, getTokenRefreshSkewMs, getPidOffsetEnabled, getPreemptiveQuotaEnabled, getPreemptiveQuotaMaxDeferralMs, getPreemptiveQuotaRemainingPercent5h, getPreemptiveQuotaRemainingPercent7d, getRoutingMutexMode, getSchedulingStrategy, loadPluginConfig, } from "./config.js";
|
|
6
6
|
import { CODEX_BASE_URL, HTTP_STATUS, OPENAI_HEADERS, OPENAI_HEADER_VALUES, URL_PATHS, } from "./constants.js";
|
|
@@ -15,6 +15,7 @@ import { CodexValidationError } from "./errors.js";
|
|
|
15
15
|
import { normalizeEmailKey } from "./storage/identity.js";
|
|
16
16
|
import { buildPinnedUnavailableErrorBody, buildTokenInvalidationBody, extractErrorCodeFromBody, isTokenInvalidationError, normalizeExhaustionStatus, parseRetryAfterBodyMs, parseRetryAfterHeaderMs, } from "./request/rate-limit-decision.js";
|
|
17
17
|
import { forwardStreamingResponse, HOP_BY_HOP_HEADERS, readErrorBody, responseHeadersForClient, withTimeout, } from "./request/stream-failover-runtime.js";
|
|
18
|
+
import { getAccountRecoveryBoundsForFamily } from "./runtime/account-status.js";
|
|
18
19
|
import { chooseAccount } from "./runtime/rotation-account-selection.js";
|
|
19
20
|
import { createRotationProxyState, recoverStaleRuntimeState, } from "./runtime/rotation-proxy-state.js";
|
|
20
21
|
import { readStorageMetaFromDisk } from "./runtime/rotation-storage-meta.js";
|
|
@@ -62,6 +63,18 @@ function toUrlHost(host) {
|
|
|
62
63
|
// failures surfaced only as a last-write-wins status.lastError string. Logs are
|
|
63
64
|
// level-gated and carry the per-request correlation id set in handleRequest.
|
|
64
65
|
const proxyLog = createLogger("runtime-proxy");
|
|
66
|
+
/**
|
|
67
|
+
* Pinned skip reasons that no timer clears: the account stays unselectable
|
|
68
|
+
* after any concurrent rate-limit record or cooldown expires, so the pinned
|
|
69
|
+
* 503 must not advertise that record's expiry as a recovery time.
|
|
70
|
+
*/
|
|
71
|
+
const PINNED_PERMANENT_SKIP_REASONS = new Set([
|
|
72
|
+
"missing",
|
|
73
|
+
"disabled",
|
|
74
|
+
"workspace-disabled",
|
|
75
|
+
"policy-blocked",
|
|
76
|
+
AUTH_INVALIDATION_MARKER,
|
|
77
|
+
]);
|
|
65
78
|
const DEFAULT_QUOTA_REMAINING_THRESHOLD = 10;
|
|
66
79
|
/** @internal Stable identity key for in-memory quota snapshots across reloads. */
|
|
67
80
|
export function buildQuotaScheduleKey(account, family, model) {
|
|
@@ -1170,7 +1183,82 @@ async function handleRequestInner(state, req, res, traceId) {
|
|
|
1170
1183
|
// null reason indicates a forecast/runtime state desync (the pinned
|
|
1171
1184
|
// account was selected but no skip reason was recorded) — see #486.
|
|
1172
1185
|
if (isPinned) {
|
|
1173
|
-
const
|
|
1186
|
+
const pinnedAccount = typeof pinnedIndex === "number"
|
|
1187
|
+
? accountManager.getAccountByIndex(pinnedIndex)
|
|
1188
|
+
: null;
|
|
1189
|
+
const pinnedSkipReason = typeof pinnedIndex === "number"
|
|
1190
|
+
? accountSkipReasons.get(pinnedIndex) ?? null
|
|
1191
|
+
: null;
|
|
1192
|
+
// A permanent blocker (disabled, no enabled workspace, invalidated
|
|
1193
|
+
// auth, policy block, out-of-range pin) outlives every timed record,
|
|
1194
|
+
// so advertising a record's expiry would invite a retry into another
|
|
1195
|
+
// 503.
|
|
1196
|
+
//
|
|
1197
|
+
// The recorded reason alone is not enough to detect one. It is the
|
|
1198
|
+
// SELECTION verdict, and this request can make the pin permanently
|
|
1199
|
+
// unselectable after selection already ran: a workspace-disabled
|
|
1200
|
+
// 402/403 calls setAccountEnabled(index, false) above and then
|
|
1201
|
+
// continues, so the next pass records "already-attempted" and the
|
|
1202
|
+
// disable never surfaces. With a breaker tripped by the same failure
|
|
1203
|
+
// the 503 would then advertise the circuit's ~30s reset for an
|
|
1204
|
+
// account no timer will ever re-admit. Re-read the pin's CURRENT
|
|
1205
|
+
// runtime state so a permanent blocker cannot hide behind the
|
|
1206
|
+
// verdict; the recorded reason still covers the selection-only
|
|
1207
|
+
// verdicts ("missing", "policy-blocked") that state cannot express.
|
|
1208
|
+
const pinnedCurrentSkipReason = pinnedAccount === null
|
|
1209
|
+
? null
|
|
1210
|
+
: accountManager.getManagedAccountRuntimeSkipReason(pinnedAccount, context.family, context.model);
|
|
1211
|
+
const pinnedBlockedPermanently = (pinnedSkipReason !== null &&
|
|
1212
|
+
PINNED_PERMANENT_SKIP_REASONS.has(pinnedSkipReason)) ||
|
|
1213
|
+
(pinnedCurrentSkipReason !== null &&
|
|
1214
|
+
PINNED_PERMANENT_SKIP_REASONS.has(pinnedCurrentSkipReason));
|
|
1215
|
+
// Otherwise recovery comes from the pinned account's persisted state, not the
|
|
1216
|
+
// recorded skip reason: a direct 429/cooldown on the pinned account
|
|
1217
|
+
// reaches this 503 as "already-attempted" (the retry loop's selection
|
|
1218
|
+
// verdict) while the record it just wrote is what actually bounds
|
|
1219
|
+
// recovery — and with several overlapping records the account stays
|
|
1220
|
+
// skipped until the LAST one expires, so the latest bound is the one
|
|
1221
|
+
// worth advertising.
|
|
1222
|
+
//
|
|
1223
|
+
// One clock for the whole body. The recovery bound and the
|
|
1224
|
+
// rate-limit bound are compared against each other below, so
|
|
1225
|
+
// reading state.now() per lookup would let a record expire between
|
|
1226
|
+
// them and report a rate-limited pin as bounded by something else.
|
|
1227
|
+
const evaluatedAtMs = state.now();
|
|
1228
|
+
// Both bounds come from a single pass over the account's records.
|
|
1229
|
+
const pinnedRecoveryBounds = pinnedAccount === null
|
|
1230
|
+
? null
|
|
1231
|
+
: getAccountRecoveryBoundsForFamily(pinnedAccount, evaluatedAtMs, context.family, context.model);
|
|
1232
|
+
const pinnedStateRecoveryAtMs = pinnedRecoveryBounds?.recoveryAtMs ?? null;
|
|
1233
|
+
// An open circuit outlives the short failure cooldowns that tripped
|
|
1234
|
+
// it; its deadline lives in the breaker, not the account record.
|
|
1235
|
+
const pinnedCircuitRecoveryAtMs = pinnedAccount === null
|
|
1236
|
+
? null
|
|
1237
|
+
: accountManager.getCircuitRecoveryTime(pinnedAccount, evaluatedAtMs);
|
|
1238
|
+
const pinnedResetAtMs = pinnedBlockedPermanently ||
|
|
1239
|
+
(pinnedStateRecoveryAtMs === null && pinnedCircuitRecoveryAtMs === null)
|
|
1240
|
+
? null
|
|
1241
|
+
: Math.max(pinnedStateRecoveryAtMs ?? 0, pinnedCircuitRecoveryAtMs ?? 0);
|
|
1242
|
+
// The message only words the recovery deadline as a rate-limit reset
|
|
1243
|
+
// when the rate-limit records are in fact what supplies it — a
|
|
1244
|
+
// breaker or cooldown can end later and then bounds it instead.
|
|
1245
|
+
const pinnedRateLimitResetAtMs = pinnedRecoveryBounds?.rateLimitAtMs ?? null;
|
|
1246
|
+
const recoveryBound = pinnedResetAtMs !== null &&
|
|
1247
|
+
pinnedRateLimitResetAtMs !== null &&
|
|
1248
|
+
pinnedRateLimitResetAtMs >= pinnedResetAtMs
|
|
1249
|
+
? "rate-limit"
|
|
1250
|
+
: "other";
|
|
1251
|
+
const errorBody = buildPinnedUnavailableErrorBody(pinnedIndex, accountSkipReasons, {
|
|
1252
|
+
// typeof check so a forced index of 0 still reads as forced.
|
|
1253
|
+
pinSource: typeof state.forcedAccountIndex === "number" ? "forced" : "manual",
|
|
1254
|
+
resetAtMs: pinnedResetAtMs,
|
|
1255
|
+
recoveryBound,
|
|
1256
|
+
// The recorded verdict above is the retry loop's
|
|
1257
|
+
// "already-attempted" on the dominant path; the re-read runtime
|
|
1258
|
+
// state is what the operator-facing sentence should describe.
|
|
1259
|
+
currentSkipReason: pinnedCurrentSkipReason,
|
|
1260
|
+
now: evaluatedAtMs,
|
|
1261
|
+
});
|
|
1174
1262
|
if (errorBody.reason === null) {
|
|
1175
1263
|
state.status.lastError = `pinned-503 missing skip reason (pinnedIndex=${pinnedIndex})`;
|
|
1176
1264
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.7",
|
|
4
4
|
"description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|