codex-multi-auth 2.23.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 -152
- 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/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/auth/token-utils.d.ts +5 -0
- package/dist/lib/auth/token-utils.js +10 -0
- package/dist/lib/codex-manager/commands/account.js +15 -0
- package/dist/lib/codex-manager/commands/resets.js +18 -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 +2 -2
- package/dist/lib/codex-manager.js +14 -1
- 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/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-readiness.d.ts +1 -1
- package/dist/lib/quota-readiness.js +9 -2
- package/dist/lib/request/rate-limit-decision.js +1 -1
- 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/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/rotation-proxy-state.d.ts +3 -0
- 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 +108 -29
- package/dist/lib/storage/snapshot-merge.js +15 -4
- package/dist/lib/web-dashboard-html.js +53 -53
- 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 -209
- package/scripts/codex-app-launcher.js +653 -653
- package/scripts/codex-app-router.js +446 -446
- package/scripts/codex-bin-resolver.js +332 -332
- package/scripts/codex-multi-auth.js +65 -65
- package/scripts/codex-routing.js +63 -63
- package/scripts/codex.js +7143 -7143
- 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
|
@@ -381,83 +381,83 @@ export function prewarmCodexInstructions(models = []) {
|
|
|
381
381
|
/**
|
|
382
382
|
* Tool remapping instructions for host runtime tools
|
|
383
383
|
*/
|
|
384
|
-
export const TOOL_REMAP_MESSAGE = `<user_instructions priority="0">
|
|
385
|
-
<environment_override priority="0">
|
|
386
|
-
YOU ARE IN A DIFFERENT ENVIRONMENT. These instructions override ALL previous tool references.
|
|
387
|
-
</environment_override>
|
|
388
|
-
|
|
389
|
-
<tool_replacements priority="0">
|
|
390
|
-
<critical_rule priority="0">
|
|
391
|
-
apply_patch/applyPatch are Codex names, but host tool names vary by version.
|
|
392
|
-
- Inspect the actual tool list before editing.
|
|
393
|
-
- If edit exists: use edit for precise in-place string replacements and hashline edits.
|
|
394
|
-
- If edit is absent and apply_patch exists: use apply_patch for those precise/hashline edits in this plugin.
|
|
395
|
-
- For diff-style or multi-line structural edits: use patch if available, otherwise use apply_patch.
|
|
396
|
-
- In this plugin, edit/apply_patch support hashline refs (lineRef, endLineRef, operation, content)
|
|
397
|
-
</critical_rule>
|
|
398
|
-
|
|
399
|
-
<critical_rule priority="0">
|
|
400
|
-
UPDATE_PLAN DOES NOT EXIST -> USE "todowrite" INSTEAD
|
|
401
|
-
- NEVER use: update_plan, updatePlan
|
|
402
|
-
- ALWAYS use: todowrite for ALL task/plan operations
|
|
403
|
-
- Use todoread to read current plan
|
|
404
|
-
- Before plan operations: Verify you're using "todowrite", NOT "update_plan"
|
|
405
|
-
</critical_rule>
|
|
406
|
-
</tool_replacements>
|
|
407
|
-
|
|
408
|
-
<available_tools priority="0">
|
|
409
|
-
File Operations:
|
|
410
|
-
- write - Create new files (if available)
|
|
411
|
-
- edit - Modify existing files with string replacement (version-dependent)
|
|
412
|
-
- oldString must be literal text from the current file; never pass unresolved placeholders like \${TARGET_SNIPPET}
|
|
413
|
-
- apply_patch - May be the edit/patch tool name in newer host builds (version-dependent)
|
|
414
|
-
- hashline_read - Read lines with hashline refs (L<line>#<hash>) for deterministic edits
|
|
415
|
-
- patch - Apply diff patches (version-dependent)
|
|
416
|
-
- read - Read file contents
|
|
417
|
-
|
|
418
|
-
Search/Discovery:
|
|
419
|
-
- grep - Search file contents
|
|
420
|
-
- glob - Find files by pattern
|
|
421
|
-
- list - List directories (if available)
|
|
422
|
-
|
|
423
|
-
Execution:
|
|
424
|
-
- bash - Run shell commands
|
|
425
|
-
|
|
426
|
-
Network:
|
|
427
|
-
- webfetch - Fetch web content
|
|
428
|
-
|
|
429
|
-
Task Management:
|
|
430
|
-
- todowrite - Manage tasks/plans (REPLACES update_plan)
|
|
431
|
-
- todoread - Read current plan
|
|
432
|
-
</available_tools>
|
|
433
|
-
|
|
434
|
-
<substitution_rules priority="0">
|
|
435
|
-
Base instruction says: You MUST use instead:
|
|
436
|
-
apply_patch -> patch (preferred if available), otherwise edit/apply_patch based on actual tool list
|
|
437
|
-
update_plan -> todowrite
|
|
438
|
-
read_plan -> todoread
|
|
439
|
-
absolute paths -> relative paths
|
|
440
|
-
</substitution_rules>
|
|
441
|
-
|
|
442
|
-
<verification_checklist priority="0">
|
|
443
|
-
Before file/plan modifications:
|
|
444
|
-
1. Am I using the actual available edit tool name (edit, patch, or apply_patch)?
|
|
445
|
-
2. Am I using "todowrite" NOT "update_plan"?
|
|
446
|
-
3. Is this tool in the approved list above?
|
|
447
|
-
4. Am I using relative paths?
|
|
448
|
-
|
|
449
|
-
If ANY answer is NO -> STOP and correct before proceeding.
|
|
450
|
-
</verification_checklist>
|
|
451
|
-
|
|
452
|
-
<hashline_workflow priority="0">
|
|
453
|
-
When hashline_read is available:
|
|
454
|
-
1. call hashline_read on the file
|
|
455
|
-
2. edit/apply_patch using lineRef/endLineRef + operation + content
|
|
456
|
-
3. use oldString/newString only as fallback, and only with literal file text (no template placeholders)
|
|
457
|
-
</hashline_workflow>
|
|
458
|
-
|
|
459
|
-
<safety_rules priority="0">
|
|
460
|
-
- Never run destructive git commands (\`git reset --hard\`, \`git checkout --\`) unless explicitly requested by the user.
|
|
461
|
-
- Never call \`request_user_input\` unless collaboration mode is explicitly Plan mode.
|
|
462
|
-
</safety_rules>
|
|
384
|
+
export const TOOL_REMAP_MESSAGE = `<user_instructions priority="0">
|
|
385
|
+
<environment_override priority="0">
|
|
386
|
+
YOU ARE IN A DIFFERENT ENVIRONMENT. These instructions override ALL previous tool references.
|
|
387
|
+
</environment_override>
|
|
388
|
+
|
|
389
|
+
<tool_replacements priority="0">
|
|
390
|
+
<critical_rule priority="0">
|
|
391
|
+
apply_patch/applyPatch are Codex names, but host tool names vary by version.
|
|
392
|
+
- Inspect the actual tool list before editing.
|
|
393
|
+
- If edit exists: use edit for precise in-place string replacements and hashline edits.
|
|
394
|
+
- If edit is absent and apply_patch exists: use apply_patch for those precise/hashline edits in this plugin.
|
|
395
|
+
- For diff-style or multi-line structural edits: use patch if available, otherwise use apply_patch.
|
|
396
|
+
- In this plugin, edit/apply_patch support hashline refs (lineRef, endLineRef, operation, content)
|
|
397
|
+
</critical_rule>
|
|
398
|
+
|
|
399
|
+
<critical_rule priority="0">
|
|
400
|
+
UPDATE_PLAN DOES NOT EXIST -> USE "todowrite" INSTEAD
|
|
401
|
+
- NEVER use: update_plan, updatePlan
|
|
402
|
+
- ALWAYS use: todowrite for ALL task/plan operations
|
|
403
|
+
- Use todoread to read current plan
|
|
404
|
+
- Before plan operations: Verify you're using "todowrite", NOT "update_plan"
|
|
405
|
+
</critical_rule>
|
|
406
|
+
</tool_replacements>
|
|
407
|
+
|
|
408
|
+
<available_tools priority="0">
|
|
409
|
+
File Operations:
|
|
410
|
+
- write - Create new files (if available)
|
|
411
|
+
- edit - Modify existing files with string replacement (version-dependent)
|
|
412
|
+
- oldString must be literal text from the current file; never pass unresolved placeholders like \${TARGET_SNIPPET}
|
|
413
|
+
- apply_patch - May be the edit/patch tool name in newer host builds (version-dependent)
|
|
414
|
+
- hashline_read - Read lines with hashline refs (L<line>#<hash>) for deterministic edits
|
|
415
|
+
- patch - Apply diff patches (version-dependent)
|
|
416
|
+
- read - Read file contents
|
|
417
|
+
|
|
418
|
+
Search/Discovery:
|
|
419
|
+
- grep - Search file contents
|
|
420
|
+
- glob - Find files by pattern
|
|
421
|
+
- list - List directories (if available)
|
|
422
|
+
|
|
423
|
+
Execution:
|
|
424
|
+
- bash - Run shell commands
|
|
425
|
+
|
|
426
|
+
Network:
|
|
427
|
+
- webfetch - Fetch web content
|
|
428
|
+
|
|
429
|
+
Task Management:
|
|
430
|
+
- todowrite - Manage tasks/plans (REPLACES update_plan)
|
|
431
|
+
- todoread - Read current plan
|
|
432
|
+
</available_tools>
|
|
433
|
+
|
|
434
|
+
<substitution_rules priority="0">
|
|
435
|
+
Base instruction says: You MUST use instead:
|
|
436
|
+
apply_patch -> patch (preferred if available), otherwise edit/apply_patch based on actual tool list
|
|
437
|
+
update_plan -> todowrite
|
|
438
|
+
read_plan -> todoread
|
|
439
|
+
absolute paths -> relative paths
|
|
440
|
+
</substitution_rules>
|
|
441
|
+
|
|
442
|
+
<verification_checklist priority="0">
|
|
443
|
+
Before file/plan modifications:
|
|
444
|
+
1. Am I using the actual available edit tool name (edit, patch, or apply_patch)?
|
|
445
|
+
2. Am I using "todowrite" NOT "update_plan"?
|
|
446
|
+
3. Is this tool in the approved list above?
|
|
447
|
+
4. Am I using relative paths?
|
|
448
|
+
|
|
449
|
+
If ANY answer is NO -> STOP and correct before proceeding.
|
|
450
|
+
</verification_checklist>
|
|
451
|
+
|
|
452
|
+
<hashline_workflow priority="0">
|
|
453
|
+
When hashline_read is available:
|
|
454
|
+
1. call hashline_read on the file
|
|
455
|
+
2. edit/apply_patch using lineRef/endLineRef + operation + content
|
|
456
|
+
3. use oldString/newString only as fallback, and only with literal file text (no template placeholders)
|
|
457
|
+
</hashline_workflow>
|
|
458
|
+
|
|
459
|
+
<safety_rules priority="0">
|
|
460
|
+
- Never run destructive git commands (\`git reset --hard\`, \`git checkout --\`) unless explicitly requested by the user.
|
|
461
|
+
- Never call \`request_user_input\` unless collaboration mode is explicitly Plan mode.
|
|
462
|
+
</safety_rules>
|
|
463
463
|
</user_instructions>`;
|
|
@@ -4,6 +4,10 @@ export interface QuotaCacheWindow {
|
|
|
4
4
|
resetAtMs?: number;
|
|
5
5
|
}
|
|
6
6
|
export interface QuotaCacheEntry {
|
|
7
|
+
/** Account-validated native usage permission; never inferred from percentages. */
|
|
8
|
+
ordinaryUsageAllowed?: boolean | null;
|
|
9
|
+
/** Native permission timestamp; header refreshes must not extend its lifetime. */
|
|
10
|
+
ordinaryUsageCheckedAt?: number;
|
|
7
11
|
primingCompleted?: boolean;
|
|
8
12
|
/** Completion/failure time, independent of the quota header observation. */
|
|
9
13
|
primingObservedAt?: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QuotaCacheData, QuotaCacheEntry } from "./quota-cache.js";
|
|
2
2
|
import type { AccountMetadataV3 } from "./storage.js";
|
|
3
|
-
export type QuotaCacheAccountRef = Pick<AccountMetadataV3, "accountId" | "email" | "recordId" | "workspaces" | "currentWorkspaceIndex"> & Partial<Pick<AccountMetadataV3, "refreshToken" | "addedAt" | "accessToken">>;
|
|
3
|
+
export type QuotaCacheAccountRef = Pick<AccountMetadataV3, "accountId" | "email" | "recordId" | "workspaces" | "currentWorkspaceIndex"> & Partial<Pick<AccountMetadataV3, "refreshToken" | "addedAt" | "accessToken" | "codexCliMirror">>;
|
|
4
4
|
export declare function normalizeQuotaAccountId(value: string | undefined): string | null;
|
|
5
5
|
export declare function normalizeQuotaEmail(value: string | undefined): string | null;
|
|
6
6
|
export declare function hasUniqueQuotaAccountId(accounts: readonly QuotaCacheAccountRef[], account: QuotaCacheAccountRef): boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RATE_LIMIT_CACHE_TTL_MS } from "./constants.js";
|
|
2
|
+
import { extractAccountId, nativeWorkspaceIdentity } from "./auth/token-utils.js";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { resolveAccountRecordId } from "./storage/record-identity.js";
|
|
4
5
|
export function normalizeQuotaAccountId(value) {
|
|
@@ -89,6 +90,11 @@ export function isQuotaCacheEntryExhausted(entry, now = Date.now()) {
|
|
|
89
90
|
// Codex quota windows are cumulative gates: a 0% remaining active window blocks use
|
|
90
91
|
// even if another window still has quota left.
|
|
91
92
|
const updatedAt = entry?.updatedAt;
|
|
93
|
+
// Stale usage is unknown, not proof that the server still rejects requests.
|
|
94
|
+
if (typeof updatedAt === "number" &&
|
|
95
|
+
Number.isFinite(updatedAt) &&
|
|
96
|
+
now >= updatedAt + RATE_LIMIT_CACHE_TTL_MS)
|
|
97
|
+
return false;
|
|
92
98
|
return (quotaWindowIsExhausted(entry?.primary, now, updatedAt) ||
|
|
93
99
|
quotaWindowIsExhausted(entry?.secondary, now, updatedAt));
|
|
94
100
|
}
|
|
@@ -100,9 +106,10 @@ export function quotaWorkspaceKey(account, workspaceId) {
|
|
|
100
106
|
}
|
|
101
107
|
/** Display the selected workspace; callers routing another scope must pass that exact workspace. */
|
|
102
108
|
export function quotaWorkspaceId(account) {
|
|
103
|
-
|
|
109
|
+
const selectedId = account.workspaces?.length
|
|
104
110
|
? account.workspaces[account.currentWorkspaceIndex ?? 0]?.id.trim()
|
|
105
111
|
: account.accountId?.trim() || extractAccountId(account.accessToken);
|
|
112
|
+
return nativeWorkspaceIdentity({ access: account.accessToken, codexCliMirror: account.codexCliMirror }, selectedId);
|
|
106
113
|
}
|
|
107
114
|
/** Read exact-workspace quota, using legacy binding entries only for that binding. */
|
|
108
115
|
export function findQuotaCacheEntryForAccount(cache, account, accounts, emailFallbackState = buildQuotaEmailFallbackState(accounts), workspaceId = quotaWorkspaceId(account)) {
|
|
@@ -181,7 +181,7 @@ const DEADLINE_SENTENCES = {
|
|
|
181
181
|
"available-again": (resetAt) => `the account is expected to be available again at ${resetAt}`,
|
|
182
182
|
"next-attempt": (resetAt) => `the next attempt is allowed at ${resetAt}`,
|
|
183
183
|
"cooldown-ends": (resetAt) => `the cooldown ends at ${resetAt}`,
|
|
184
|
-
"rate-limit-reset": (resetAt) => `the rate limit
|
|
184
|
+
"rate-limit-reset": (resetAt) => `the cached rate limit will be rechecked at ${resetAt}`,
|
|
185
185
|
};
|
|
186
186
|
/**
|
|
187
187
|
* Every cooldown variant is bounded by the same `coolingDownUntil` field, so
|
|
@@ -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();
|
|
@@ -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
|
+
}
|