@cortexkit/anthropic-auth-core 1.19.1 → 1.21.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/README.md +1 -1
- package/dist/accounts.d.ts +81 -5
- package/dist/accounts.js +531 -81
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +3 -2
- package/dist/cache1h.d.ts +1 -1
- package/dist/cache1h.js +1 -1
- package/dist/cachekeep.d.ts +20 -2
- package/dist/cachekeep.js +134 -28
- package/dist/cch.d.ts +1 -0
- package/dist/cch.js +6 -1
- package/dist/claude-code.d.ts +5 -2
- package/dist/claude-code.js +127 -24
- package/dist/commands/account.js +1 -1
- package/dist/constants.d.ts +3 -3
- package/dist/dump.d.ts +13 -1
- package/dist/dump.js +171 -22
- package/dist/index.d.ts +3 -0
- package/dist/index.js +27 -24
- package/dist/killswitch.js +1 -1
- package/dist/models.d.ts +22 -6
- package/dist/models.js +16 -0
- package/dist/oauth-profile.d.ts +2 -1
- package/dist/oauth-profile.js +10 -4
- package/dist/prime.d.ts +219 -0
- package/dist/prime.js +823 -0
- package/dist/quota-header-feed.d.ts +40 -0
- package/dist/quota-header-feed.js +248 -0
- package/dist/quota-headers.js +6 -4
- package/dist/quota-manager.d.ts +41 -25
- package/dist/quota-manager.js +391 -151
- package/dist/quotas.d.ts +4 -4
- package/dist/quotas.js +2 -2
- package/dist/relay.d.ts +2 -0
- package/dist/relay.js +10 -4
- package/dist/routing.d.ts +1 -1
- package/dist/routing.js +1 -1
- package/dist/start.d.ts +13 -0
- package/dist/start.js +29 -0
- package/dist/sticky-routing.d.ts +15 -2
- package/dist/sticky-routing.js +44 -17
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -2,10 +2,9 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import { parseRetryAfterHeader, refreshClaudeOAuthToken } from
|
|
6
|
-
import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from
|
|
7
|
-
import { log, logger } from
|
|
8
|
-
import { tokenFingerprint } from "./token-fingerprint.js";
|
|
5
|
+
import { parseRetryAfterHeader, refreshClaudeOAuthToken } from './auth.js';
|
|
6
|
+
import { CACHE_1H_MODES, CLAUDE_CODE_VERSION, DEFAULT_CACHE_1H_MODE, } from './constants.js';
|
|
7
|
+
import { log, logger } from './logger.js';
|
|
9
8
|
const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
|
|
10
9
|
const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
|
|
11
10
|
export const ACCOUNT_FILE_NAME = 'anthropic-auth.json';
|
|
@@ -116,6 +115,9 @@ function normalizeAccount(value) {
|
|
|
116
115
|
return {
|
|
117
116
|
...normalizeAccountBase(value),
|
|
118
117
|
type: 'oauth',
|
|
118
|
+
authLineageId: typeof value.authLineageId === 'string' && value.authLineageId.trim()
|
|
119
|
+
? value.authLineageId
|
|
120
|
+
: undefined,
|
|
119
121
|
access: typeof value.access === 'string' ? value.access : undefined,
|
|
120
122
|
refresh: value.refresh,
|
|
121
123
|
expires: typeof value.expires === 'number' ? value.expires : undefined,
|
|
@@ -126,6 +128,7 @@ function normalizeAccount(value) {
|
|
|
126
128
|
lastQuotaRefreshError: normalizeOperationError(value.lastQuotaRefreshError),
|
|
127
129
|
quota: normalizeQuota(value.quota),
|
|
128
130
|
profile: normalizeOAuthAccountProfile(value.profile),
|
|
131
|
+
prime: normalizePrimeUsageCounters(value.prime),
|
|
129
132
|
};
|
|
130
133
|
}
|
|
131
134
|
function normalizeOAuthAccountProfile(value) {
|
|
@@ -143,6 +146,10 @@ function normalizeOAuthAccountProfile(value) {
|
|
|
143
146
|
tier: value.tier.trim(),
|
|
144
147
|
orgType: value.orgType.trim(),
|
|
145
148
|
checkedAt: value.checkedAt,
|
|
149
|
+
...(typeof value.accountIdentity === 'string' &&
|
|
150
|
+
value.accountIdentity.trim() && {
|
|
151
|
+
accountIdentity: value.accountIdentity.trim(),
|
|
152
|
+
}),
|
|
146
153
|
...(typeof value.tokenFingerprint === 'string' &&
|
|
147
154
|
value.tokenFingerprint.trim() && {
|
|
148
155
|
tokenFingerprint: value.tokenFingerprint.trim(),
|
|
@@ -165,6 +172,9 @@ function normalizeOperationError(value) {
|
|
|
165
172
|
checkedAt,
|
|
166
173
|
nextRetryAt: Number.isFinite(nextRetryAt) ? nextRetryAt : undefined,
|
|
167
174
|
retryCount: Number.isFinite(retryCount) ? retryCount : undefined,
|
|
175
|
+
accountIdentity: typeof value.accountIdentity === 'string' && value.accountIdentity.trim()
|
|
176
|
+
? value.accountIdentity.trim()
|
|
177
|
+
: undefined,
|
|
168
178
|
tokenHash: typeof value.tokenHash === 'string' ? value.tokenHash : undefined,
|
|
169
179
|
// Preserve the dead-token discriminators across save/load. Without these,
|
|
170
180
|
// a retry-exhausted transient (permanent=false, 24h backoff) would lose its
|
|
@@ -192,6 +202,27 @@ function normalizeQuotaWindow(value) {
|
|
|
192
202
|
resetsAt: typeof value.resetsAt === 'string' ? value.resetsAt : undefined,
|
|
193
203
|
};
|
|
194
204
|
}
|
|
205
|
+
function normalizePrimeUsageCounters(value) {
|
|
206
|
+
if (!isRecord(value))
|
|
207
|
+
return undefined;
|
|
208
|
+
const count = Number(value.count);
|
|
209
|
+
const inputTokens = Number(value.inputTokens);
|
|
210
|
+
const outputTokens = Number(value.outputTokens);
|
|
211
|
+
const since = Number(value.since);
|
|
212
|
+
if (![count, inputTokens, outputTokens, since].every(Number.isFinite) ||
|
|
213
|
+
count < 0 ||
|
|
214
|
+
inputTokens < 0 ||
|
|
215
|
+
outputTokens < 0 ||
|
|
216
|
+
since < 0) {
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
count: Math.floor(count),
|
|
221
|
+
inputTokens: Math.floor(inputTokens),
|
|
222
|
+
outputTokens: Math.floor(outputTokens),
|
|
223
|
+
since: Math.floor(since),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
195
226
|
function normalizeQuota(value) {
|
|
196
227
|
if (!isRecord(value))
|
|
197
228
|
return undefined;
|
|
@@ -209,6 +240,10 @@ function normalizeQuota(value) {
|
|
|
209
240
|
if (typeof value.checkedAt === 'number' && Number.isFinite(value.checkedAt)) {
|
|
210
241
|
quota.checkedAt = value.checkedAt;
|
|
211
242
|
}
|
|
243
|
+
if (typeof value.accountIdentity === 'string' &&
|
|
244
|
+
value.accountIdentity.trim()) {
|
|
245
|
+
quota.accountIdentity = value.accountIdentity.trim();
|
|
246
|
+
}
|
|
212
247
|
if (Array.isArray(value.scoped)) {
|
|
213
248
|
const scoped = value.scoped
|
|
214
249
|
.map((entry) => {
|
|
@@ -307,6 +342,9 @@ function normalizeStorage(value) {
|
|
|
307
342
|
return null;
|
|
308
343
|
return {
|
|
309
344
|
version: 1,
|
|
345
|
+
mainAccountId: typeof value.mainAccountId === 'string' && value.mainAccountId.trim()
|
|
346
|
+
? value.mainAccountId.trim()
|
|
347
|
+
: undefined,
|
|
310
348
|
main: {
|
|
311
349
|
type: 'opencode',
|
|
312
350
|
provider: 'anthropic',
|
|
@@ -318,6 +356,9 @@ function normalizeStorage(value) {
|
|
|
318
356
|
: undefined,
|
|
319
357
|
refresh: isRecord(value.refresh) ? value.refresh : undefined,
|
|
320
358
|
quota: isRecord(value.quota) ? value.quota : undefined,
|
|
359
|
+
quotaHeaderFeed: isRecord(value.quotaHeaderFeed)
|
|
360
|
+
? value.quotaHeaderFeed
|
|
361
|
+
: undefined,
|
|
321
362
|
claudeCache: isRecord(value.claudeCache) ? value.claudeCache : undefined,
|
|
322
363
|
dump: isRecord(value.dump) ? value.dump : undefined,
|
|
323
364
|
claudeFast: isRecord(value.claudeFast) ? value.claudeFast : undefined,
|
|
@@ -326,6 +367,35 @@ function normalizeStorage(value) {
|
|
|
326
367
|
relay: isRecord(value.relay) ? value.relay : undefined,
|
|
327
368
|
logging: isRecord(value.logging) ? value.logging : undefined,
|
|
328
369
|
killswitch: isRecord(value.killswitch) ? value.killswitch : undefined,
|
|
370
|
+
prime: (() => {
|
|
371
|
+
if (!isRecord(value.prime))
|
|
372
|
+
return undefined;
|
|
373
|
+
const enabled = typeof value.prime.enabled === 'boolean'
|
|
374
|
+
? value.prime.enabled
|
|
375
|
+
: undefined;
|
|
376
|
+
const main = normalizePrimeUsageCounters(value.prime.main);
|
|
377
|
+
const mainAuthLineageId = typeof value.prime.mainAuthLineageId === 'string' &&
|
|
378
|
+
value.prime.mainAuthLineageId.trim()
|
|
379
|
+
? value.prime.mainAuthLineageId
|
|
380
|
+
: undefined;
|
|
381
|
+
const mainAuthLineageRefreshTokenFingerprint = typeof value.prime.mainAuthLineageRefreshTokenFingerprint ===
|
|
382
|
+
'string' && value.prime.mainAuthLineageRefreshTokenFingerprint.trim()
|
|
383
|
+
? value.prime.mainAuthLineageRefreshTokenFingerprint
|
|
384
|
+
: undefined;
|
|
385
|
+
if (enabled === undefined &&
|
|
386
|
+
!main &&
|
|
387
|
+
!mainAuthLineageId &&
|
|
388
|
+
!mainAuthLineageRefreshTokenFingerprint)
|
|
389
|
+
return undefined;
|
|
390
|
+
return {
|
|
391
|
+
...(enabled !== undefined && { enabled }),
|
|
392
|
+
...(main && { main }),
|
|
393
|
+
...(mainAuthLineageId && { mainAuthLineageId }),
|
|
394
|
+
...(mainAuthLineageRefreshTokenFingerprint && {
|
|
395
|
+
mainAuthLineageRefreshTokenFingerprint,
|
|
396
|
+
}),
|
|
397
|
+
};
|
|
398
|
+
})(),
|
|
329
399
|
accounts: value.accounts
|
|
330
400
|
.map(normalizeAccount)
|
|
331
401
|
.filter((account) => account != null),
|
|
@@ -349,6 +419,14 @@ function objectWithDefinedEntries(value) {
|
|
|
349
419
|
function numericField(value) {
|
|
350
420
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
351
421
|
}
|
|
422
|
+
// A clear is a monotonic tombstone; older or absent writes must not reopen backoff.
|
|
423
|
+
export function mergeMainQuotaErrorClearedAt(existing, incoming) {
|
|
424
|
+
if (incoming === undefined || !Number.isFinite(incoming))
|
|
425
|
+
return existing;
|
|
426
|
+
if (existing === undefined || !Number.isFinite(existing))
|
|
427
|
+
return incoming;
|
|
428
|
+
return Math.max(existing, incoming);
|
|
429
|
+
}
|
|
352
430
|
function accountCredentialTimestamp(value) {
|
|
353
431
|
return Math.max(numericField(value.lastRefreshedAt), numericField(value.lastUsed), numericField(value.addedAt));
|
|
354
432
|
}
|
|
@@ -393,6 +471,19 @@ function mergeConfigAndState(configValue, stateValue) {
|
|
|
393
471
|
const refreshConfig = isRecord(configValue.refresh) ? configValue.refresh : {};
|
|
394
472
|
const mainQuotaSource = mainState ?? quotaConfig;
|
|
395
473
|
const mainRefreshSource = mainState ?? refreshConfig;
|
|
474
|
+
const mainQuotaErrorClearedAt = typeof mainState?.quotaErrorClearedAt === 'number' &&
|
|
475
|
+
Number.isFinite(mainState.quotaErrorClearedAt) &&
|
|
476
|
+
mainState.quotaErrorClearedAt >= 0
|
|
477
|
+
? mainState.quotaErrorClearedAt
|
|
478
|
+
: undefined;
|
|
479
|
+
const configQuotaError = quotaConfig.mainLastQuotaApiError;
|
|
480
|
+
const mainLastQuotaApiError = mainState?.lastQuotaApiError ??
|
|
481
|
+
(mainQuotaErrorClearedAt !== undefined &&
|
|
482
|
+
isRecord(configQuotaError) &&
|
|
483
|
+
typeof configQuotaError.checkedAt === 'number' &&
|
|
484
|
+
configQuotaError.checkedAt <= mainQuotaErrorClearedAt
|
|
485
|
+
? undefined
|
|
486
|
+
: configQuotaError);
|
|
396
487
|
const accounts = Array.isArray(configValue.accounts)
|
|
397
488
|
? configValue.accounts.map((account) => {
|
|
398
489
|
if (!isRecord(account))
|
|
@@ -422,8 +513,44 @@ function mergeConfigAndState(configValue, stateValue) {
|
|
|
422
513
|
mainQuota: mainQuotaSource.quota,
|
|
423
514
|
mainQuotaCheckedAt: mainQuotaSource.quotaCheckedAt,
|
|
424
515
|
mainQuotaToken: mainQuotaSource.quotaToken,
|
|
425
|
-
mainLastQuotaApiError
|
|
516
|
+
mainLastQuotaApiError,
|
|
517
|
+
mainQuotaErrorGeneration: mainQuotaSource.quotaErrorGeneration,
|
|
518
|
+
mainQuotaErrorClearedAt: mainQuotaSource.quotaErrorClearedAt,
|
|
426
519
|
}),
|
|
520
|
+
// Carry the main-side prime counters from the state file back into the
|
|
521
|
+
// merged storage so a subsequent read sees the cumulative counters. The
|
|
522
|
+
// `enabled` flag stays sourced from the config side; main-side counters
|
|
523
|
+
// live exclusively on the state file.
|
|
524
|
+
prime: (() => {
|
|
525
|
+
const configPrime = isRecord(configValue.prime)
|
|
526
|
+
? configValue.prime
|
|
527
|
+
: undefined;
|
|
528
|
+
const mainCounters = normalizePrimeUsageCounters(mainState?.prime);
|
|
529
|
+
const mainAuthLineageId = typeof mainState?.primeAuthLineageId === 'string' &&
|
|
530
|
+
mainState.primeAuthLineageId.trim()
|
|
531
|
+
? mainState.primeAuthLineageId
|
|
532
|
+
: undefined;
|
|
533
|
+
const mainAuthLineageRefreshTokenFingerprint = typeof mainState?.primeAuthLineageRefreshTokenFingerprint ===
|
|
534
|
+
'string' && mainState.primeAuthLineageRefreshTokenFingerprint.trim()
|
|
535
|
+
? mainState.primeAuthLineageRefreshTokenFingerprint
|
|
536
|
+
: undefined;
|
|
537
|
+
if (!configPrime &&
|
|
538
|
+
!mainCounters &&
|
|
539
|
+
!mainAuthLineageId &&
|
|
540
|
+
!mainAuthLineageRefreshTokenFingerprint)
|
|
541
|
+
return undefined;
|
|
542
|
+
return {
|
|
543
|
+
...(configPrime &&
|
|
544
|
+
typeof configPrime.enabled === 'boolean' && {
|
|
545
|
+
enabled: configPrime.enabled,
|
|
546
|
+
}),
|
|
547
|
+
...(mainCounters && { main: mainCounters }),
|
|
548
|
+
...(mainAuthLineageId && { mainAuthLineageId }),
|
|
549
|
+
...(mainAuthLineageRefreshTokenFingerprint && {
|
|
550
|
+
mainAuthLineageRefreshTokenFingerprint,
|
|
551
|
+
}),
|
|
552
|
+
};
|
|
553
|
+
})(),
|
|
427
554
|
accounts,
|
|
428
555
|
};
|
|
429
556
|
}
|
|
@@ -464,6 +591,7 @@ function accountRuntimeState(account) {
|
|
|
464
591
|
});
|
|
465
592
|
}
|
|
466
593
|
return objectWithDefinedEntries({
|
|
594
|
+
authLineageId: account.authLineageId,
|
|
467
595
|
access: account.access,
|
|
468
596
|
refresh: account.refresh,
|
|
469
597
|
expires: account.expires,
|
|
@@ -473,9 +601,11 @@ function accountRuntimeState(account) {
|
|
|
473
601
|
lastQuotaRefreshError: account.lastQuotaRefreshError,
|
|
474
602
|
quota: account.quota,
|
|
475
603
|
profile: account.profile,
|
|
604
|
+
prime: account.prime,
|
|
476
605
|
});
|
|
477
606
|
}
|
|
478
|
-
|
|
607
|
+
/** Returns the latest timestamp carried by any quota snapshot window. */
|
|
608
|
+
export function quotaSnapshotCheckedAt(quota) {
|
|
479
609
|
return Math.max(quota?.five_hour?.checkedAt ?? 0, quota?.seven_day?.checkedAt ?? 0, ...(quota?.scoped?.map((window) => window.checkedAt) ?? []), quota?.checkedAt ?? 0);
|
|
480
610
|
}
|
|
481
611
|
function quotaSourcePrecedence(quota) {
|
|
@@ -519,7 +649,7 @@ function mergeHeaderOwnedWindow(existingSnapshot, incomingSnapshot, key) {
|
|
|
519
649
|
? existing
|
|
520
650
|
: incoming;
|
|
521
651
|
}
|
|
522
|
-
function mergeHeaderQuotaForPersistence(existing, incoming) {
|
|
652
|
+
export function mergeHeaderQuotaForPersistence(existing, incoming) {
|
|
523
653
|
if (!existing || incoming.source !== 'headers')
|
|
524
654
|
return incoming;
|
|
525
655
|
const preservePollBinding = existing.bindingWindowSource === 'poll';
|
|
@@ -639,11 +769,13 @@ function configFromStorage(storage) {
|
|
|
639
769
|
: undefined;
|
|
640
770
|
return omitUndefinedTopLevel({
|
|
641
771
|
version: 1,
|
|
772
|
+
mainAccountId: storage.mainAccountId,
|
|
642
773
|
main: { type: 'opencode', provider: 'anthropic' },
|
|
643
774
|
routing: storage.routing,
|
|
644
775
|
fallbackOn: storage.fallbackOn,
|
|
645
776
|
refresh,
|
|
646
777
|
quota,
|
|
778
|
+
quotaHeaderFeed: storage.quotaHeaderFeed,
|
|
647
779
|
claudeCache: storage.claudeCache,
|
|
648
780
|
dump: storage.dump,
|
|
649
781
|
logging: storage.logging,
|
|
@@ -652,6 +784,15 @@ function configFromStorage(storage) {
|
|
|
652
784
|
cacheKeep: storage.cacheKeep,
|
|
653
785
|
relay: storage.relay,
|
|
654
786
|
killswitch: storage.killswitch,
|
|
787
|
+
prime: (() => {
|
|
788
|
+
// Config side carries ONLY the `enabled` flag — runtime counters and
|
|
789
|
+
// lineage bindings stay on the state file. Write `enabled` whenever it was explicitly set so a
|
|
790
|
+
// toggle off persists `{ enabled: false }` and is visible to a stale
|
|
791
|
+
// reader that only inspects the config.
|
|
792
|
+
if (typeof storage.prime?.enabled !== 'boolean')
|
|
793
|
+
return undefined;
|
|
794
|
+
return { enabled: storage.prime.enabled };
|
|
795
|
+
})(),
|
|
655
796
|
accounts: storage.accounts.map(accountConfig),
|
|
656
797
|
});
|
|
657
798
|
}
|
|
@@ -757,6 +898,27 @@ export function saveAccounts(storage, path = getAccountStoragePath(), options =
|
|
|
757
898
|
const resolvedPath = path;
|
|
758
899
|
return enqueueSave(() => saveAccountsLocked(storage, resolvedPath, options));
|
|
759
900
|
}
|
|
901
|
+
export async function getOrCreateMainAccountId(path = getAccountStoragePath(), createId = randomUUID) {
|
|
902
|
+
return enqueueSave(async () => {
|
|
903
|
+
const lock = await acquireAccountConfigWriteLock(path);
|
|
904
|
+
try {
|
|
905
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
906
|
+
if (storage.mainAccountId)
|
|
907
|
+
return storage.mainAccountId;
|
|
908
|
+
const mainAccountId = createId();
|
|
909
|
+
const nextStorage = { ...storage, mainAccountId };
|
|
910
|
+
const existing = await loadExistingTopLevelFields(path);
|
|
911
|
+
await writeJsonAtomic(path, {
|
|
912
|
+
...existing,
|
|
913
|
+
...configFromStorage(nextStorage),
|
|
914
|
+
});
|
|
915
|
+
return mainAccountId;
|
|
916
|
+
}
|
|
917
|
+
finally {
|
|
918
|
+
await lock.release();
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
}
|
|
760
922
|
async function saveAccountsLocked(storage, path, options) {
|
|
761
923
|
const lock = await acquireAccountConfigWriteLock(path);
|
|
762
924
|
try {
|
|
@@ -792,6 +954,74 @@ function applyMainProfileStatePatch(state, storage) {
|
|
|
792
954
|
}
|
|
793
955
|
function applyMainQuotaStatePatch(state, storage) {
|
|
794
956
|
state.main = state.main ?? {};
|
|
957
|
+
const incomingError = storage.quota?.mainLastQuotaApiError;
|
|
958
|
+
const incomingGeneration = typeof storage.quota?.mainQuotaErrorGeneration === 'number' &&
|
|
959
|
+
Number.isSafeInteger(storage.quota.mainQuotaErrorGeneration) &&
|
|
960
|
+
storage.quota.mainQuotaErrorGeneration >= 0
|
|
961
|
+
? storage.quota.mainQuotaErrorGeneration
|
|
962
|
+
: undefined;
|
|
963
|
+
const incomingClearedAt = typeof storage.quota?.mainQuotaErrorClearedAt === 'number' &&
|
|
964
|
+
Number.isFinite(storage.quota.mainQuotaErrorClearedAt) &&
|
|
965
|
+
storage.quota.mainQuotaErrorClearedAt >= 0
|
|
966
|
+
? storage.quota.mainQuotaErrorClearedAt
|
|
967
|
+
: undefined;
|
|
968
|
+
const existingGeneration = typeof state.main.quotaErrorGeneration === 'number' &&
|
|
969
|
+
Number.isSafeInteger(state.main.quotaErrorGeneration) &&
|
|
970
|
+
state.main.quotaErrorGeneration >= 0
|
|
971
|
+
? state.main.quotaErrorGeneration
|
|
972
|
+
: 0;
|
|
973
|
+
const existingErrorObservedAt = typeof state.main.lastQuotaApiError?.checkedAt === 'number' &&
|
|
974
|
+
Number.isFinite(state.main.lastQuotaApiError.checkedAt) &&
|
|
975
|
+
state.main.lastQuotaApiError.checkedAt >= 0
|
|
976
|
+
? state.main.lastQuotaApiError.checkedAt
|
|
977
|
+
: undefined;
|
|
978
|
+
const existingClearedAt = typeof state.main.quotaErrorClearedAt === 'number' &&
|
|
979
|
+
Number.isFinite(state.main.quotaErrorClearedAt) &&
|
|
980
|
+
state.main.quotaErrorClearedAt >= 0
|
|
981
|
+
? state.main.quotaErrorClearedAt
|
|
982
|
+
: undefined;
|
|
983
|
+
const existingObservedAt = Math.max(existingErrorObservedAt ?? 0, existingClearedAt ?? 0);
|
|
984
|
+
const incomingErrorObservedAt = typeof incomingError?.checkedAt === 'number' &&
|
|
985
|
+
Number.isFinite(incomingError.checkedAt) &&
|
|
986
|
+
incomingError.checkedAt >= 0
|
|
987
|
+
? incomingError.checkedAt
|
|
988
|
+
: undefined;
|
|
989
|
+
// Error transitions are ordered by observation time, not write order. All
|
|
990
|
+
// instances share one host clock, so an error observed before a clear cannot
|
|
991
|
+
// resurrect that clear even when both processes allocated the same generation.
|
|
992
|
+
if (incomingError === undefined &&
|
|
993
|
+
incomingClearedAt !== undefined &&
|
|
994
|
+
incomingClearedAt >= existingObservedAt) {
|
|
995
|
+
state.main.lastQuotaApiError = undefined;
|
|
996
|
+
state.main.quotaErrorClearedAt = mergeMainQuotaErrorClearedAt(existingClearedAt, incomingClearedAt);
|
|
997
|
+
if (incomingGeneration !== undefined &&
|
|
998
|
+
incomingGeneration >= existingGeneration) {
|
|
999
|
+
state.main.quotaErrorGeneration = incomingGeneration;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
else if (incomingError) {
|
|
1003
|
+
const acceptsByObservation = incomingErrorObservedAt !== undefined &&
|
|
1004
|
+
incomingErrorObservedAt > existingObservedAt;
|
|
1005
|
+
const acceptsEqualObservation = incomingErrorObservedAt !== undefined &&
|
|
1006
|
+
incomingErrorObservedAt === existingObservedAt &&
|
|
1007
|
+
existingErrorObservedAt !== undefined &&
|
|
1008
|
+
existingClearedAt === undefined &&
|
|
1009
|
+
incomingGeneration !== undefined &&
|
|
1010
|
+
incomingGeneration >= existingGeneration;
|
|
1011
|
+
const acceptsLegacyGeneration = incomingErrorObservedAt === undefined &&
|
|
1012
|
+
incomingClearedAt === undefined &&
|
|
1013
|
+
incomingGeneration !== undefined &&
|
|
1014
|
+
incomingGeneration >= existingGeneration &&
|
|
1015
|
+
existingClearedAt === undefined;
|
|
1016
|
+
if (acceptsByObservation ||
|
|
1017
|
+
acceptsEqualObservation ||
|
|
1018
|
+
acceptsLegacyGeneration) {
|
|
1019
|
+
state.main.lastQuotaApiError = incomingError;
|
|
1020
|
+
if (incomingGeneration !== undefined) {
|
|
1021
|
+
state.main.quotaErrorGeneration = Math.max(existingGeneration, incomingGeneration);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
795
1025
|
const incomingQuota = storage.quota?.mainQuota;
|
|
796
1026
|
const sameToken = Boolean(state.main.quotaToken &&
|
|
797
1027
|
storage.quota?.mainQuotaToken &&
|
|
@@ -818,7 +1048,6 @@ function applyMainQuotaStatePatch(state, storage) {
|
|
|
818
1048
|
? Math.max(existingCheckedAt, incomingCheckedAt)
|
|
819
1049
|
: storage.quota?.mainQuotaCheckedAt;
|
|
820
1050
|
state.main.quotaToken = storage.quota?.mainQuotaToken;
|
|
821
|
-
state.main.lastQuotaApiError = storage.quota?.mainLastQuotaApiError;
|
|
822
1051
|
}
|
|
823
1052
|
function applyMainRefreshStatePatch(state, storage) {
|
|
824
1053
|
state.main = state.main ?? {};
|
|
@@ -827,6 +1056,29 @@ function applyMainRefreshStatePatch(state, storage) {
|
|
|
827
1056
|
state.main.refreshLeaseUntil = storage.refresh?.mainRefreshLeaseUntil;
|
|
828
1057
|
state.main.refreshLeaseTokenHash = storage.refresh?.mainRefreshLeaseTokenHash;
|
|
829
1058
|
}
|
|
1059
|
+
function applyMainPrimeStatePatch(state, storage) {
|
|
1060
|
+
const incoming = storage.prime?.main;
|
|
1061
|
+
const incomingAuthLineageId = storage.prime?.mainAuthLineageId;
|
|
1062
|
+
const incomingAuthLineageRefreshTokenFingerprint = storage.prime?.mainAuthLineageRefreshTokenFingerprint;
|
|
1063
|
+
if (!incoming &&
|
|
1064
|
+
!incomingAuthLineageId &&
|
|
1065
|
+
!incomingAuthLineageRefreshTokenFingerprint)
|
|
1066
|
+
return;
|
|
1067
|
+
state.main = state.main ?? {};
|
|
1068
|
+
// Last-writer-wins on prime counters — the only writer is the prime manager
|
|
1069
|
+
// itself, monotonically accumulating per success, so there is no race for an
|
|
1070
|
+
// older write to overwrite a newer one within this process. Across processes
|
|
1071
|
+
// the cross-process claim marker (#1247) keeps the fire exclusive.
|
|
1072
|
+
if (incoming)
|
|
1073
|
+
state.main.prime = incoming;
|
|
1074
|
+
if (incomingAuthLineageId) {
|
|
1075
|
+
state.main.primeAuthLineageId = incomingAuthLineageId;
|
|
1076
|
+
}
|
|
1077
|
+
if (incomingAuthLineageRefreshTokenFingerprint) {
|
|
1078
|
+
state.main.primeAuthLineageRefreshTokenFingerprint =
|
|
1079
|
+
incomingAuthLineageRefreshTokenFingerprint;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
830
1082
|
function pruneUndefined(value) {
|
|
831
1083
|
if (Array.isArray(value))
|
|
832
1084
|
return value.map(pruneUndefined);
|
|
@@ -866,38 +1118,41 @@ export function saveOAuthProfileState(input, path = getAccountStoragePath()) {
|
|
|
866
1118
|
const next = isRecord(existing)
|
|
867
1119
|
? { ...existing, version: 1 }
|
|
868
1120
|
: { version: 1 };
|
|
869
|
-
const { accountId,
|
|
870
|
-
if (profile?.tokenFingerprint &&
|
|
871
|
-
profile.tokenFingerprint !== expectedTokenFingerprint) {
|
|
872
|
-
return false;
|
|
873
|
-
}
|
|
1121
|
+
const { accountId, accountIdentity, profile } = input;
|
|
874
1122
|
if (accountId === 'main') {
|
|
875
1123
|
next.main = { ...(next.main ?? {}) };
|
|
876
1124
|
const existingProfile = normalizeOAuthAccountProfile(next.main.profile);
|
|
877
|
-
|
|
878
|
-
if (!profile &&
|
|
879
|
-
existingProfile &&
|
|
880
|
-
persistedToken === expectedTokenFingerprint) {
|
|
881
|
-
return false;
|
|
882
|
-
}
|
|
883
|
-
if (profile) {
|
|
884
|
-
if (persistedToken && persistedToken !== expectedTokenFingerprint) {
|
|
885
|
-
return false;
|
|
886
|
-
}
|
|
1125
|
+
if (!profile) {
|
|
887
1126
|
if (existingProfile &&
|
|
888
|
-
|
|
1127
|
+
(accountIdentity === undefined ||
|
|
1128
|
+
existingProfile.accountIdentity === undefined ||
|
|
1129
|
+
existingProfile.accountIdentity === accountIdentity)) {
|
|
889
1130
|
return false;
|
|
890
1131
|
}
|
|
1132
|
+
next.main.profile = undefined;
|
|
1133
|
+
return await writeJsonAtomic(statePath, pruneUndefined(next)).then(() => true);
|
|
891
1134
|
}
|
|
892
|
-
|
|
893
|
-
|
|
1135
|
+
if (profile.accountIdentity !== undefined &&
|
|
1136
|
+
profile.accountIdentity !== accountIdentity) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
const persistedProfile = accountIdentity === undefined ||
|
|
1140
|
+
profile.accountIdentity === accountIdentity
|
|
1141
|
+
? profile
|
|
1142
|
+
: { ...profile, accountIdentity };
|
|
1143
|
+
if (existingProfile &&
|
|
1144
|
+
(accountIdentity === undefined ||
|
|
1145
|
+
existingProfile.accountIdentity === undefined ||
|
|
1146
|
+
existingProfile.accountIdentity === accountIdentity) &&
|
|
1147
|
+
existingProfile.checkedAt > persistedProfile.checkedAt) {
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
next.main.profile = persistedProfile;
|
|
894
1151
|
}
|
|
895
1152
|
else {
|
|
896
1153
|
const account = current?.accounts.find((candidate) => candidate.id === accountId && isOAuthAccount(candidate));
|
|
897
|
-
if (!account
|
|
898
|
-
tokenFingerprint(account.access) !== expectedTokenFingerprint) {
|
|
1154
|
+
if (!account)
|
|
899
1155
|
return false;
|
|
900
|
-
}
|
|
901
1156
|
next.accounts = {
|
|
902
1157
|
...(isRecord(next.accounts) ? next.accounts : {}),
|
|
903
1158
|
};
|
|
@@ -905,16 +1160,33 @@ export function saveOAuthProfileState(input, path = getAccountStoragePath()) {
|
|
|
905
1160
|
? { ...next.accounts[accountId] }
|
|
906
1161
|
: {};
|
|
907
1162
|
const existingProfile = normalizeOAuthAccountProfile(existingEntry.profile);
|
|
908
|
-
if (!profile
|
|
909
|
-
existingProfile
|
|
910
|
-
|
|
1163
|
+
if (!profile) {
|
|
1164
|
+
if (existingProfile &&
|
|
1165
|
+
(accountIdentity === undefined ||
|
|
1166
|
+
existingProfile.accountIdentity === undefined ||
|
|
1167
|
+
existingProfile.accountIdentity === accountIdentity)) {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
existingEntry.profile = undefined;
|
|
911
1171
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1172
|
+
else {
|
|
1173
|
+
if (profile.accountIdentity !== undefined &&
|
|
1174
|
+
profile.accountIdentity !== accountIdentity) {
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
const persistedProfile = accountIdentity === undefined ||
|
|
1178
|
+
profile.accountIdentity === accountIdentity
|
|
1179
|
+
? profile
|
|
1180
|
+
: { ...profile, accountIdentity };
|
|
1181
|
+
if (existingProfile &&
|
|
1182
|
+
(accountIdentity === undefined ||
|
|
1183
|
+
existingProfile.accountIdentity === undefined ||
|
|
1184
|
+
existingProfile.accountIdentity === accountIdentity) &&
|
|
1185
|
+
existingProfile.checkedAt > persistedProfile.checkedAt) {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
existingEntry.profile = persistedProfile;
|
|
916
1189
|
}
|
|
917
|
-
existingEntry.profile = profile;
|
|
918
1190
|
next.accounts[accountId] = existingEntry;
|
|
919
1191
|
}
|
|
920
1192
|
await writeJsonAtomic(statePath, pruneUndefined(next));
|
|
@@ -941,6 +1213,8 @@ async function saveAccountStateUnlocked(storage, path, scope) {
|
|
|
941
1213
|
applyMainQuotaStatePatch(next, storage);
|
|
942
1214
|
if (scope.mainRefresh)
|
|
943
1215
|
applyMainRefreshStatePatch(next, storage);
|
|
1216
|
+
if (scope.mainPrime)
|
|
1217
|
+
applyMainPrimeStatePatch(next, storage);
|
|
944
1218
|
if (scope.accounts) {
|
|
945
1219
|
const ids = scope.accounts === true ? null : new Set(scope.accounts);
|
|
946
1220
|
next.accounts = { ...(isRecord(next.accounts) ? next.accounts : {}) };
|
|
@@ -1284,6 +1558,134 @@ export async function setCacheKeepSubagentsEnabled(enabled, path = getAccountSto
|
|
|
1284
1558
|
await saveAccounts(storage, path);
|
|
1285
1559
|
return storage;
|
|
1286
1560
|
}
|
|
1561
|
+
export function isPrimePersistentlyEnabled(storage) {
|
|
1562
|
+
return storage?.prime?.enabled === true;
|
|
1563
|
+
}
|
|
1564
|
+
export async function setPrimePersistentEnabled(enabled, path = getAccountStoragePath()) {
|
|
1565
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1566
|
+
storage.prime = {
|
|
1567
|
+
...(storage.prime ?? {}),
|
|
1568
|
+
enabled,
|
|
1569
|
+
};
|
|
1570
|
+
await saveAccounts(storage, path);
|
|
1571
|
+
return storage;
|
|
1572
|
+
}
|
|
1573
|
+
/** Return the stable prime marker identity for an OAuth account. */
|
|
1574
|
+
export async function getOrCreatePrimeAuthLineageId(accountId, path = getAccountStoragePath()) {
|
|
1575
|
+
return enqueueSave(async () => {
|
|
1576
|
+
const configLock = await acquireAccountConfigWriteLock(path);
|
|
1577
|
+
try {
|
|
1578
|
+
const stateLock = await acquireAccountStateWriteLock(path);
|
|
1579
|
+
try {
|
|
1580
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1581
|
+
if (accountId === 'main') {
|
|
1582
|
+
const existing = storage.prime?.mainAuthLineageId;
|
|
1583
|
+
if (existing)
|
|
1584
|
+
return existing;
|
|
1585
|
+
const mainAccountId = storage.mainAccountId ?? randomUUID();
|
|
1586
|
+
if (!storage.mainAccountId) {
|
|
1587
|
+
const existingConfig = await loadExistingTopLevelFields(path);
|
|
1588
|
+
await writeJsonAtomic(path, {
|
|
1589
|
+
...existingConfig,
|
|
1590
|
+
...configFromStorage({ ...storage, mainAccountId }),
|
|
1591
|
+
});
|
|
1592
|
+
storage.mainAccountId = mainAccountId;
|
|
1593
|
+
}
|
|
1594
|
+
storage.prime = {
|
|
1595
|
+
...(storage.prime ?? {}),
|
|
1596
|
+
mainAuthLineageId: mainAccountId,
|
|
1597
|
+
};
|
|
1598
|
+
await saveAccountStateUnlocked(storage, path, { mainPrime: true });
|
|
1599
|
+
return mainAccountId;
|
|
1600
|
+
}
|
|
1601
|
+
const account = storage.accounts.find((candidate) => candidate.id === accountId && isOAuthAccount(candidate));
|
|
1602
|
+
if (!account) {
|
|
1603
|
+
throw new Error(`getOrCreatePrimeAuthLineageId: OAuth account "${accountId}" not found`);
|
|
1604
|
+
}
|
|
1605
|
+
if (account.authLineageId)
|
|
1606
|
+
return account.authLineageId;
|
|
1607
|
+
// Upgrading changes the marker namespace once, so an already-primed
|
|
1608
|
+
// window may fire again; persisting the seed prevents every later token
|
|
1609
|
+
// rotation from repeating that migration cost.
|
|
1610
|
+
const authLineageId = randomUUID();
|
|
1611
|
+
account.authLineageId = authLineageId;
|
|
1612
|
+
await saveAccountStateUnlocked(storage, path, {
|
|
1613
|
+
accounts: [accountId],
|
|
1614
|
+
});
|
|
1615
|
+
return authLineageId;
|
|
1616
|
+
}
|
|
1617
|
+
finally {
|
|
1618
|
+
await stateLock.release();
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
finally {
|
|
1622
|
+
await configLock.release();
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Atomically increment an account's cumulative prime counters and persist via
|
|
1628
|
+
* the scoped runtime-state path. The `main` account lives at state.main.prime;
|
|
1629
|
+
* every other account lives at state.accounts[id].prime. Config-side writes
|
|
1630
|
+
* are intentionally NOT triggered so prime counters cannot leak into
|
|
1631
|
+
* `anthropic-auth.json`. Callers should not depend on this function to mutate
|
|
1632
|
+
* the caller's storage object.
|
|
1633
|
+
*/
|
|
1634
|
+
export async function incrementPrimeUsagePersistent(accountId, usage, path = getAccountStoragePath(), now = Date.now()) {
|
|
1635
|
+
const inputTokens = Number.isFinite(usage?.inputTokens)
|
|
1636
|
+
? Math.max(0, Math.floor(usage.inputTokens))
|
|
1637
|
+
: 0;
|
|
1638
|
+
const outputTokens = Number.isFinite(usage?.outputTokens)
|
|
1639
|
+
? Math.max(0, Math.floor(usage.outputTokens))
|
|
1640
|
+
: 0;
|
|
1641
|
+
return enqueueSave(async () => {
|
|
1642
|
+
// The config-write lock is the repository-wide outer lock for config and
|
|
1643
|
+
// runtime-state RMW operations; taking it before the state write preserves
|
|
1644
|
+
// saveAccountsLocked's config → state ordering across processes.
|
|
1645
|
+
const lock = await acquireAccountConfigWriteLock(path);
|
|
1646
|
+
try {
|
|
1647
|
+
const stateLock = await acquireAccountStateWriteLock(path);
|
|
1648
|
+
try {
|
|
1649
|
+
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1650
|
+
if (accountId === 'main') {
|
|
1651
|
+
const existing = storage.prime?.main;
|
|
1652
|
+
const next = {
|
|
1653
|
+
count: (existing?.count ?? 0) + 1,
|
|
1654
|
+
inputTokens: (existing?.inputTokens ?? 0) + inputTokens,
|
|
1655
|
+
outputTokens: (existing?.outputTokens ?? 0) + outputTokens,
|
|
1656
|
+
since: existing?.since ?? Math.floor(now),
|
|
1657
|
+
};
|
|
1658
|
+
storage.prime = { ...(storage.prime ?? {}), main: next };
|
|
1659
|
+
await saveAccountStateUnlocked(storage, path, { mainPrime: true });
|
|
1660
|
+
return next;
|
|
1661
|
+
}
|
|
1662
|
+
const index = storage.accounts.findIndex((account) => account.id === accountId);
|
|
1663
|
+
if (index < 0) {
|
|
1664
|
+
throw new Error(`incrementPrimeUsagePersistent: account "${accountId}" not found`);
|
|
1665
|
+
}
|
|
1666
|
+
const account = storage.accounts[index];
|
|
1667
|
+
const existing = account.prime;
|
|
1668
|
+
const next = {
|
|
1669
|
+
count: (existing?.count ?? 0) + 1,
|
|
1670
|
+
inputTokens: (existing?.inputTokens ?? 0) + inputTokens,
|
|
1671
|
+
outputTokens: (existing?.outputTokens ?? 0) + outputTokens,
|
|
1672
|
+
since: existing?.since ?? Math.floor(now),
|
|
1673
|
+
};
|
|
1674
|
+
storage.accounts[index] = { ...account, prime: next };
|
|
1675
|
+
await saveAccountStateUnlocked(storage, path, {
|
|
1676
|
+
accounts: [accountId],
|
|
1677
|
+
});
|
|
1678
|
+
return next;
|
|
1679
|
+
}
|
|
1680
|
+
finally {
|
|
1681
|
+
await stateLock.release();
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
finally {
|
|
1685
|
+
await lock.release();
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1287
1689
|
function getFallbackStatuses(storage) {
|
|
1288
1690
|
return storage?.fallbackOn?.length ? storage.fallbackOn : DEFAULT_FALLBACK_ON;
|
|
1289
1691
|
}
|
|
@@ -1337,9 +1739,8 @@ function isTransientRefreshError(error) {
|
|
|
1337
1739
|
error.code === 'UND_ERR_CONNECT_TIMEOUT')));
|
|
1338
1740
|
}
|
|
1339
1741
|
export function buildRefreshOperationError(input) {
|
|
1340
|
-
const
|
|
1341
|
-
|
|
1342
|
-
? (input.previous.retryCount ?? 0)
|
|
1742
|
+
const previousRetryCount = input.previous?.accountIdentity === input.accountIdentity
|
|
1743
|
+
? (input.previous?.retryCount ?? 0)
|
|
1343
1744
|
: 0;
|
|
1344
1745
|
const retryCount = previousRetryCount + 1;
|
|
1345
1746
|
const retryAfterFromError = input.error
|
|
@@ -1374,7 +1775,7 @@ export function buildRefreshOperationError(input) {
|
|
|
1374
1775
|
checkedAt: input.now,
|
|
1375
1776
|
nextRetryAt: input.now + delay,
|
|
1376
1777
|
retryCount,
|
|
1377
|
-
|
|
1778
|
+
accountIdentity: input.accountIdentity,
|
|
1378
1779
|
status,
|
|
1379
1780
|
permanent: status === 400 && isInvalidGrant,
|
|
1380
1781
|
};
|
|
@@ -1407,17 +1808,40 @@ export function isPermanentRefreshError(error) {
|
|
|
1407
1808
|
}
|
|
1408
1809
|
return false;
|
|
1409
1810
|
}
|
|
1410
|
-
export function refreshBackoffActive(error,
|
|
1811
|
+
export function refreshBackoffActive(error, accountIdentity, now) {
|
|
1411
1812
|
if (!error?.nextRetryAt || error.nextRetryAt <= now)
|
|
1412
1813
|
return false;
|
|
1413
|
-
if (!
|
|
1814
|
+
if (!error.accountIdentity)
|
|
1815
|
+
return true;
|
|
1816
|
+
if (!accountIdentity)
|
|
1414
1817
|
return true;
|
|
1415
|
-
return error.
|
|
1818
|
+
return error.accountIdentity === accountIdentity;
|
|
1416
1819
|
}
|
|
1417
1820
|
export function formatRefreshBackoffMessage(error, now) {
|
|
1418
1821
|
const seconds = Math.max(1, Math.ceil(((error.nextRetryAt ?? now) - now) / 1000));
|
|
1419
1822
|
return `Claude OAuth refresh is backed off for ${seconds}s after: ${error.message}`;
|
|
1420
1823
|
}
|
|
1824
|
+
function createRefreshBackoffActiveError(error, now) {
|
|
1825
|
+
return Object.assign(new Error(formatRefreshBackoffMessage(error, now)), {
|
|
1826
|
+
refreshBackoffError: error,
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
function existingRefreshBackoffError(error) {
|
|
1830
|
+
if (!error || typeof error !== 'object')
|
|
1831
|
+
return undefined;
|
|
1832
|
+
const existing = error
|
|
1833
|
+
.refreshBackoffError;
|
|
1834
|
+
return existing && typeof existing.message === 'string' ? existing : undefined;
|
|
1835
|
+
}
|
|
1836
|
+
export function getFallbackReauthLabels(storage) {
|
|
1837
|
+
if (!storage)
|
|
1838
|
+
return [];
|
|
1839
|
+
return storage.accounts
|
|
1840
|
+
.filter((account) => account.enabled !== false &&
|
|
1841
|
+
isOAuthAccount(account) &&
|
|
1842
|
+
isPermanentRefreshError(account.lastRefreshError))
|
|
1843
|
+
.map((account) => account.label?.trim() || account.id);
|
|
1844
|
+
}
|
|
1421
1845
|
export function isQuotaPolicyAuthError(error) {
|
|
1422
1846
|
const status = error.status;
|
|
1423
1847
|
if (status === 403)
|
|
@@ -1425,7 +1849,9 @@ export function isQuotaPolicyAuthError(error) {
|
|
|
1425
1849
|
return /Claude quota check failed: 403\b/.test(formatErrorMessage(error));
|
|
1426
1850
|
}
|
|
1427
1851
|
export function buildQuotaOperationError(input) {
|
|
1428
|
-
const previousRetryCount = input.previous?.
|
|
1852
|
+
const previousRetryCount = input.previous?.accountIdentity === input.accountIdentity
|
|
1853
|
+
? (input.previous?.retryCount ?? 0)
|
|
1854
|
+
: 0;
|
|
1429
1855
|
const retryCount = previousRetryCount + 1;
|
|
1430
1856
|
const delay = isTransientQuotaError(input.error)
|
|
1431
1857
|
? Math.min(MAX_QUOTA_RETRY_DELAY_MS, MIN_QUOTA_RETRY_DELAY_MS * 2 ** Math.min(retryCount - 1, 6))
|
|
@@ -1435,6 +1861,9 @@ export function buildQuotaOperationError(input) {
|
|
|
1435
1861
|
checkedAt: input.now,
|
|
1436
1862
|
nextRetryAt: input.now + delay,
|
|
1437
1863
|
retryCount,
|
|
1864
|
+
...(input.accountIdentity !== undefined && {
|
|
1865
|
+
accountIdentity: input.accountIdentity,
|
|
1866
|
+
}),
|
|
1438
1867
|
};
|
|
1439
1868
|
}
|
|
1440
1869
|
export function quotaBackoffActive(error, now) {
|
|
@@ -1454,7 +1883,7 @@ export function getPersistedLogLevel(storage) {
|
|
|
1454
1883
|
return storage?.logging?.level;
|
|
1455
1884
|
}
|
|
1456
1885
|
export async function setLogLevelPersistent(level, path = getAccountStoragePath()) {
|
|
1457
|
-
const { setLogLevel } = await import(
|
|
1886
|
+
const { setLogLevel } = await import('./logger.js');
|
|
1458
1887
|
const storage = (await loadAccounts(path)) ?? createEmptyStorage();
|
|
1459
1888
|
storage.logging = {
|
|
1460
1889
|
...(storage.logging ?? {}),
|
|
@@ -1470,6 +1899,7 @@ export function getPersistedMainQuota(storage) {
|
|
|
1470
1899
|
quota: storage.quota.mainQuota,
|
|
1471
1900
|
checkedAt: storage.quota.mainQuotaCheckedAt,
|
|
1472
1901
|
tokenFingerprint: storage.quota.mainQuotaToken,
|
|
1902
|
+
accountIdentity: storage.quota.mainQuota.accountIdentity,
|
|
1473
1903
|
};
|
|
1474
1904
|
}
|
|
1475
1905
|
/**
|
|
@@ -1486,15 +1916,18 @@ function failClosedOnUnknownQuota(storage) {
|
|
|
1486
1916
|
return (storage?.quota?.failClosedOnUnknownQuota ??
|
|
1487
1917
|
DEFAULT_FAIL_CLOSED_ON_UNKNOWN_QUOTA);
|
|
1488
1918
|
}
|
|
1919
|
+
function normalizeScopedQuotaModel(value) {
|
|
1920
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
1921
|
+
}
|
|
1489
1922
|
function scopedQuotaModelKey(model) {
|
|
1490
1923
|
if (typeof model !== 'string')
|
|
1491
1924
|
return null;
|
|
1492
|
-
const normalized = model
|
|
1925
|
+
const normalized = normalizeScopedQuotaModel(model);
|
|
1493
1926
|
if (normalized.includes('fable'))
|
|
1494
1927
|
return 'fable';
|
|
1495
1928
|
if (normalized.includes('mythos'))
|
|
1496
1929
|
return 'mythos';
|
|
1497
|
-
return
|
|
1930
|
+
return normalized;
|
|
1498
1931
|
}
|
|
1499
1932
|
export function getScopedQuotaWindowForModel(quota, model) {
|
|
1500
1933
|
const key = scopedQuotaModelKey(model);
|
|
@@ -1503,8 +1936,8 @@ export function getScopedQuotaWindowForModel(quota, model) {
|
|
|
1503
1936
|
return quota?.scoped?.find((window) => {
|
|
1504
1937
|
const haystack = [window.modelId, window.modelName, window.title]
|
|
1505
1938
|
.filter((value) => typeof value === 'string')
|
|
1506
|
-
.
|
|
1507
|
-
.
|
|
1939
|
+
.map(normalizeScopedQuotaModel)
|
|
1940
|
+
.join(' ');
|
|
1508
1941
|
return haystack.includes(key);
|
|
1509
1942
|
});
|
|
1510
1943
|
}
|
|
@@ -1533,6 +1966,9 @@ export function quotaSnapshotPassesPolicy(quota, storage) {
|
|
|
1533
1966
|
}
|
|
1534
1967
|
return true;
|
|
1535
1968
|
}
|
|
1969
|
+
export function quotaSnapshotHasStandardWindows(quota) {
|
|
1970
|
+
return Boolean(quota?.five_hour && quota.seven_day);
|
|
1971
|
+
}
|
|
1536
1972
|
// ---------------------------------------------------------------------------
|
|
1537
1973
|
// Killswitch — hard-block requests when remaining quota drops below per-account
|
|
1538
1974
|
// thresholds, even if the API would still accept them.
|
|
@@ -1997,15 +2433,20 @@ function formatErrorMessage(error) {
|
|
|
1997
2433
|
return error instanceof Error ? error.message : String(error);
|
|
1998
2434
|
}
|
|
1999
2435
|
function recordRefreshError(account, error, now) {
|
|
2436
|
+
const existing = existingRefreshBackoffError(error);
|
|
2437
|
+
if (existing) {
|
|
2438
|
+
account.lastRefreshError = existing;
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2000
2441
|
account.lastRefreshError = buildRefreshOperationError({
|
|
2001
2442
|
error,
|
|
2002
2443
|
now,
|
|
2003
|
-
|
|
2444
|
+
accountIdentity: account.id,
|
|
2004
2445
|
previous: account.lastRefreshError,
|
|
2005
2446
|
});
|
|
2006
2447
|
}
|
|
2007
2448
|
function recordQuotaRefreshError(account, error, now) {
|
|
2008
|
-
if (isQuotaPolicyAuthError(error))
|
|
2449
|
+
if (isQuotaPolicyAuthError(error) || existingRefreshBackoffError(error))
|
|
2009
2450
|
return;
|
|
2010
2451
|
account.lastQuotaRefreshError = buildQuotaOperationError({
|
|
2011
2452
|
error,
|
|
@@ -2052,10 +2493,10 @@ export class FallbackAccountManager {
|
|
|
2052
2493
|
return;
|
|
2053
2494
|
if (!account.quota)
|
|
2054
2495
|
return;
|
|
2055
|
-
const checkedAt =
|
|
2496
|
+
const checkedAt = quotaSnapshotCheckedAt(account.quota);
|
|
2056
2497
|
if (checkedAt <= 0)
|
|
2057
2498
|
return;
|
|
2058
|
-
const existing = this.quotaManager.getFallback(account.id, account
|
|
2499
|
+
const existing = this.quotaManager.getFallback(account.id, account);
|
|
2059
2500
|
if (existing && existing.checkedAt >= checkedAt)
|
|
2060
2501
|
return;
|
|
2061
2502
|
const checkInterval = getQuotaCheckIntervalMs(storage);
|
|
@@ -2063,7 +2504,7 @@ export class FallbackAccountManager {
|
|
|
2063
2504
|
quota: account.quota,
|
|
2064
2505
|
refreshAfter: checkedAt + checkInterval,
|
|
2065
2506
|
checkedAt,
|
|
2066
|
-
}, account
|
|
2507
|
+
}, account);
|
|
2067
2508
|
}
|
|
2068
2509
|
async load() {
|
|
2069
2510
|
return loadAccounts(this.configPath);
|
|
@@ -2104,20 +2545,20 @@ export class FallbackAccountManager {
|
|
|
2104
2545
|
for (const account of storage.accounts) {
|
|
2105
2546
|
if (account.enabled === false || !isOAuthAccount(account))
|
|
2106
2547
|
continue;
|
|
2548
|
+
let next = account;
|
|
2107
2549
|
try {
|
|
2108
|
-
let next = account;
|
|
2109
2550
|
if (tokenNeedsRefresh(next, storage, this.now())) {
|
|
2110
2551
|
const refreshError = next.lastRefreshError;
|
|
2111
2552
|
if (refreshError &&
|
|
2112
|
-
refreshBackoffActive(refreshError, next.
|
|
2113
|
-
throw
|
|
2553
|
+
refreshBackoffActive(refreshError, next.id, this.now())) {
|
|
2554
|
+
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2114
2555
|
}
|
|
2115
2556
|
next = await this.refreshAccount(next, storage);
|
|
2116
2557
|
changed = true;
|
|
2117
2558
|
}
|
|
2118
2559
|
this.seedFallbackQuota(next, storage);
|
|
2119
2560
|
const stale = this.quotaManager
|
|
2120
|
-
? this.quotaManager.isFallbackStale(next.id, next.access, options.modelId)
|
|
2561
|
+
? this.quotaManager.isFallbackStale(next.id, next.access, options.modelId, next)
|
|
2121
2562
|
: quotaIsStale(next, storage, this.now(), options.modelId);
|
|
2122
2563
|
// Skip the request-time refresh when this account's quota API is
|
|
2123
2564
|
// backed off (recent 429/5xx). Hitting it again would extend the
|
|
@@ -2125,7 +2566,7 @@ export class FallbackAccountManager {
|
|
|
2125
2566
|
// the background refreshQuotaForDueAccounts() guard.
|
|
2126
2567
|
if (stale &&
|
|
2127
2568
|
!quotaBackoffActive(next.lastQuotaRefreshError, this.now())) {
|
|
2128
|
-
next = await this.refreshAccountQuota(next, storage);
|
|
2569
|
+
next = (await this.refreshAccountQuota(next, storage)).account;
|
|
2129
2570
|
changed = true;
|
|
2130
2571
|
}
|
|
2131
2572
|
// Single source of truth: evaluate quota policy from the unified
|
|
@@ -2137,20 +2578,21 @@ export class FallbackAccountManager {
|
|
|
2137
2578
|
usable.push(next);
|
|
2138
2579
|
}
|
|
2139
2580
|
catch (error) {
|
|
2140
|
-
if (canUseCachedQuotaAfterRefreshError(
|
|
2581
|
+
if (canUseCachedQuotaAfterRefreshError(next, storage, error, this.now())) {
|
|
2141
2582
|
log('[refresh] fallback quota using cached quota after refresh error', {
|
|
2142
|
-
accountId:
|
|
2583
|
+
accountId: next.id,
|
|
2143
2584
|
error: formatErrorMessage(error),
|
|
2144
2585
|
});
|
|
2145
|
-
if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(
|
|
2586
|
+
if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(next), storage, {
|
|
2146
2587
|
modelId: options.modelId,
|
|
2147
2588
|
})) {
|
|
2148
|
-
usable.push(
|
|
2589
|
+
usable.push(next);
|
|
2149
2590
|
}
|
|
2150
2591
|
}
|
|
2151
2592
|
else if (!failClosedOnUnknownQuota(storage) &&
|
|
2152
|
-
|
|
2153
|
-
|
|
2593
|
+
!refreshBackoffActive(next.lastRefreshError, next.id, this.now()) &&
|
|
2594
|
+
quotaSnapshotPassesModelScope(next.quota, options.modelId)) {
|
|
2595
|
+
usable.push(next);
|
|
2154
2596
|
}
|
|
2155
2597
|
}
|
|
2156
2598
|
}
|
|
@@ -2181,7 +2623,7 @@ export class FallbackAccountManager {
|
|
|
2181
2623
|
quotaPolicyAccount(account) {
|
|
2182
2624
|
if (!this.quotaManager)
|
|
2183
2625
|
return account;
|
|
2184
|
-
const cached = this.quotaManager.getFallback(account.id, account
|
|
2626
|
+
const cached = this.quotaManager.getFallback(account.id, account)?.quota;
|
|
2185
2627
|
return cached ? { ...account, quota: cached } : account;
|
|
2186
2628
|
}
|
|
2187
2629
|
async refreshDueAccounts() {
|
|
@@ -2194,7 +2636,7 @@ export class FallbackAccountManager {
|
|
|
2194
2636
|
continue;
|
|
2195
2637
|
if (!tokenNeedsRefresh(account, storage, this.now()))
|
|
2196
2638
|
continue;
|
|
2197
|
-
if (refreshBackoffActive(account.lastRefreshError, account.
|
|
2639
|
+
if (refreshBackoffActive(account.lastRefreshError, account.id, this.now())) {
|
|
2198
2640
|
// Backoff skips are steady-state while a fallback account is waiting for
|
|
2199
2641
|
// its next retry. Logging every background tick from every OpenCode
|
|
2200
2642
|
// process creates noise without adding new diagnostic signal; the
|
|
@@ -2237,7 +2679,7 @@ export class FallbackAccountManager {
|
|
|
2237
2679
|
let next = account;
|
|
2238
2680
|
try {
|
|
2239
2681
|
if (tokenNeedsRefresh(next, storage, this.now())) {
|
|
2240
|
-
if (refreshBackoffActive(next.lastRefreshError, next.
|
|
2682
|
+
if (refreshBackoffActive(next.lastRefreshError, next.id, this.now())) {
|
|
2241
2683
|
continue;
|
|
2242
2684
|
}
|
|
2243
2685
|
next = await this.refreshAccount(next, storage);
|
|
@@ -2284,8 +2726,8 @@ export class FallbackAccountManager {
|
|
|
2284
2726
|
if (tokenNeedsRefresh(next, storage, this.now())) {
|
|
2285
2727
|
const refreshError = next.lastRefreshError;
|
|
2286
2728
|
if (refreshError &&
|
|
2287
|
-
refreshBackoffActive(refreshError, next.
|
|
2288
|
-
throw
|
|
2729
|
+
refreshBackoffActive(refreshError, next.id, this.now())) {
|
|
2730
|
+
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2289
2731
|
}
|
|
2290
2732
|
next = await this.refreshAccount(next, storage);
|
|
2291
2733
|
changed = true;
|
|
@@ -2358,9 +2800,9 @@ export class FallbackAccountManager {
|
|
|
2358
2800
|
}
|
|
2359
2801
|
const refreshError = latestAccount.lastRefreshError;
|
|
2360
2802
|
if (refreshError &&
|
|
2361
|
-
refreshBackoffActive(refreshError, latestAccount.
|
|
2803
|
+
refreshBackoffActive(refreshError, latestAccount.id, this.now())) {
|
|
2362
2804
|
updateStoredAccount(storage, latestAccount);
|
|
2363
|
-
throw
|
|
2805
|
+
throw createRefreshBackoffActiveError(refreshError, this.now());
|
|
2364
2806
|
}
|
|
2365
2807
|
}
|
|
2366
2808
|
return null;
|
|
@@ -2411,11 +2853,14 @@ export class FallbackAccountManager {
|
|
|
2411
2853
|
});
|
|
2412
2854
|
const refreshed = await refreshClaudeOAuthToken({
|
|
2413
2855
|
refreshToken,
|
|
2856
|
+
authLineageId: sourceAccount.authLineageId,
|
|
2414
2857
|
fetchImpl: this.fetchImpl,
|
|
2415
2858
|
now: this.now,
|
|
2416
2859
|
});
|
|
2417
2860
|
sourceAccount.access = refreshed.access;
|
|
2418
2861
|
sourceAccount.refresh = refreshed.refresh;
|
|
2862
|
+
sourceAccount.authLineageId =
|
|
2863
|
+
refreshed.authLineageId ?? sourceAccount.authLineageId;
|
|
2419
2864
|
sourceAccount.expires = refreshed.expires;
|
|
2420
2865
|
sourceAccount.lastRefreshedAt =
|
|
2421
2866
|
refreshed.expires - refreshed.expiresIn * 1000;
|
|
@@ -2443,15 +2888,18 @@ export class FallbackAccountManager {
|
|
|
2443
2888
|
// deduplication and 429 backoff gating around the same quota API. Fall back
|
|
2444
2889
|
// to a direct fetch only when no QuotaManager is wired (e.g. in isolation).
|
|
2445
2890
|
const fetchSnapshot = (accessToken) => this.quotaManager
|
|
2446
|
-
? this.quotaManager.
|
|
2891
|
+
? this.quotaManager.refreshFallbackWithMetadata(target.id, accessToken, target)
|
|
2447
2892
|
: fetchOAuthQuotaSnapshot({
|
|
2448
2893
|
accessToken,
|
|
2449
2894
|
fetchImpl: this.fetchImpl,
|
|
2450
2895
|
now: this.now,
|
|
2451
|
-
});
|
|
2896
|
+
}).then((quota) => ({ quota, fetched: true }));
|
|
2452
2897
|
const fetchStartedAt = this.now();
|
|
2898
|
+
let fetched = false;
|
|
2453
2899
|
try {
|
|
2454
|
-
|
|
2900
|
+
const result = await fetchSnapshot(target.access);
|
|
2901
|
+
target.quota = result.quota;
|
|
2902
|
+
fetched = result.fetched;
|
|
2455
2903
|
}
|
|
2456
2904
|
catch (error) {
|
|
2457
2905
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -2463,7 +2911,9 @@ export class FallbackAccountManager {
|
|
|
2463
2911
|
if (!target.access)
|
|
2464
2912
|
throw error;
|
|
2465
2913
|
// 401 does not arm QuotaManager backoff, so this retry proceeds.
|
|
2466
|
-
|
|
2914
|
+
const result = await fetchSnapshot(target.access);
|
|
2915
|
+
target.quota = result.quota;
|
|
2916
|
+
fetched = result.fetched;
|
|
2467
2917
|
}
|
|
2468
2918
|
const latestStorage = await this.load();
|
|
2469
2919
|
const latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === target.id && isOAuthAccount(candidate));
|
|
@@ -2472,7 +2922,7 @@ export class FallbackAccountManager {
|
|
|
2472
2922
|
latestAccount.access !== target.access) {
|
|
2473
2923
|
this.seedFallbackQuota(latestAccount, latestStorage);
|
|
2474
2924
|
updateStoredAccount(storage, latestAccount);
|
|
2475
|
-
return latestAccount;
|
|
2925
|
+
return { account: latestAccount, fetched: false };
|
|
2476
2926
|
}
|
|
2477
2927
|
if (latestStorage &&
|
|
2478
2928
|
latestAccount?.access === target.access &&
|
|
@@ -2481,7 +2931,7 @@ export class FallbackAccountManager {
|
|
|
2481
2931
|
quotaSnapshotIsFresh(latestAccount.quota, latestStorage, this.now())) {
|
|
2482
2932
|
this.seedFallbackQuota(latestAccount, latestStorage);
|
|
2483
2933
|
updateStoredAccount(storage, latestAccount);
|
|
2484
|
-
return latestAccount;
|
|
2934
|
+
return { account: latestAccount, fetched };
|
|
2485
2935
|
}
|
|
2486
2936
|
target.lastQuotaRefreshError = undefined;
|
|
2487
2937
|
updateStoredAccount(storage, target);
|
|
@@ -2494,8 +2944,8 @@ export class FallbackAccountManager {
|
|
|
2494
2944
|
quota: target.quota,
|
|
2495
2945
|
refreshAfter: now + getQuotaCheckIntervalMs(storage),
|
|
2496
2946
|
checkedAt: now,
|
|
2497
|
-
}, target
|
|
2947
|
+
}, target);
|
|
2498
2948
|
}
|
|
2499
|
-
return target;
|
|
2949
|
+
return { account: target, fetched };
|
|
2500
2950
|
}
|
|
2501
2951
|
}
|