codex-multi-auth 2.22.0 → 2.24.0
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 +9 -9
- package/LICENSE +21 -21
- package/README.md +159 -150
- package/assets/codex-multi-auth-icon.svg +39 -39
- package/assets/readme-hero.svg +31 -31
- package/config/README.md +31 -31
- package/config/codex-legacy.json +1021 -1021
- package/config/codex-modern.json +347 -347
- package/config/minimal-codex.json +15 -15
- package/config/schema/config.schema.json +8 -0
- package/dist/index.js +33 -10
- package/dist/lib/account-policy.d.ts +2 -0
- package/dist/lib/account-policy.js +3 -1
- package/dist/lib/accounts/rate-limits.d.ts +6 -0
- package/dist/lib/accounts/rate-limits.js +14 -1
- package/dist/lib/accounts.d.ts +6 -1
- package/dist/lib/accounts.js +33 -8
- package/dist/lib/api-route-store.d.ts +1 -0
- package/dist/lib/api-route-store.js +2 -1
- package/dist/lib/auth/account-access.js +2 -1
- package/dist/lib/auth/auth.js +3 -2
- package/dist/lib/auth/device-auth.js +2 -1
- package/dist/lib/auth/http.d.ts +1 -0
- package/dist/lib/auth/http.js +1 -0
- package/dist/lib/auth/token-utils.d.ts +5 -0
- package/dist/lib/auth/token-utils.js +10 -0
- package/dist/lib/codex-manager/account-manager-commands.js +1 -0
- package/dist/lib/codex-manager/commands/account.js +15 -0
- package/dist/lib/codex-manager/commands/locale.d.ts +12 -0
- package/dist/lib/codex-manager/commands/locale.js +25 -0
- package/dist/lib/codex-manager/commands/resets.js +18 -5
- package/dist/lib/codex-manager/commands/status.d.ts +4 -0
- package/dist/lib/codex-manager/commands/status.js +476 -5
- package/dist/lib/codex-manager/commands/workspace.d.ts +4 -2
- package/dist/lib/codex-manager/commands/workspace.js +33 -2
- package/dist/lib/codex-manager/help.js +5 -3
- package/dist/lib/codex-manager.js +112 -30
- package/dist/lib/config.js +3 -0
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +2 -0
- package/dist/lib/context-budget-response.js +8 -8
- package/dist/lib/context-overflow.js +10 -10
- package/dist/lib/locale.d.ts +7 -0
- package/dist/lib/locale.js +35 -0
- package/dist/lib/oauth-success.html +125 -125
- package/dist/lib/policy/runtime-policy-cache.js +1 -1
- package/dist/lib/policy/runtime-policy.d.ts +1 -0
- package/dist/lib/policy/runtime-policy.js +4 -0
- package/dist/lib/preemptive-quota-scheduler.js +12 -3
- package/dist/lib/prompts/codex-host-bridge.js +138 -138
- package/dist/lib/prompts/codex.js +79 -79
- package/dist/lib/quota-cache.d.ts +4 -0
- package/dist/lib/quota-probe.js +2 -1
- package/dist/lib/quota-readiness.d.ts +1 -1
- package/dist/lib/quota-readiness.js +9 -2
- package/dist/lib/request/proxy-fetch.d.ts +5 -0
- package/dist/lib/request/proxy-fetch.js +37 -0
- package/dist/lib/request/rate-limit-decision.js +1 -1
- package/dist/lib/request/url-rewriting.js +17 -0
- package/dist/lib/runtime/account-model-catalog.d.ts +2 -0
- package/dist/lib/runtime/account-model-catalog.js +7 -2
- package/dist/lib/runtime/account-reset-credits.d.ts +11 -2
- package/dist/lib/runtime/account-reset-credits.js +35 -12
- package/dist/lib/runtime/app-bind.d.ts +4 -1
- package/dist/lib/runtime/app-bind.js +63 -3
- package/dist/lib/runtime/automatic-account-checks.js +6 -5
- package/dist/lib/runtime/automatic-subscription-checks.js +4 -3
- package/dist/lib/runtime/model-discovery-status.js +2 -2
- package/dist/lib/runtime/reset-credit-routing.d.ts +11 -3
- package/dist/lib/runtime/reset-credit-routing.js +66 -21
- package/dist/lib/runtime/reset-credits.d.ts +31 -7
- package/dist/lib/runtime/reset-credits.js +135 -28
- package/dist/lib/runtime/responses-websocket.d.ts +30 -0
- package/dist/lib/runtime/responses-websocket.js +243 -29
- package/dist/lib/runtime/rotation-account-selection.d.ts +5 -4
- package/dist/lib/runtime/rotation-proxy-state.d.ts +3 -0
- package/dist/lib/runtime/rotation-server-types.d.ts +4 -1
- package/dist/lib/runtime/subscription-quota-order.d.ts +1 -0
- package/dist/lib/runtime/subscription-quota-order.js +7 -3
- package/dist/lib/runtime/workspace-model-scopes.d.ts +2 -0
- package/dist/lib/runtime/workspace-model-scopes.js +14 -4
- package/dist/lib/runtime-rotation-proxy.js +138 -38
- package/dist/lib/schemas.d.ts +5 -0
- package/dist/lib/schemas.js +1 -0
- package/dist/lib/storage/snapshot-merge.js +15 -4
- package/dist/lib/web-dashboard-html.d.ts +1 -0
- package/dist/lib/web-dashboard-html.js +66 -0
- package/dist/lib/web-dashboard.d.ts +36 -0
- package/dist/lib/web-dashboard.js +322 -0
- package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
- package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
- package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
- package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
- package/node_modules/@codex-ai/plugin/package.json +9 -9
- package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
- package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
- package/node_modules/@codex-ai/sdk/package.json +8 -8
- package/package.json +209 -207
- package/scripts/codex-app-launcher.js +653 -653
- package/scripts/codex-app-router.js +446 -402
- package/scripts/codex-bin-resolver.js +332 -332
- package/scripts/codex-multi-auth.js +65 -65
- package/scripts/codex-routing.js +63 -62
- package/scripts/codex.js +7143 -7006
- package/scripts/install-codex-auth-utils.js +213 -213
- package/scripts/mcodex.js +367 -367
- package/scripts/postinstall.js +114 -114
- package/scripts/preuninstall.js +265 -265
- package/skills/codex-auth-setup/SKILL.md +73 -73
- package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
- package/vendor/codex-ai-plugin/dist/index.js +2 -2
- package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
- package/vendor/codex-ai-plugin/dist/tool.js +29 -29
- package/vendor/codex-ai-plugin/package.json +9 -9
- package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
- package/vendor/codex-ai-sdk/dist/index.js +2 -2
- package/vendor/codex-ai-sdk/package.json +8 -8
|
@@ -98,11 +98,28 @@ function shouldBypassProxyForUrl(url, noProxyValue) {
|
|
|
98
98
|
}
|
|
99
99
|
return false;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Loopback destinations never traverse a configured proxy: routing them through
|
|
103
|
+
* HTTP_PROXY would leak managed bearer tokens to the proxy and break the
|
|
104
|
+
* loopback-only runtime proxy contract. This bypass applies even when NO_PROXY
|
|
105
|
+
* is unset, matching curl/git proxy conventions.
|
|
106
|
+
*/
|
|
107
|
+
function isLoopbackRequestHost(url) {
|
|
108
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
109
|
+
return (hostname === "localhost" ||
|
|
110
|
+
hostname.endsWith(".localhost") ||
|
|
111
|
+
hostname === "::1" ||
|
|
112
|
+
hostname.startsWith("127.") ||
|
|
113
|
+
hostname.startsWith("::ffff:127."));
|
|
114
|
+
}
|
|
101
115
|
export function resolveProxyUrlForRequest(url, env = process.env) {
|
|
102
116
|
const parsed = new URL(url);
|
|
103
117
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
104
118
|
return undefined;
|
|
105
119
|
}
|
|
120
|
+
if (isLoopbackRequestHost(parsed)) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
106
123
|
const httpProxy = resolveProxyEnvValue(env, "http_proxy", "HTTP_PROXY");
|
|
107
124
|
const httpsProxy = resolveProxyEnvValue(env, "https_proxy", "HTTPS_PROXY");
|
|
108
125
|
if (!httpProxy && !httpsProxy) {
|
|
@@ -23,6 +23,8 @@ export declare class AccountModelCatalog {
|
|
|
23
23
|
private readonly pending;
|
|
24
24
|
constructor(fetchCatalog: (accountKey: string) => Promise<unknown>, now?: () => number, ttlMs?: number);
|
|
25
25
|
private read;
|
|
26
|
+
/** Includes successful empty catalogs; stale or failed discovery remains unknown. */
|
|
27
|
+
hasUsablePickerSnapshot(key: string): boolean;
|
|
26
28
|
/** Picker-only stale-while-refresh view; routing still awaits fresh discovery. */
|
|
27
29
|
cachedList(accountKeys: string[]): CatalogModel[];
|
|
28
30
|
invalidate(): void;
|
|
@@ -83,12 +83,17 @@ export class AccountModelCatalog {
|
|
|
83
83
|
this.pending.delete(key);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
/** Includes successful empty catalogs; stale or failed discovery remains unknown. */
|
|
87
|
+
hasUsablePickerSnapshot(key) {
|
|
88
|
+
const entry = this.cache.get(key);
|
|
89
|
+
return Boolean(entry && !entry.error && entry.checkedAt <= this.now() && this.now() - entry.checkedAt < 15 * 60_000);
|
|
90
|
+
}
|
|
86
91
|
/** Picker-only stale-while-refresh view; routing still awaits fresh discovery. */
|
|
87
92
|
cachedList(accountKeys) {
|
|
88
93
|
return buildVisibleModelUnion(accountKeys.map((key, index) => {
|
|
89
94
|
const entry = this.cache.get(key);
|
|
90
|
-
const usable =
|
|
91
|
-
return { id: key, kind: "oauth", priority: index, enabled: true, models: usable ? entry
|
|
95
|
+
const usable = this.hasUsablePickerSnapshot(key);
|
|
96
|
+
return { id: key, kind: "oauth", priority: index, enabled: true, models: usable ? entry?.models ?? [] : [], visibleModels: null };
|
|
92
97
|
}));
|
|
93
98
|
}
|
|
94
99
|
invalidate() { this.cache.clear(); }
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { AccountManager } from '../accounts.js';
|
|
2
2
|
import { type AccountMetadataV3 } from '../storage.js';
|
|
3
|
-
import { ResetCreditService, type ResetTarget } from './reset-credits.js';
|
|
3
|
+
import { ResetCreditService, type ResetCreditDetail, type ResetTarget } from './reset-credits.js';
|
|
4
4
|
export declare function resetTargetForStoredAccount(account: AccountMetadataV3): ResetTarget | null;
|
|
5
5
|
export declare function isResetTargetEnabled(account: AccountMetadataV3, target: ResetTarget): boolean;
|
|
6
6
|
export declare function createResetCreditService(manager?: AccountManager): ResetCreditService;
|
|
7
7
|
export declare function loadResetCreditState(): Promise<{
|
|
8
|
-
version: 1;
|
|
8
|
+
version: 1 | 2;
|
|
9
9
|
policy: "last-resort" | "manual";
|
|
10
10
|
snapshots: Record<string, {
|
|
11
11
|
updatedAt: number;
|
|
12
12
|
availableCount: number | null;
|
|
13
|
+
credits?: {
|
|
14
|
+
expiresAt?: number | null | undefined;
|
|
15
|
+
title?: string | null | undefined;
|
|
16
|
+
}[] | null | undefined;
|
|
13
17
|
ordinaryUsageAllowed: boolean | null;
|
|
14
18
|
planType: string | null;
|
|
15
19
|
primary: {
|
|
@@ -26,6 +30,7 @@ export declare function loadResetCreditState(): Promise<{
|
|
|
26
30
|
pending?: {
|
|
27
31
|
key: string;
|
|
28
32
|
idempotencyKey: string;
|
|
33
|
+
creditId?: string | undefined;
|
|
29
34
|
} | undefined;
|
|
30
35
|
lastAutomaticCheckAt?: number | undefined;
|
|
31
36
|
lastRedemptionAt?: number | undefined;
|
|
@@ -38,4 +43,8 @@ export declare function loadResetCreditState(): Promise<{
|
|
|
38
43
|
}>;
|
|
39
44
|
/** Local terminal labels only; never used in provider requests or public artifacts. */
|
|
40
45
|
export declare function resetAccountLabel(account: Pick<AccountMetadataV3, "email">, index: number): string;
|
|
46
|
+
/** Strips terminal control codes, invisible formatters, and whitespace before printing credit titles. */
|
|
47
|
+
export declare function sanitizeCreditTitle(title: string | null | undefined): string | null;
|
|
48
|
+
/** One local terminal line per credit row; expiresAt is Unix seconds and null means it never expires. */
|
|
49
|
+
export declare function formatResetCreditLine(credit: ResetCreditDetail, index: number): string;
|
|
41
50
|
export declare function refreshAndPrintResetCredits(log: (message: string) => void): Promise<void>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { AccountManager,
|
|
2
|
+
import { AccountManager, resolveAccountRecordId, sanitizeEmail } from '../accounts.js';
|
|
3
3
|
import { loadAccounts } from '../storage.js';
|
|
4
4
|
import { getCodexMultiAuthDir } from '../runtime-paths.js';
|
|
5
5
|
import { ensureFreshAccessToken } from './rotation-token-refresh.js';
|
|
6
|
-
import { modelScopeId, workspaceModelScopes } from './workspace-model-scopes.js';
|
|
6
|
+
import { modelScopeId, workspaceModelScopes, nativeWorkspaceIdentity } from './workspace-model-scopes.js';
|
|
7
7
|
import { nativeRateLimitsRpc } from './native-rate-limits.js';
|
|
8
8
|
import { codexCliAccountIdFor } from '../auth/token-utils.js';
|
|
9
9
|
import { isRecord } from '../utils.js';
|
|
@@ -11,10 +11,13 @@ import { ResetCreditService } from './reset-credits.js';
|
|
|
11
11
|
import { withCheckProgress } from '../ui/check-progress.js';
|
|
12
12
|
export function resetTargetForStoredAccount(account) {
|
|
13
13
|
// Organization display aliases are not native ChatGPT workspace identities.
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const selected = account.workspaces?.length ? account.workspaces[account.currentWorkspaceIndex ?? 0] : undefined;
|
|
15
|
+
if (account.workspaces?.length && (!selected || selected.enabled === false))
|
|
16
|
+
return null;
|
|
17
|
+
const stored = (selected?.id ?? account.accountId)?.trim();
|
|
18
|
+
const id = nativeWorkspaceIdentity({ access: account.accessToken, codexCliMirror: account.codexCliMirror }, stored);
|
|
16
19
|
const workspace = account.workspaces?.find(w => w.id === id);
|
|
17
|
-
if (!id || id.startsWith("org-") || account.enabled === false || workspace?.enabled === false)
|
|
20
|
+
if (!id || id.startsWith("org-") || account.enabled === false || (!selected && workspace?.enabled === false))
|
|
18
21
|
return null;
|
|
19
22
|
return { key: modelScopeId('oauth', resolveAccountRecordId(account), id), accountId: id };
|
|
20
23
|
}
|
|
@@ -24,8 +27,7 @@ export function isResetTargetEnabled(account, target) {
|
|
|
24
27
|
const canonical = resetTargetForStoredAccount(account);
|
|
25
28
|
if (canonical?.key === target.key && canonical.accountId === target.accountId)
|
|
26
29
|
return true;
|
|
27
|
-
|
|
28
|
-
return !target.accountId.startsWith('org-') && scopeKey === target.key && Boolean(account.workspaces?.some(w => w.id === target.accountId && w.enabled !== false));
|
|
30
|
+
return false;
|
|
29
31
|
}
|
|
30
32
|
export function createResetCreditService(manager) {
|
|
31
33
|
const auth = async (target) => {
|
|
@@ -43,17 +45,17 @@ export function createResetCreditService(manager) {
|
|
|
43
45
|
return { accessToken: fresh.accessToken, accountId: target.accountId, expiresAt: fresh.account.expires ?? 0, codexCliMirror: current.codexCliMirror };
|
|
44
46
|
};
|
|
45
47
|
return new ResetCreditService(join(getCodexMultiAuthDir(), 'reset-credits.json'), {
|
|
46
|
-
read: async (target) => {
|
|
48
|
+
read: async (target, { creditDetails }) => {
|
|
47
49
|
// The backend answers for the id written into its auth.json (the mirror id when one
|
|
48
50
|
// applies). Verify the reply against that id; the snapshot stays keyed on the target.
|
|
49
51
|
const freshAuth = await auth(target);
|
|
50
52
|
const sent = codexCliAccountIdFor(freshAuth, freshAuth.accessToken) ?? freshAuth.accountId;
|
|
51
|
-
const reply = await nativeRateLimitsRpc(freshAuth, 'account/rateLimits/read', { excludeResetCreditDetails:
|
|
53
|
+
const reply = await nativeRateLimitsRpc(freshAuth, 'account/rateLimits/read', { excludeResetCreditDetails: !creditDetails });
|
|
52
54
|
if (!isRecord(reply) || reply.accountId !== sent)
|
|
53
55
|
throw Error('Reset credit workspace identity mismatch');
|
|
54
56
|
return { ...reply, accountId: target.accountId };
|
|
55
57
|
},
|
|
56
|
-
consume: async (target, idempotencyKey) => {
|
|
58
|
+
consume: async (target, idempotencyKey, creditId) => {
|
|
57
59
|
const freshAuth = await auth(target);
|
|
58
60
|
const disk = await loadAccounts();
|
|
59
61
|
const matches = disk?.accounts.filter(a => isResetTargetEnabled(a, target)) ?? [];
|
|
@@ -61,7 +63,7 @@ export function createResetCreditService(manager) {
|
|
|
61
63
|
throw Error('Reset account identity is ambiguous; no credit consumed');
|
|
62
64
|
if (matches.length !== 1)
|
|
63
65
|
throw Error('Reset target was removed, disabled, or changed; no credit consumed');
|
|
64
|
-
return nativeRateLimitsRpc(freshAuth, 'account/rateLimitResetCredit/consume', { idempotencyKey });
|
|
66
|
+
return nativeRateLimitsRpc(freshAuth, 'account/rateLimitResetCredit/consume', { idempotencyKey, ...(creditId === undefined ? {} : { creditId }) });
|
|
65
67
|
},
|
|
66
68
|
});
|
|
67
69
|
}
|
|
@@ -72,6 +74,22 @@ export function resetAccountLabel(account, index) {
|
|
|
72
74
|
const printable = email && !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(email);
|
|
73
75
|
return `Account ${index + 1}${printable ? ` (${email})` : ""}`;
|
|
74
76
|
}
|
|
77
|
+
/** Strips terminal control codes, invisible formatters, and whitespace before printing credit titles. */
|
|
78
|
+
export function sanitizeCreditTitle(title) {
|
|
79
|
+
if (typeof title !== "string")
|
|
80
|
+
return null;
|
|
81
|
+
const trimmed = title.trim();
|
|
82
|
+
if (!trimmed || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(trimmed))
|
|
83
|
+
return null;
|
|
84
|
+
return trimmed;
|
|
85
|
+
}
|
|
86
|
+
/** One local terminal line per credit row; expiresAt is Unix seconds and null means it never expires. */
|
|
87
|
+
export function formatResetCreditLine(credit, index) {
|
|
88
|
+
const date = new Date(typeof credit.expiresAt === 'number' && credit.expiresAt > 0 ? credit.expiresAt * 1000 : Number.NaN);
|
|
89
|
+
const expiry = credit.expiresAt === null ? 'no expiry' : Number.isNaN(date.getTime()) ? 'expires unknown' : `expires ${date.toLocaleString()}`;
|
|
90
|
+
const title = sanitizeCreditTitle(credit.title);
|
|
91
|
+
return ` - credit #${index + 1}: ${expiry}${title ? ` (${title})` : ''}`;
|
|
92
|
+
}
|
|
75
93
|
export async function refreshAndPrintResetCredits(log) {
|
|
76
94
|
const storage = await loadAccounts();
|
|
77
95
|
if (!storage?.accounts.length)
|
|
@@ -80,7 +98,12 @@ export async function refreshAndPrintResetCredits(log) {
|
|
|
80
98
|
const targets = storage.accounts.map(resetTargetForStoredAccount).filter((t) => Boolean(t));
|
|
81
99
|
try {
|
|
82
100
|
const snapshots = await withCheckProgress(`Checking reset credits for ${targets.length} accounts`, () => createResetCreditService(manager).refresh(targets), log);
|
|
83
|
-
storage.accounts.forEach((a, i) => {
|
|
101
|
+
storage.accounts.forEach((a, i) => {
|
|
102
|
+
const target = resetTargetForStoredAccount(a);
|
|
103
|
+
const snap = target ? snapshots[target.key] : undefined;
|
|
104
|
+
log(`${resetAccountLabel(a, i)}: available subscription resets: ${snap?.availableCount ?? 'unknown (not verified)'}`);
|
|
105
|
+
snap?.credits?.forEach((c, idx) => log(formatResetCreditLine(c, idx)));
|
|
106
|
+
});
|
|
84
107
|
}
|
|
85
108
|
finally {
|
|
86
109
|
await manager.flushPendingSave();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WebSocketDisconnectDiagnostic } from "./responses-websocket.js";
|
|
1
|
+
import type { WebSocketDisconnectDiagnostic, WebSocketUpgradeRejectionDiagnostic } from "./responses-websocket.js";
|
|
2
2
|
export interface AppBindPaths {
|
|
3
3
|
codexHome: string;
|
|
4
4
|
configPath: string;
|
|
@@ -39,6 +39,9 @@ export interface AppBindState {
|
|
|
39
39
|
}
|
|
40
40
|
export interface AppBindRouterStatus {
|
|
41
41
|
lastWebsocketDisconnect?: WebSocketDisconnectDiagnostic | null;
|
|
42
|
+
websocketActiveConnections?: number | null;
|
|
43
|
+
websocketCapacityReclaims?: number | null;
|
|
44
|
+
lastWebsocketUpgradeRejection?: WebSocketUpgradeRejectionDiagnostic | null;
|
|
42
45
|
schedulingStrategy?: "hybrid" | "sequential";
|
|
43
46
|
state: string | null;
|
|
44
47
|
pid: number | null;
|
|
@@ -232,6 +232,29 @@ function readWebsocketDisconnect(value) {
|
|
|
232
232
|
if (!isRecord(value) || typeof value.at !== "number" || !Number.isFinite(value.at) || value.at < 0)
|
|
233
233
|
return null;
|
|
234
234
|
const { cause, stage, closeCode, routeKind, connectionAgeMs, eventCount } = value;
|
|
235
|
+
const transport = {};
|
|
236
|
+
for (const key of ["requestBytes", "inputItemCount"]) {
|
|
237
|
+
const field = value[key];
|
|
238
|
+
if (field !== undefined) {
|
|
239
|
+
if (typeof field !== "number" || !Number.isSafeInteger(field) || field < 0)
|
|
240
|
+
return null;
|
|
241
|
+
transport[key] = field;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const key of ["isPrewarm", "hasPreviousResponse", "reusedConnection", "sendCompleted", "compressionNegotiated", "tcpEndReceived", "tcpCloseHadError"]) {
|
|
245
|
+
const field = value[key];
|
|
246
|
+
if (field !== undefined) {
|
|
247
|
+
if (typeof field !== "boolean")
|
|
248
|
+
return null;
|
|
249
|
+
transport[key] = field;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const errorCode = value.transportErrorCode;
|
|
253
|
+
if (errorCode !== undefined) {
|
|
254
|
+
if (errorCode !== "ECONNRESET" && errorCode !== "EPIPE" && errorCode !== "ETIMEDOUT" && errorCode !== "OTHER")
|
|
255
|
+
return null;
|
|
256
|
+
transport.transportErrorCode = errorCode;
|
|
257
|
+
}
|
|
235
258
|
if (routeKind !== undefined && routeKind !== "oauth" && routeKind !== "api" && routeKind !== "zdr")
|
|
236
259
|
return null;
|
|
237
260
|
for (const count of [connectionAgeMs, eventCount])
|
|
@@ -246,7 +269,24 @@ function readWebsocketDisconnect(value) {
|
|
|
246
269
|
return { at: value.at, cause, stage, ...(typeof closeCode === "number" ? { closeCode } : {}),
|
|
247
270
|
...(routeKind === "oauth" || routeKind === "api" || routeKind === "zdr" ? { routeKind } : {}),
|
|
248
271
|
...(typeof connectionAgeMs === "number" ? { connectionAgeMs } : {}),
|
|
249
|
-
...(typeof eventCount === "number" ? { eventCount } : {}) };
|
|
272
|
+
...(typeof eventCount === "number" ? { eventCount } : {}), ...transport };
|
|
273
|
+
}
|
|
274
|
+
function readWebsocketCounter(value, maximum = Number.MAX_SAFE_INTEGER) {
|
|
275
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= maximum
|
|
276
|
+
? value : null;
|
|
277
|
+
}
|
|
278
|
+
function readWebsocketUpgradeRejection(value) {
|
|
279
|
+
if (!isRecord(value))
|
|
280
|
+
return null;
|
|
281
|
+
const at = readWebsocketCounter(value.at, 8.64e15);
|
|
282
|
+
const activeConnections = readWebsocketCounter(value.activeConnections, 64);
|
|
283
|
+
const { reason, status } = value;
|
|
284
|
+
if (at === null || activeConnections === null)
|
|
285
|
+
return null;
|
|
286
|
+
if (!((reason === "origin" && status === 403) ||
|
|
287
|
+
((reason === "capacity" || reason === "repeated_disconnect") && status === 426)))
|
|
288
|
+
return null;
|
|
289
|
+
return { at, reason, status, activeConnections };
|
|
250
290
|
}
|
|
251
291
|
async function readRouterStatus(path) {
|
|
252
292
|
const record = await readJsonFile(path);
|
|
@@ -254,6 +294,9 @@ async function readRouterStatus(path) {
|
|
|
254
294
|
return null;
|
|
255
295
|
return {
|
|
256
296
|
lastWebsocketDisconnect: readWebsocketDisconnect(record.lastWebsocketDisconnect),
|
|
297
|
+
websocketActiveConnections: readWebsocketCounter(record.websocketActiveConnections, 64),
|
|
298
|
+
websocketCapacityReclaims: readWebsocketCounter(record.websocketCapacityReclaims),
|
|
299
|
+
lastWebsocketUpgradeRejection: readWebsocketUpgradeRejection(record.lastWebsocketUpgradeRejection),
|
|
257
300
|
schedulingStrategy: record.schedulingStrategy === "hybrid" || record.schedulingStrategy === "sequential"
|
|
258
301
|
? record.schedulingStrategy : undefined,
|
|
259
302
|
state: readString(record, "state"),
|
|
@@ -1407,7 +1450,22 @@ export function formatWebsocketDisconnect(value) {
|
|
|
1407
1450
|
const diagnostic = readWebsocketDisconnect(value);
|
|
1408
1451
|
if (!diagnostic || diagnostic.at > 8.64e15)
|
|
1409
1452
|
return null;
|
|
1410
|
-
|
|
1453
|
+
const transport = ["requestBytes", "inputItemCount", "isPrewarm", "hasPreviousResponse", "reusedConnection", "sendCompleted", "compressionNegotiated", "transportErrorCode", "tcpEndReceived", "tcpCloseHadError"]
|
|
1454
|
+
.filter(key => diagnostic[key] !== undefined).map(key => `; ${key}=${diagnostic[key]}`).join("");
|
|
1455
|
+
return `Last WebSocket disconnect: ${new Date(diagnostic.at).toISOString()}; cause=${diagnostic.cause}; stage=${diagnostic.stage}${diagnostic.closeCode !== undefined ? `; closeCode=${diagnostic.closeCode}` : ""}${diagnostic.routeKind !== undefined ? `; route=${diagnostic.routeKind}` : ""}${diagnostic.connectionAgeMs !== undefined ? `; connectionAgeMs=${diagnostic.connectionAgeMs}` : ""}${diagnostic.eventCount !== undefined ? `; events=${diagnostic.eventCount}` : ""}${transport}`;
|
|
1456
|
+
}
|
|
1457
|
+
function formatWebsocketAdmission(status) {
|
|
1458
|
+
const active = readWebsocketCounter(status?.websocketActiveConnections, 64);
|
|
1459
|
+
const reclaimed = readWebsocketCounter(status?.websocketCapacityReclaims);
|
|
1460
|
+
const counters = [
|
|
1461
|
+
...(active !== null ? [`active=${active}`] : []),
|
|
1462
|
+
...(reclaimed !== null ? [`idleReclaimed=${reclaimed}`] : []),
|
|
1463
|
+
];
|
|
1464
|
+
const rejection = readWebsocketUpgradeRejection(status?.lastWebsocketUpgradeRejection);
|
|
1465
|
+
return [
|
|
1466
|
+
...(counters.length ? [`WebSocket connections: ${counters.join("; ")}`] : []),
|
|
1467
|
+
...(rejection ? [`Last WebSocket upgrade rejection: ${new Date(rejection.at).toISOString()}; reason=${rejection.reason}; status=${rejection.status}; active=${rejection.activeConnections}`] : []),
|
|
1468
|
+
];
|
|
1411
1469
|
}
|
|
1412
1470
|
export function formatAppBindStatus(status) {
|
|
1413
1471
|
if (status.unmanagedBind && !status.state) {
|
|
@@ -1434,11 +1492,13 @@ export function formatAppBindStatus(status) {
|
|
|
1434
1492
|
parts.push(`lastAccount=Account ${status.router.lastAccountIndex + 1}`);
|
|
1435
1493
|
}
|
|
1436
1494
|
const diagnostic = formatWebsocketDisconnect(status.router?.lastWebsocketDisconnect);
|
|
1495
|
+
const admission = formatWebsocketAdmission(status.router);
|
|
1437
1496
|
if (status.state.nativeOpenai)
|
|
1438
|
-
return [`Codex app bind: ${parts.join(", ")}, native OpenAI, live account catalogs`, ...(diagnostic ? [diagnostic] : [])].join("\n");
|
|
1497
|
+
return [`Codex app bind: ${parts.join(", ")}, native OpenAI, live account catalogs`, ...(diagnostic ? [diagnostic] : []), ...admission].join("\n");
|
|
1439
1498
|
return [
|
|
1440
1499
|
`Codex app bind: ${parts.join(", ")}`,
|
|
1441
1500
|
...(diagnostic ? [diagnostic] : []),
|
|
1501
|
+
...admission,
|
|
1442
1502
|
[
|
|
1443
1503
|
"Note: Codex Desktop may hide history while the app bind selects the",
|
|
1444
1504
|
"codex-multi-auth-runtime-proxy provider; use `codex-multi-auth rotation",
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { promises as fs } from "node:fs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { resolveAccountRecordId } from "../accounts.js";
|
|
5
|
-
import { extractAccountId } from "../auth/token-utils.js";
|
|
5
|
+
import { extractAccountId, nativeWorkspaceIdentity } from "../auth/token-utils.js";
|
|
6
6
|
import { getAccountPolicyKey } from "../account-policy.js";
|
|
7
7
|
import { withFileTransactionLock } from "../storage/file-lock.js";
|
|
8
8
|
import { withRetry } from "../fs-retry.js";
|
|
@@ -17,11 +17,12 @@ const schema = z.record(z.string().regex(/^sha256:[a-f0-9]{64}$/), z.number().fi
|
|
|
17
17
|
const retry = { maxAttempts: 6, backoffMs: 25 };
|
|
18
18
|
/** Honor the saved selection; a disabled/invalid selection must not fall back to a sibling. */
|
|
19
19
|
export function automaticCheckWorkspaceId(account) {
|
|
20
|
+
const identity = { access: account.accessToken, codexCliMirror: account.codexCliMirror };
|
|
20
21
|
if (account.workspaces?.length) {
|
|
21
22
|
const selected = account.workspaces[account.currentWorkspaceIndex ?? 0];
|
|
22
|
-
return selected?.enabled === false ? undefined : selected?.id
|
|
23
|
+
return selected?.enabled === false ? undefined : nativeWorkspaceIdentity(identity, selected?.id);
|
|
23
24
|
}
|
|
24
|
-
return account.accountId?.trim() || extractAccountId(account.accessToken)
|
|
25
|
+
return nativeWorkspaceIdentity(identity, account.accountId?.trim() || extractAccountId(account.accessToken));
|
|
25
26
|
}
|
|
26
27
|
/** Policy keys can be shared by organization members; attempt limits must never be. */
|
|
27
28
|
export function automaticAccountCheckKey(account) {
|
|
@@ -72,7 +73,7 @@ export async function runAutomaticAccountChecks(options) {
|
|
|
72
73
|
// Keep every still-saved workspace's throttle, including temporarily disabled selections.
|
|
73
74
|
const keys = new Set(storage.accounts.flatMap(account => [
|
|
74
75
|
automaticAccountCheckKey(account),
|
|
75
|
-
...(account.workspaces ?? []).map(workspace => workspaceAttemptKey(account, workspace.id
|
|
76
|
+
...(account.workspaces ?? []).map(workspace => workspaceAttemptKey(account, nativeWorkspaceIdentity({ access: account.accessToken, codexCliMirror: account.codexCliMirror }, workspace.id) ?? "")),
|
|
76
77
|
]));
|
|
77
78
|
for (const key of Object.keys(attempts))
|
|
78
79
|
if (!keys.has(key))
|
|
@@ -84,7 +85,7 @@ export async function runAutomaticAccountChecks(options) {
|
|
|
84
85
|
if (!account)
|
|
85
86
|
continue;
|
|
86
87
|
const key = automaticAccountCheckKey(account), policy = policies.accounts[getAccountPolicyKey(account)], now = options.now?.() ?? Date.now();
|
|
87
|
-
if (!policy?.autoPrime || policy.paused || policy.drained || account.enabled === false || account.authInvalidatedAt || (account.coolingDownUntil ?? 0) > now)
|
|
88
|
+
if (!policy?.autoPrime || policy.paused || policy.drained || policy.emergencyOnly || account.enabled === false || account.authInvalidatedAt || (account.coolingDownUntil ?? 0) > now)
|
|
88
89
|
continue;
|
|
89
90
|
const lastAttempt = attempts[key];
|
|
90
91
|
if (lastAttempt !== undefined && lastAttempt <= now + FUTURE_SKEW_MS && now - lastAttempt < AUTOMATIC_CHECK_INTERVAL_MS)
|
|
@@ -20,7 +20,8 @@ export function createAutomaticSubscriptionCheck(observe) {
|
|
|
20
20
|
await saveQuotaCache(cache, baseline);
|
|
21
21
|
});
|
|
22
22
|
return (signal) => runWithStoragePathState(storageState, () => runAutomaticAccountChecks({
|
|
23
|
-
|
|
23
|
+
// Priming may consume quota: malformed permissions must never normalize to OFF.
|
|
24
|
+
path: `${path}.automatic-checks.json`, signal, loadPolicies: () => loadAccountPolicyStore({ strict: true }),
|
|
24
25
|
loadAccounts: async () => { const snapshot = await read(); return snapshot.verified ? snapshot.storage : null; },
|
|
25
26
|
check: async (storage, index) => {
|
|
26
27
|
const manager = new AccountManager(undefined, storage);
|
|
@@ -36,8 +37,8 @@ export function createAutomaticSubscriptionCheck(observe) {
|
|
|
36
37
|
// A refresh is an I/O boundary: revalidate removal, policy, auth and workspace changes.
|
|
37
38
|
const disk = await read();
|
|
38
39
|
const current = disk.verified ? disk.storage?.accounts.find(row => resolveAccountRecordId(row) === resolveAccountRecordId(fresh.account)) : undefined;
|
|
39
|
-
const policy = current ? (await loadAccountPolicyStore()).accounts[getAccountPolicyKey(current)] : undefined;
|
|
40
|
-
if (!disk.storage || !current || current.refreshToken !== fresh.account.refreshToken || current.accessToken !== fresh.accessToken || !policy?.autoPrime || policy.paused || policy.drained || current.enabled === false || current.authInvalidatedAt || (current.coolingDownUntil ?? 0) > Date.now())
|
|
40
|
+
const policy = current ? (await loadAccountPolicyStore({ strict: true })).accounts[getAccountPolicyKey(current)] : undefined;
|
|
41
|
+
if (!disk.storage || !current || current.refreshToken !== fresh.account.refreshToken || current.accessToken !== fresh.accessToken || !policy?.autoPrime || policy.paused || policy.drained || policy.emergencyOnly || current.enabled === false || current.authInvalidatedAt || (current.coolingDownUntil ?? 0) > Date.now())
|
|
41
42
|
return;
|
|
42
43
|
if (automaticCheckWorkspaceId(current) !== targetId || signal.aborted)
|
|
43
44
|
return;
|
|
@@ -68,7 +68,7 @@ export async function discoverModelInventory(storage, routes, options = {}) {
|
|
|
68
68
|
const scopes = manager.getAccountsSnapshot().flatMap(workspaceModelScopes);
|
|
69
69
|
const catalog = new AccountModelCatalog(async (key) => {
|
|
70
70
|
const scope = scopes.find(item => item.id === key);
|
|
71
|
-
const account = scope?.
|
|
71
|
+
const account = scope?.routable ? manager.getAccountByIndex(scope.accountIndex) : null;
|
|
72
72
|
if (!account || account.enabled === false)
|
|
73
73
|
throw Error("Disabled");
|
|
74
74
|
const fresh = await ensureFreshAccessToken({
|
|
@@ -105,7 +105,7 @@ export async function discoverModelInventory(storage, routes, options = {}) {
|
|
|
105
105
|
}, now);
|
|
106
106
|
try {
|
|
107
107
|
entries.push(...await mapWithConcurrency(scopes, 3, async (scope) => {
|
|
108
|
-
if (scope.
|
|
108
|
+
if (scope.routable)
|
|
109
109
|
await catalog.list([scope.id]);
|
|
110
110
|
const snapshot = catalog.snapshot(scope.id);
|
|
111
111
|
return { id: scope.id, label: scope.label, kind: "oauth", enabled: scope.enabled, routable: scope.routable, bound: scope.bound, selected: scope.selected,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AccountManager, ManagedAccount } from '../accounts.js';
|
|
2
2
|
import type { ModelFamily } from '../prompts/codex.js';
|
|
3
3
|
import type { QuotaCacheEntry } from '../quota-cache.js';
|
|
4
|
-
import { type ResetCreditService } from './reset-credits.js';
|
|
5
|
-
import
|
|
4
|
+
import { type ResetSnapshot, type ResetCreditService, type ResetExhaustionEvidence } from './reset-credits.js';
|
|
5
|
+
import { workspaceModelScopes } from './workspace-model-scopes.js';
|
|
6
6
|
type Scope = ReturnType<typeof workspaceModelScopes>[number];
|
|
7
7
|
export declare function recoverResetQuota(options: {
|
|
8
8
|
model: string;
|
|
@@ -19,6 +19,7 @@ export declare function recoverResetQuota(options: {
|
|
|
19
19
|
snapshots: Awaited<ReturnType<ResetCreditService['status']>>['snapshots'];
|
|
20
20
|
}>;
|
|
21
21
|
};
|
|
22
|
+
quotaRejections?: ReadonlyMap<string, ResetExhaustionEvidence>;
|
|
22
23
|
observations: Map<string, QuotaCacheEntry>;
|
|
23
24
|
clearQuotaScheduler: (account: ManagedAccount) => void;
|
|
24
25
|
now: () => number;
|
|
@@ -37,4 +38,11 @@ export declare function applyConfirmedReset(options: {
|
|
|
37
38
|
clearQuotaScheduler: (account: ManagedAccount) => void;
|
|
38
39
|
now: number;
|
|
39
40
|
}): boolean;
|
|
41
|
+
/** Unknown, stale, or pending reset state must never authorize backup traffic. */
|
|
42
|
+
export declare function primaryPoolFullyExhausted(scopes: Map<number, Scope[]>, state: {
|
|
43
|
+
snapshots: Record<string, ResetSnapshot>;
|
|
44
|
+
pending?: {
|
|
45
|
+
key: string;
|
|
46
|
+
};
|
|
47
|
+
} | null, now: number, latestQuota?: (index: number, scope: Scope) => QuotaCacheEntry | null | undefined): boolean;
|
|
40
48
|
export {};
|
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
import { extractAccountId } from '../accounts.js';
|
|
2
1
|
import { LUNA_RESERVE_MODEL } from '../constants.js';
|
|
3
2
|
import { parseModelRoute } from '../model-route-policy.js';
|
|
4
3
|
import { subscriptionQuotaPreference } from './subscription-quota-order.js';
|
|
5
|
-
import { resetSnapshotQuota } from './reset-credits.js';
|
|
4
|
+
import { resetSnapshotQuota, isResetSubscription } from './reset-credits.js';
|
|
5
|
+
import { workspaceModelScopes } from './workspace-model-scopes.js';
|
|
6
|
+
/** Account-level quota blockers belong to the currently routed workspace. */
|
|
7
|
+
function clearRecoveredScopeQuota(options) {
|
|
8
|
+
const { account, scope, family, model, manager, clearQuotaScheduler } = options;
|
|
9
|
+
// Recheck selection and enablement: a reset read may outlive a workspace change.
|
|
10
|
+
if (account.lastRateLimitReason !== 'quota' || !workspaceModelScopes(account).some(current => current.routable && current.id === scope.id && current.accountId === scope.accountId))
|
|
11
|
+
return false;
|
|
12
|
+
delete account.rateLimitResetTimes[family];
|
|
13
|
+
delete account.rateLimitResetTimes[`${family}:${model}`];
|
|
14
|
+
if (account.cooldownReason === 'rate-limit')
|
|
15
|
+
manager.clearAccountCooldown(account);
|
|
16
|
+
clearQuotaScheduler(account);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
6
19
|
export async function recoverResetQuota(options) {
|
|
7
20
|
if (options.model === LUNA_RESERVE_MODEL || !options.native || options.pinned || parseModelRoute(options.model).kind !== 'oauth')
|
|
8
21
|
return false;
|
|
@@ -10,39 +23,62 @@ export async function recoverResetQuota(options) {
|
|
|
10
23
|
const candidates = [...options.scopes].flatMap(([index, scopes]) => { const account = options.manager.getAccountByIndex(index); return account && !account.authInvalidatedAt ? scopes.map(scope => ({ account, scope })) : []; });
|
|
11
24
|
if (!candidates.length)
|
|
12
25
|
return false;
|
|
26
|
+
const currentScope = (account, scope) => workspaceModelScopes(account).some(current => current.routable && current.id === scope.id && current.accountId === scope.accountId);
|
|
27
|
+
const rejectionForScope = (account, scope) => {
|
|
28
|
+
const proof = options.quotaRejections?.get(JSON.stringify([scope.id, options.model]));
|
|
29
|
+
const latest = options.quotaForScope(account, scope), now = options.now();
|
|
30
|
+
// An older in-flight response can deliver newer percentage headers. Only a
|
|
31
|
+
// later authoritative native permission check supersedes a body rejection.
|
|
32
|
+
const checkedAt = typeof latest?.ordinaryUsageAllowed === 'boolean'
|
|
33
|
+
? latest.ordinaryUsageCheckedAt ?? latest.updatedAt : 0;
|
|
34
|
+
return proof && proof.key === scope.id && proof.accountId === scope.accountId &&
|
|
35
|
+
Number.isFinite(proof.observedAt) && proof.observedAt <= now && now - proof.observedAt <= 60000 &&
|
|
36
|
+
proof.observedAt > checkedAt ? proof : undefined;
|
|
37
|
+
};
|
|
13
38
|
// Any usable capacity (including the reserve), unknown quota, or non-quota
|
|
14
39
|
// failure prevents an automatic credit from being spent.
|
|
15
40
|
if (candidates.some(({ account, scope }) => {
|
|
16
|
-
if (account.enabled === false || account.authInvalidatedAt || !scope.routable)
|
|
41
|
+
if (account.enabled === false || account.authInvalidatedAt || !scope.routable || !currentScope(account, scope))
|
|
17
42
|
return true;
|
|
18
43
|
const skip = options.manager.getAccountRuntimeSkipReason(account.index, family, options.model);
|
|
19
44
|
if (skip && !['rate-limited', 'cooling-down:rate-limit'].includes(skip))
|
|
20
45
|
return true;
|
|
21
46
|
const quota = subscriptionQuotaPreference(options.quotaForScope(account, scope), options.now());
|
|
22
|
-
return !quota.exhausted && !['rate-limited', 'cooling-down:rate-limit'].includes(skip ?? '');
|
|
47
|
+
return !quota.exhausted && !rejectionForScope(account, scope) && !['rate-limited', 'cooling-down:rate-limit'].includes(skip ?? '');
|
|
23
48
|
}))
|
|
24
49
|
return false;
|
|
25
50
|
candidates.sort((a, b) => Number(b.account.index === options.preferredIndex) - Number(a.account.index === options.preferredIndex) || (options.priorityByAccount?.[a.account.index] ?? 1) - (options.priorityByAccount?.[b.account.index] ?? 1));
|
|
26
51
|
const targets = candidates.filter(({ scope }) => !scope.accountId.startsWith("org-")).map(({ scope }) => ({ key: scope.id, accountId: scope.accountId }));
|
|
27
|
-
|
|
52
|
+
const evidence = candidates.flatMap(({ account, scope }) => {
|
|
53
|
+
if (scope.accountId.startsWith("org-"))
|
|
54
|
+
return [];
|
|
55
|
+
const rejection = rejectionForScope(account, scope);
|
|
56
|
+
if (rejection)
|
|
57
|
+
return [rejection];
|
|
58
|
+
const quota = subscriptionQuotaPreference(options.quotaForScope(account, scope), options.now());
|
|
59
|
+
return quota.exhausted && quota.observedAt !== null
|
|
60
|
+
? [{ key: scope.id, accountId: scope.accountId, observedAt: quota.observedAt }] : [];
|
|
61
|
+
});
|
|
62
|
+
await options.service.automatic(targets, evidence);
|
|
28
63
|
const state = await options.service.status();
|
|
29
64
|
let recovered = false;
|
|
30
65
|
for (const { account, scope } of candidates) {
|
|
31
66
|
const snapshot = state.snapshots[scope.id];
|
|
32
67
|
const previous = options.quotaForScope(account, scope);
|
|
33
|
-
|
|
68
|
+
const rejectedAt = rejectionForScope(account, scope)?.observedAt ?? 0;
|
|
69
|
+
// Healthy headers may arrive late from an older in-flight request. Native
|
|
70
|
+
// permission chronology wins there; unknown, exhausted, and failed reads do not.
|
|
71
|
+
const healthyHeaders = snapshot?.ordinaryUsageAllowed === true && previous?.status === 200 &&
|
|
72
|
+
(subscriptionQuotaPreference(previous, options.now()).remainingPercent ?? 0) > 0;
|
|
73
|
+
const previousAt = healthyHeaders
|
|
74
|
+
? typeof previous?.ordinaryUsageAllowed === 'boolean' ? previous.ordinaryUsageCheckedAt ?? previous.updatedAt : 0
|
|
75
|
+
: previous?.updatedAt ?? 0;
|
|
76
|
+
if (!snapshot || snapshot.updatedAt < Math.max(previousAt, rejectedAt) || options.now() - snapshot.updatedAt > 60000 || snapshot.updatedAt > options.now())
|
|
34
77
|
continue;
|
|
35
78
|
options.observations.set(JSON.stringify([scope.id, options.model]), resetSnapshotQuota(snapshot));
|
|
36
79
|
if (snapshot.ordinaryUsageAllowed !== true)
|
|
37
80
|
continue;
|
|
38
|
-
|
|
39
|
-
if ((scope.bound || (account.accountId?.startsWith('org-') && scope.accountId === extractAccountId(account.access))) && account.lastRateLimitReason === 'quota') {
|
|
40
|
-
delete account.rateLimitResetTimes[family];
|
|
41
|
-
delete account.rateLimitResetTimes[`${family}:${options.model}`];
|
|
42
|
-
if (account.cooldownReason === 'rate-limit')
|
|
43
|
-
options.manager.clearAccountCooldown(account);
|
|
44
|
-
options.clearQuotaScheduler(account);
|
|
45
|
-
}
|
|
81
|
+
clearRecoveredScopeQuota({ ...options, account, scope, family });
|
|
46
82
|
recovered = true;
|
|
47
83
|
}
|
|
48
84
|
if (recovered)
|
|
@@ -57,13 +93,22 @@ export function applyConfirmedReset(options) {
|
|
|
57
93
|
if (!s || options.lastRedemptionAt === undefined || s.updatedAt < options.lastRedemptionAt || s.ordinaryUsageAllowed !== true || s.updatedAt <= (options.previous?.updatedAt ?? 0) || options.now - s.updatedAt > 60000 || s.updatedAt > options.now)
|
|
58
94
|
return false;
|
|
59
95
|
options.observations.set(JSON.stringify([options.scope.id, options.model]), resetSnapshotQuota(s));
|
|
60
|
-
if ((options
|
|
61
|
-
delete options.account.rateLimitResetTimes[options.family];
|
|
62
|
-
delete options.account.rateLimitResetTimes[`${options.family}:${options.model}`];
|
|
63
|
-
if (options.account.cooldownReason === 'rate-limit')
|
|
64
|
-
options.manager.clearAccountCooldown(options.account);
|
|
65
|
-
options.clearQuotaScheduler(options.account);
|
|
96
|
+
if (clearRecoveredScopeQuota(options))
|
|
66
97
|
options.manager.saveToDiskDebounced();
|
|
67
|
-
}
|
|
68
98
|
return true;
|
|
69
99
|
}
|
|
100
|
+
/** Unknown, stale, or pending reset state must never authorize backup traffic. */
|
|
101
|
+
export function primaryPoolFullyExhausted(scopes, state, now, latestQuota) {
|
|
102
|
+
const primary = [...scopes].flatMap(([index, rows]) => rows.map(scope => ({ index, scope })));
|
|
103
|
+
return primary.length > 0 && !!state && !state.pending && primary.every(({ index, scope }) => {
|
|
104
|
+
const snapshot = state.snapshots[scope.id];
|
|
105
|
+
const latest = latestQuota?.(index, scope);
|
|
106
|
+
if (snapshot && latest && latest.updatedAt >= snapshot.updatedAt &&
|
|
107
|
+
!subscriptionQuotaPreference(latest, now).exhausted)
|
|
108
|
+
return false;
|
|
109
|
+
return scope.routable && !!snapshot && Number.isFinite(snapshot.updatedAt) &&
|
|
110
|
+
snapshot.updatedAt <= now && now - snapshot.updatedAt <= 60_000 &&
|
|
111
|
+
isResetSubscription(snapshot) && snapshot.ordinaryUsageAllowed === false &&
|
|
112
|
+
snapshot.availableCount === 0;
|
|
113
|
+
});
|
|
114
|
+
}
|