@cortexkit/anthropic-auth-core 1.20.0 → 1.22.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 +66 -16
- package/dist/accounts.js +693 -223
- package/dist/auth.js +3 -10
- package/dist/cch.d.ts +13 -9
- package/dist/cch.js +57 -19
- package/dist/claude-code.d.ts +4 -3
- package/dist/claude-code.js +130 -25
- package/dist/claustrum.d.ts +109 -0
- package/dist/claustrum.js +529 -0
- package/dist/commands/account.d.ts +5 -1
- package/dist/commands/account.js +16 -2
- package/dist/constants.d.ts +4 -5
- package/dist/constants.js +4 -5
- package/dist/dump.d.ts +1 -0
- package/dist/dump.js +8 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/json.d.ts +1 -0
- package/dist/json.js +10 -0
- package/dist/mid-conversation-output-config.d.ts +13 -0
- package/dist/mid-conversation-output-config.js +89 -0
- package/dist/models.d.ts +27 -1
- package/dist/models.js +43 -0
- package/dist/network-errors.d.ts +1 -0
- package/dist/network-errors.js +27 -0
- package/dist/oauth-profile.d.ts +2 -1
- package/dist/oauth-profile.js +10 -4
- package/dist/quota-header-feed.d.ts +52 -0
- package/dist/quota-header-feed.js +279 -0
- package/dist/quota-headers.js +63 -8
- package/dist/quota-manager.d.ts +41 -28
- package/dist/quota-manager.js +405 -148
- package/dist/sticky-routing.d.ts +14 -2
- package/dist/sticky-routing.js +30 -13
- package/dist/thinking-binding.d.ts +7 -0
- package/dist/thinking-binding.js +54 -0
- package/package.json +3 -1
package/dist/quota-manager.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* share one QuotaManager instance so they see the same in-memory cache.
|
|
6
6
|
* Handles deduplication, rate-limiting (429 backoff), and staleness.
|
|
7
7
|
*/
|
|
8
|
-
import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, getScopedQuotaWindowForModel, isQuotaPolicyAuthError, quotaBackoffActive, quotaSnapshotCheckedAt, } from './accounts.js';
|
|
8
|
+
import { acquireRefreshFileLock, buildQuotaOperationError, fetchOAuthQuotaSnapshot, getPersistedMainQuota, getQuotaCheckIntervalMs, getQuotaNextRefreshAt, getQuotaRefreshEveryNRequests, getScopedQuotaWindowForModel, isQuotaPolicyAuthError, mergeMainQuotaErrorClearedAt, quotaBackoffActive, quotaSnapshotCheckedAt, } from './accounts.js';
|
|
9
|
+
import { logger } from './logger.js';
|
|
9
10
|
import { mergeHeaderQuotaSnapshot } from './quota-headers.js';
|
|
10
11
|
export { tokenFingerprint } from './token-fingerprint.js';
|
|
11
12
|
import { tokenFingerprint } from './token-fingerprint.js';
|
|
@@ -38,21 +39,23 @@ function mergePollCompletionWithNewerHeaders(current, polled) {
|
|
|
38
39
|
export class QuotaManager {
|
|
39
40
|
// --- State ---
|
|
40
41
|
main = null;
|
|
41
|
-
|
|
42
|
+
mainAccountId;
|
|
43
|
+
mainQuotaIdentityKnown = false;
|
|
44
|
+
mainQuotaIdentityStrict = false;
|
|
45
|
+
mainGeneration = 0;
|
|
46
|
+
mainQuotaErrorGeneration = 0;
|
|
47
|
+
mainQuotaErrorClearedAt;
|
|
42
48
|
fallbacks = new Map();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// stale entry instead of being treated as fresh.
|
|
46
|
-
fallbackTokenFps = new Map();
|
|
49
|
+
fallbackAuthLineages = new Map();
|
|
50
|
+
fallbackGenerations = new Map();
|
|
47
51
|
// --- Inflight deduplication ---
|
|
48
52
|
inflightMain = null;
|
|
49
|
-
|
|
53
|
+
inflightMainAccountId;
|
|
50
54
|
inflightFallbacks = new Map();
|
|
51
55
|
// --- Rate-limiting (scoped per route so a fallback 429 never backs off the
|
|
52
56
|
// main account or vice versa) ---
|
|
53
57
|
mainLastApiError = undefined;
|
|
54
58
|
fallbackApiErrors = new Map();
|
|
55
|
-
fallbackErrorTokenFps = new Map();
|
|
56
59
|
// --- Serial API gate (prevents concurrent quota API calls) ---
|
|
57
60
|
apiGate = Promise.resolve();
|
|
58
61
|
lastApiCallAt = 0;
|
|
@@ -62,55 +65,43 @@ export class QuotaManager {
|
|
|
62
65
|
now;
|
|
63
66
|
onMainQuotaFetched;
|
|
64
67
|
onApiError;
|
|
68
|
+
onFallbackQuotaFetched;
|
|
69
|
+
onFallbackApiError;
|
|
65
70
|
constructor(opts) {
|
|
66
71
|
this.storage = opts.storage;
|
|
67
72
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
68
73
|
this.now = opts.now ?? Date.now;
|
|
69
74
|
this.onMainQuotaFetched = opts.onMainQuotaFetched;
|
|
70
75
|
this.onApiError = opts.onApiError;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// quota from being served during backoff.
|
|
75
|
-
this.seedMainFromStorage(opts.storage);
|
|
76
|
+
this.onFallbackQuotaFetched = opts.onFallbackQuotaFetched;
|
|
77
|
+
this.onFallbackApiError = opts.onFallbackApiError;
|
|
78
|
+
this.seedMainFromStorage(opts.storage, opts.storage?.mainAccountId);
|
|
76
79
|
this.seedMainBackoffFromStorage(opts.storage);
|
|
77
80
|
}
|
|
78
81
|
// =========================================================================
|
|
79
82
|
// Get (synchronous, from cache)
|
|
80
83
|
// =========================================================================
|
|
81
84
|
/**
|
|
82
|
-
* Cached main quota entry
|
|
83
|
-
*
|
|
84
|
-
* account switched), it is dropped and null is returned so the caller
|
|
85
|
-
* refetches for the current account. Called without a token (e.g. for
|
|
86
|
-
* display) it returns whatever is cached.
|
|
85
|
+
* Cached main quota entry, scoped to the account behind the main credential.
|
|
86
|
+
* An unresolved identity keeps a retained cache unverified rather than exposing it.
|
|
87
87
|
*/
|
|
88
|
-
getMain(
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
getMain(mainAccountId) {
|
|
89
|
+
if (this.mainAccountId !== mainAccountId)
|
|
90
|
+
return null;
|
|
91
|
+
if (this.mainQuotaIdentityKnown &&
|
|
92
|
+
this.mainQuotaIdentityStrict &&
|
|
93
|
+
(mainAccountId === undefined ||
|
|
94
|
+
this.main?.quota.accountIdentity !== mainAccountId)) {
|
|
95
|
+
return null;
|
|
95
96
|
}
|
|
96
97
|
return this.main;
|
|
97
98
|
}
|
|
98
99
|
/**
|
|
99
|
-
* Cached fallback quota entry
|
|
100
|
-
* binding: if the entry was produced by a different token (account re-login),
|
|
101
|
-
* it is dropped and null is returned so the caller refetches.
|
|
100
|
+
* Cached fallback quota entry, scoped to the configured account id.
|
|
102
101
|
*/
|
|
103
|
-
getFallback(accountId,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return entry;
|
|
107
|
-
const fp = this.fallbackTokenFps.get(accountId);
|
|
108
|
-
if (fp !== tokenFingerprint(accessToken)) {
|
|
109
|
-
this.fallbacks.delete(accountId);
|
|
110
|
-
this.fallbackTokenFps.delete(accountId);
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
return entry;
|
|
102
|
+
getFallback(accountId, account) {
|
|
103
|
+
this.bindFallbackLineage(accountId, account);
|
|
104
|
+
return this.fallbacks.get(accountId) ?? null;
|
|
114
105
|
}
|
|
115
106
|
getAllFallbacks() {
|
|
116
107
|
return this.fallbacks;
|
|
@@ -118,104 +109,175 @@ export class QuotaManager {
|
|
|
118
109
|
// =========================================================================
|
|
119
110
|
// Set (manual inject — seeding from persisted account.quota on boot)
|
|
120
111
|
// =========================================================================
|
|
121
|
-
setMain(
|
|
122
|
-
this.
|
|
112
|
+
setMain(mainAccountId, entry) {
|
|
113
|
+
this.mainAccountId = mainAccountId;
|
|
123
114
|
this.main = entry;
|
|
124
115
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
116
|
+
/** Bind quota state to the account behind the current main credential. */
|
|
117
|
+
setMainQuotaAccountIdentity(accountIdentity, strict = true) {
|
|
118
|
+
const previousIdentityKnown = this.mainQuotaIdentityKnown;
|
|
119
|
+
const previousAccountIdentity = this.mainAccountId;
|
|
120
|
+
const changed = !previousIdentityKnown ||
|
|
121
|
+
this.mainAccountId !== accountIdentity ||
|
|
122
|
+
this.mainQuotaIdentityStrict !== strict;
|
|
123
|
+
this.mainQuotaIdentityKnown = true;
|
|
124
|
+
this.mainQuotaIdentityStrict = strict;
|
|
125
|
+
if (!changed)
|
|
126
|
+
return undefined;
|
|
127
|
+
this.mainGeneration += 1;
|
|
128
|
+
this.mainAccountId = accountIdentity;
|
|
129
|
+
if (accountIdentity !== undefined &&
|
|
130
|
+
this.main?.quota.accountIdentity !== accountIdentity) {
|
|
131
|
+
this.main = null;
|
|
129
132
|
}
|
|
130
|
-
|
|
131
|
-
this.
|
|
133
|
+
if (accountIdentity !== undefined &&
|
|
134
|
+
this.mainLastApiError &&
|
|
135
|
+
(this.mainLastApiError.accountIdentity !== undefined
|
|
136
|
+
? this.mainLastApiError.accountIdentity !== accountIdentity
|
|
137
|
+
: previousIdentityKnown &&
|
|
138
|
+
previousAccountIdentity !== undefined &&
|
|
139
|
+
previousAccountIdentity !== accountIdentity)) {
|
|
140
|
+
this.mainLastApiError = undefined;
|
|
141
|
+
this.mainQuotaErrorGeneration += 1;
|
|
142
|
+
this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, this.now());
|
|
143
|
+
return this.mainQuotaErrorGeneration;
|
|
132
144
|
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
setFallback(accountId, entry, account) {
|
|
148
|
+
this.bindFallbackLineage(accountId, account);
|
|
149
|
+
this.fallbacks.set(accountId, entry);
|
|
150
|
+
}
|
|
151
|
+
clearFallback(accountId) {
|
|
152
|
+
this.fallbackGenerations.set(accountId, (this.fallbackGenerations.get(accountId) ?? 0) + 1);
|
|
153
|
+
this.fallbacks.delete(accountId);
|
|
154
|
+
this.fallbackApiErrors.delete(accountId);
|
|
155
|
+
this.inflightFallbacks.delete(QuotaManager.fallbackInflightKey(accountId));
|
|
133
156
|
}
|
|
134
|
-
pushMainFromHeaders(
|
|
157
|
+
pushMainFromHeaders(mainAccountId, incoming) {
|
|
135
158
|
const checkedAt = incoming.checkedAt ?? this.now();
|
|
136
|
-
const
|
|
137
|
-
|
|
159
|
+
const quota = mergeHeaderQuotaSnapshot(this.mainAccountId === mainAccountId &&
|
|
160
|
+
(!this.mainQuotaIdentityKnown ||
|
|
161
|
+
!this.mainQuotaIdentityStrict ||
|
|
162
|
+
this.main?.quota.accountIdentity === mainAccountId)
|
|
163
|
+
? this.main?.quota
|
|
164
|
+
: undefined, {
|
|
165
|
+
...incoming,
|
|
166
|
+
...(mainAccountId !== undefined && { accountIdentity: mainAccountId }),
|
|
167
|
+
});
|
|
138
168
|
const entry = {
|
|
139
169
|
quota,
|
|
140
170
|
checkedAt,
|
|
141
171
|
refreshAfter: getQuotaNextRefreshAt(quota, this.storage, checkedAt),
|
|
142
172
|
};
|
|
143
|
-
this.setMain(
|
|
173
|
+
this.setMain(mainAccountId, entry);
|
|
144
174
|
return entry;
|
|
145
175
|
}
|
|
146
|
-
pushFallbackFromHeaders(accountId,
|
|
176
|
+
pushFallbackFromHeaders(accountId, incoming, account) {
|
|
177
|
+
if (!this.acceptFallbackQuotaObservation(accountId, account))
|
|
178
|
+
return null;
|
|
147
179
|
const checkedAt = incoming.checkedAt ?? this.now();
|
|
148
|
-
const quota = mergeHeaderQuotaSnapshot(this.
|
|
180
|
+
const quota = mergeHeaderQuotaSnapshot(this.fallbacks.get(accountId)?.quota, {
|
|
181
|
+
...incoming,
|
|
182
|
+
accountIdentity: accountId,
|
|
183
|
+
});
|
|
149
184
|
const entry = {
|
|
150
185
|
quota,
|
|
151
186
|
checkedAt,
|
|
152
187
|
refreshAfter: getQuotaNextRefreshAt(quota, this.storage, checkedAt),
|
|
153
188
|
};
|
|
154
|
-
this.
|
|
189
|
+
this.fallbacks.set(accountId, entry);
|
|
155
190
|
return entry;
|
|
156
191
|
}
|
|
157
192
|
// =========================================================================
|
|
158
193
|
// Refresh (async, deduplicated, rate-limited)
|
|
159
194
|
// =========================================================================
|
|
160
|
-
async refreshMain(accessToken) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
195
|
+
async refreshMain(mainAccountIdOrAccessToken, accessToken, expectedIdentityGeneration) {
|
|
196
|
+
const mainAccountId = mainAccountIdOrAccessToken;
|
|
197
|
+
const credential = accessToken ?? mainAccountIdOrAccessToken;
|
|
198
|
+
if (!credential)
|
|
199
|
+
throw new Error('Main OAuth access token is unavailable');
|
|
200
|
+
return (await this.refreshMainWithMetadata(mainAccountId, credential, expectedIdentityGeneration)).quota;
|
|
201
|
+
}
|
|
202
|
+
async refreshMainWithMetadata(mainAccountIdOrAccessToken, accessToken, expectedIdentityGeneration) {
|
|
203
|
+
const effectiveAccountId = mainAccountIdOrAccessToken;
|
|
204
|
+
const credential = accessToken ?? mainAccountIdOrAccessToken;
|
|
205
|
+
if (!credential)
|
|
206
|
+
throw new Error('Main OAuth access token is unavailable');
|
|
207
|
+
if (expectedIdentityGeneration !== undefined &&
|
|
208
|
+
(this.mainGeneration !== expectedIdentityGeneration ||
|
|
209
|
+
this.mainAccountId !== effectiveAccountId)) {
|
|
210
|
+
throw new Error('Main quota identity changed before refresh');
|
|
211
|
+
}
|
|
212
|
+
if (this.mainAccountId !== effectiveAccountId) {
|
|
169
213
|
this.main = null;
|
|
170
|
-
this.
|
|
214
|
+
this.mainAccountId = effectiveAccountId;
|
|
215
|
+
if (this.mainQuotaIdentityKnown)
|
|
216
|
+
this.mainGeneration += 1;
|
|
171
217
|
}
|
|
172
|
-
|
|
173
|
-
if (this.inflightMain && this.inflightMainFp === fp)
|
|
218
|
+
if (this.inflightMain && this.inflightMainAccountId === effectiveAccountId)
|
|
174
219
|
return this.inflightMain;
|
|
175
220
|
// Rate-limit — if API recently 429'd, return stale or throw
|
|
176
221
|
if (this.isBackedOff()) {
|
|
177
|
-
if (this.main)
|
|
222
|
+
if (this.main && this.mainAccountId === effectiveAccountId) {
|
|
178
223
|
return { quota: this.main.quota, fetched: false };
|
|
224
|
+
}
|
|
179
225
|
throw new Error('Quota API rate-limited — try again later');
|
|
180
226
|
}
|
|
181
|
-
this.
|
|
182
|
-
|
|
227
|
+
this.inflightMainAccountId = effectiveAccountId;
|
|
228
|
+
const generation = this.mainGeneration;
|
|
229
|
+
this.inflightMain = this._fetchMain(effectiveAccountId, credential, generation);
|
|
183
230
|
return this.inflightMain;
|
|
184
231
|
}
|
|
185
|
-
async refreshFallback(accountId, accessToken) {
|
|
186
|
-
return (await this.refreshFallbackWithMetadata(accountId, accessToken))
|
|
187
|
-
.quota;
|
|
232
|
+
async refreshFallback(accountId, accessToken, account) {
|
|
233
|
+
return (await this.refreshFallbackWithMetadata(accountId, accessToken, account)).quota;
|
|
188
234
|
}
|
|
189
|
-
async refreshFallbackWithMetadata(accountId, accessToken) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const inflightKey = QuotaManager.fallbackInflightKey(accountId, accessToken);
|
|
235
|
+
async refreshFallbackWithMetadata(accountId, accessToken, account) {
|
|
236
|
+
this.bindFallbackLineage(accountId, account);
|
|
237
|
+
const inflightKey = QuotaManager.fallbackInflightKey(accountId);
|
|
193
238
|
const inflight = this.inflightFallbacks.get(inflightKey);
|
|
194
239
|
if (inflight)
|
|
195
240
|
return inflight;
|
|
196
241
|
// Rate-limit — scoped to THIS fallback account only
|
|
197
|
-
if (this.isFallbackBackedOff(accountId
|
|
198
|
-
const cached = this.getFallback(accountId
|
|
242
|
+
if (this.isFallbackBackedOff(accountId)) {
|
|
243
|
+
const cached = this.getFallback(accountId);
|
|
199
244
|
if (cached)
|
|
200
245
|
return { quota: cached.quota, fetched: false };
|
|
201
246
|
throw new Error('Quota API rate-limited — try again later');
|
|
202
247
|
}
|
|
203
|
-
const
|
|
248
|
+
const generation = this.fallbackGenerations.get(accountId) ?? 0;
|
|
249
|
+
const promise = this._fetchFallback(accountId, accessToken, generation, account);
|
|
204
250
|
this.inflightFallbacks.set(inflightKey, promise);
|
|
251
|
+
void promise.then(() => {
|
|
252
|
+
if (this.inflightFallbacks.get(inflightKey) === promise) {
|
|
253
|
+
this.inflightFallbacks.delete(inflightKey);
|
|
254
|
+
}
|
|
255
|
+
}, () => {
|
|
256
|
+
if (this.inflightFallbacks.get(inflightKey) === promise) {
|
|
257
|
+
this.inflightFallbacks.delete(inflightKey);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
205
260
|
return promise;
|
|
206
261
|
}
|
|
207
|
-
async refreshAllFallbacks(accounts) {
|
|
262
|
+
async refreshAllFallbacks(accounts, resolveAccessToken) {
|
|
208
263
|
const now = this.now();
|
|
209
264
|
for (const account of accounts) {
|
|
210
265
|
if (account.enabled === false)
|
|
211
266
|
continue;
|
|
212
|
-
|
|
267
|
+
const accessToken = resolveAccessToken?.(account) ??
|
|
268
|
+
(account.access &&
|
|
269
|
+
account.expires !== undefined &&
|
|
270
|
+
account.expires > now
|
|
271
|
+
? account.access
|
|
272
|
+
: undefined);
|
|
273
|
+
if (!accessToken)
|
|
213
274
|
continue;
|
|
214
|
-
|
|
275
|
+
this.bindFallbackLineage(account.id, account);
|
|
276
|
+
const cached = this.getFallback(account.id);
|
|
215
277
|
if (cached && now < cached.refreshAfter)
|
|
216
278
|
continue;
|
|
217
279
|
try {
|
|
218
|
-
await this.refreshFallback(account.id, account
|
|
280
|
+
await this.refreshFallback(account.id, accessToken, account);
|
|
219
281
|
}
|
|
220
282
|
catch {
|
|
221
283
|
// Best-effort — keep stale cache entry if fetch fails
|
|
@@ -230,7 +292,7 @@ export class QuotaManager {
|
|
|
230
292
|
return;
|
|
231
293
|
if (this.isBackedOff())
|
|
232
294
|
return;
|
|
233
|
-
void this.refreshMain(accessToken).catch(() => { });
|
|
295
|
+
void this.refreshMain(this.mainAccountId, accessToken).catch(() => { });
|
|
234
296
|
}
|
|
235
297
|
// =========================================================================
|
|
236
298
|
// Staleness queries
|
|
@@ -243,12 +305,19 @@ export class QuotaManager {
|
|
|
243
305
|
isMainStale(modelId) {
|
|
244
306
|
if (!this.main)
|
|
245
307
|
return true;
|
|
308
|
+
if (this.mainQuotaIdentityKnown &&
|
|
309
|
+
this.mainQuotaIdentityStrict &&
|
|
310
|
+
(this.mainAccountId === undefined ||
|
|
311
|
+
this.main.quota.accountIdentity !== this.mainAccountId)) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
246
314
|
return (this.now() >= this.main.refreshAfter ||
|
|
247
315
|
this.scopedWindowIsStale(this.main, modelId));
|
|
248
316
|
}
|
|
249
|
-
isFallbackStale(accountId,
|
|
250
|
-
//
|
|
251
|
-
|
|
317
|
+
isFallbackStale(accountId, _accessToken, modelId, account) {
|
|
318
|
+
// Keep the unused slot to avoid breaking internal callers that pass modelId
|
|
319
|
+
// third; cached quota is scoped to stable account identity.
|
|
320
|
+
const entry = this.getFallback(accountId, account);
|
|
252
321
|
if (!entry)
|
|
253
322
|
return true;
|
|
254
323
|
return (this.now() >= entry.refreshAfter ||
|
|
@@ -273,7 +342,7 @@ export class QuotaManager {
|
|
|
273
342
|
// =========================================================================
|
|
274
343
|
updateStorage(storage) {
|
|
275
344
|
this.storage = storage;
|
|
276
|
-
this.seedMainFromStorage(storage);
|
|
345
|
+
this.seedMainFromStorage(storage, this.mainQuotaIdentityKnown ? this.mainAccountId : storage?.mainAccountId);
|
|
277
346
|
this.seedMainBackoffFromStorage(storage);
|
|
278
347
|
}
|
|
279
348
|
/**
|
|
@@ -282,37 +351,106 @@ export class QuotaManager {
|
|
|
282
351
|
* can stop this process from showing "checking…" or making a redundant quota
|
|
283
352
|
* API call.
|
|
284
353
|
*/
|
|
285
|
-
seedMainFromStorage(storage,
|
|
354
|
+
seedMainFromStorage(storage, mainAccountId) {
|
|
286
355
|
const persisted = getPersistedMainQuota(storage);
|
|
287
356
|
if (!persisted)
|
|
288
357
|
return;
|
|
289
|
-
const accessTokenFp = accessToken ? tokenFingerprint(accessToken) : null;
|
|
290
|
-
if (accessTokenFp &&
|
|
291
|
-
persisted.tokenFingerprint &&
|
|
292
|
-
persisted.tokenFingerprint !== accessTokenFp) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
358
|
const entry = {
|
|
296
|
-
quota:
|
|
359
|
+
quota: {
|
|
360
|
+
...persisted.quota,
|
|
361
|
+
...((!this.mainQuotaIdentityKnown || !this.mainQuotaIdentityStrict) &&
|
|
362
|
+
mainAccountId !== undefined &&
|
|
363
|
+
persisted.accountIdentity === undefined && {
|
|
364
|
+
accountIdentity: mainAccountId,
|
|
365
|
+
}),
|
|
366
|
+
},
|
|
297
367
|
refreshAfter: getQuotaNextRefreshAt(persisted.quota, storage, persisted.checkedAt),
|
|
298
368
|
checkedAt: persisted.checkedAt,
|
|
299
369
|
};
|
|
370
|
+
const current = this.main &&
|
|
371
|
+
this.mainAccountId === mainAccountId &&
|
|
372
|
+
(!this.mainQuotaIdentityKnown ||
|
|
373
|
+
!this.mainQuotaIdentityStrict ||
|
|
374
|
+
this.main.quota.accountIdentity === mainAccountId)
|
|
375
|
+
? this.main
|
|
376
|
+
: undefined;
|
|
377
|
+
if (current &&
|
|
378
|
+
current.checkedAt >= entry.checkedAt &&
|
|
379
|
+
current.quota.source === 'headers' &&
|
|
380
|
+
entry.quota.source === 'poll') {
|
|
381
|
+
const mergedQuota = mergeHeaderQuotaSnapshot(entry.quota, current.quota);
|
|
382
|
+
this.main = {
|
|
383
|
+
quota: mergedQuota,
|
|
384
|
+
refreshAfter: getQuotaNextRefreshAt(mergedQuota, storage, current.checkedAt),
|
|
385
|
+
checkedAt: current.checkedAt,
|
|
386
|
+
};
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
300
389
|
if (this.main &&
|
|
301
|
-
this.
|
|
302
|
-
|
|
303
|
-
!this.mainTokenFp ||
|
|
304
|
-
this.mainTokenFp === accessTokenFp)) {
|
|
390
|
+
this.mainAccountId === mainAccountId &&
|
|
391
|
+
this.main.checkedAt >= entry.checkedAt) {
|
|
305
392
|
return;
|
|
306
393
|
}
|
|
307
394
|
this.main = entry;
|
|
308
|
-
|
|
395
|
+
if (mainAccountId !== undefined)
|
|
396
|
+
this.mainAccountId = mainAccountId;
|
|
309
397
|
}
|
|
310
398
|
seedMainBackoffFromStorage(storage) {
|
|
311
399
|
const persistedError = storage?.quota?.mainLastQuotaApiError;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
400
|
+
const persistedIdentity = persistedError?.accountIdentity;
|
|
401
|
+
const persistedGeneration = typeof storage?.quota?.mainQuotaErrorGeneration === 'number' &&
|
|
402
|
+
Number.isSafeInteger(storage.quota.mainQuotaErrorGeneration) &&
|
|
403
|
+
storage.quota.mainQuotaErrorGeneration >= 0
|
|
404
|
+
? storage.quota.mainQuotaErrorGeneration
|
|
405
|
+
: undefined;
|
|
406
|
+
const persistedClearedAt = typeof storage?.quota?.mainQuotaErrorClearedAt === 'number' &&
|
|
407
|
+
Number.isFinite(storage.quota.mainQuotaErrorClearedAt) &&
|
|
408
|
+
storage.quota.mainQuotaErrorClearedAt >= 0
|
|
409
|
+
? storage.quota.mainQuotaErrorClearedAt
|
|
410
|
+
: undefined;
|
|
411
|
+
if (persistedClearedAt !== undefined &&
|
|
412
|
+
(this.mainQuotaErrorClearedAt === undefined ||
|
|
413
|
+
persistedClearedAt > this.mainQuotaErrorClearedAt)) {
|
|
414
|
+
this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, persistedClearedAt);
|
|
415
|
+
if ((!persistedError ||
|
|
416
|
+
(typeof persistedError.checkedAt === 'number' &&
|
|
417
|
+
persistedError.checkedAt <= persistedClearedAt)) &&
|
|
418
|
+
(this.mainLastApiError?.checkedAt === undefined ||
|
|
419
|
+
this.mainLastApiError.checkedAt <= persistedClearedAt)) {
|
|
420
|
+
this.mainLastApiError = undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (persistedGeneration !== undefined &&
|
|
424
|
+
persistedGeneration > this.mainQuotaErrorGeneration) {
|
|
425
|
+
this.mainQuotaErrorGeneration = persistedGeneration;
|
|
426
|
+
if (!persistedError &&
|
|
427
|
+
(this.mainLastApiError?.checkedAt === undefined ||
|
|
428
|
+
this.mainQuotaErrorClearedAt === undefined ||
|
|
429
|
+
this.mainLastApiError.checkedAt <= this.mainQuotaErrorClearedAt)) {
|
|
430
|
+
this.mainLastApiError = undefined;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (persistedError &&
|
|
434
|
+
(!this.mainQuotaIdentityKnown ||
|
|
435
|
+
persistedIdentity === undefined ||
|
|
436
|
+
persistedIdentity === this.mainAccountId) &&
|
|
437
|
+
(this.mainQuotaErrorClearedAt === undefined ||
|
|
438
|
+
typeof persistedError.checkedAt !== 'number' ||
|
|
439
|
+
persistedError.checkedAt > this.mainQuotaErrorClearedAt) &&
|
|
440
|
+
(this.mainLastApiError?.checkedAt === undefined ||
|
|
441
|
+
persistedError.checkedAt >= this.mainLastApiError.checkedAt) &&
|
|
442
|
+
quotaBackoffActive(persistedError, this.now())) {
|
|
443
|
+
this.mainLastApiError = persistedError;
|
|
444
|
+
if (persistedGeneration !== undefined) {
|
|
445
|
+
this.mainQuotaErrorGeneration = persistedGeneration;
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (this.mainAccountId !== undefined &&
|
|
450
|
+
this.mainLastApiError?.accountIdentity !== undefined &&
|
|
451
|
+
this.mainLastApiError.accountIdentity !== this.mainAccountId) {
|
|
452
|
+
this.mainLastApiError = undefined;
|
|
453
|
+
}
|
|
316
454
|
}
|
|
317
455
|
/**
|
|
318
456
|
* Seed fallback cache entries from persisted account.quota data.
|
|
@@ -321,22 +459,44 @@ export class QuotaManager {
|
|
|
321
459
|
*/
|
|
322
460
|
seedFallbacksFromAccounts(accounts) {
|
|
323
461
|
const checkInterval = getQuotaCheckIntervalMs(this.storage);
|
|
462
|
+
const configuredIds = new Set(accounts.map((account) => account.id));
|
|
463
|
+
for (const accountId of this.fallbackAuthLineages.keys()) {
|
|
464
|
+
if (!configuredIds.has(accountId)) {
|
|
465
|
+
this.clearFallback(accountId);
|
|
466
|
+
this.fallbackAuthLineages.delete(accountId);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
324
469
|
for (const account of accounts) {
|
|
470
|
+
const lineageChanged = this.bindFallbackLineage(account.id, account);
|
|
471
|
+
if (lineageChanged)
|
|
472
|
+
continue;
|
|
325
473
|
if (account.enabled === false)
|
|
326
474
|
continue;
|
|
475
|
+
const persistedError = account.lastQuotaRefreshError;
|
|
476
|
+
const persistedClearAt = quotaSnapshotCheckedAt(account.quota);
|
|
477
|
+
const currentError = this.fallbackApiErrors.get(account.id);
|
|
478
|
+
if (persistedError &&
|
|
479
|
+
quotaBackoffActive(persistedError, this.now()) &&
|
|
480
|
+
persistedClearAt <= (persistedError.checkedAt ?? 0) &&
|
|
481
|
+
(!currentError || persistedError.checkedAt >= currentError.checkedAt)) {
|
|
482
|
+
this.fallbackApiErrors.set(account.id, persistedError);
|
|
483
|
+
}
|
|
484
|
+
else if (currentError && persistedClearAt >= currentError.checkedAt) {
|
|
485
|
+
this.fallbackApiErrors.delete(account.id);
|
|
486
|
+
}
|
|
327
487
|
if (!account.quota)
|
|
328
488
|
continue;
|
|
329
489
|
const checkedAt = quotaSnapshotCheckedAt(account.quota);
|
|
330
490
|
if (checkedAt <= 0)
|
|
331
491
|
continue;
|
|
332
|
-
const existing = this.getFallback(account.id
|
|
492
|
+
const existing = this.getFallback(account.id);
|
|
333
493
|
if (existing && existing.checkedAt >= checkedAt)
|
|
334
494
|
continue;
|
|
335
495
|
this.setFallback(account.id, {
|
|
336
|
-
quota: account.quota,
|
|
496
|
+
quota: { ...account.quota, accountIdentity: account.id },
|
|
337
497
|
refreshAfter: checkedAt + checkInterval,
|
|
338
498
|
checkedAt,
|
|
339
|
-
}, account
|
|
499
|
+
}, account);
|
|
340
500
|
}
|
|
341
501
|
}
|
|
342
502
|
/**
|
|
@@ -344,34 +504,77 @@ export class QuotaManager {
|
|
|
344
504
|
* account — a fallback account's 429 never reports here.
|
|
345
505
|
*/
|
|
346
506
|
isBackedOff() {
|
|
507
|
+
// Unlike cached numbers above, restrictions stay active while identity is
|
|
508
|
+
// ambiguous; only a confirmed account change may release an inherited guard.
|
|
347
509
|
return quotaBackoffActive(this.mainLastApiError, this.now());
|
|
348
510
|
}
|
|
349
511
|
/**
|
|
350
512
|
* Whether a specific fallback account's quota API is in backoff.
|
|
351
513
|
*/
|
|
352
|
-
isFallbackBackedOff(accountId
|
|
353
|
-
if (accessToken) {
|
|
354
|
-
const errorFp = this.fallbackErrorTokenFps.get(accountId);
|
|
355
|
-
if (errorFp !== tokenFingerprint(accessToken))
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
514
|
+
isFallbackBackedOff(accountId) {
|
|
358
515
|
return quotaBackoffActive(this.fallbackApiErrors.get(accountId), this.now());
|
|
359
516
|
}
|
|
360
517
|
getLastApiError() {
|
|
361
518
|
return this.mainLastApiError;
|
|
362
519
|
}
|
|
520
|
+
getMainQuotaErrorGeneration() {
|
|
521
|
+
return this.mainQuotaErrorGeneration;
|
|
522
|
+
}
|
|
523
|
+
getMainQuotaIdentityGeneration() {
|
|
524
|
+
return this.mainGeneration;
|
|
525
|
+
}
|
|
526
|
+
getMainQuotaErrorClearedAt() {
|
|
527
|
+
return this.mainQuotaErrorClearedAt;
|
|
528
|
+
}
|
|
529
|
+
clearMainBackoff() {
|
|
530
|
+
if (!this.mainLastApiError)
|
|
531
|
+
return undefined;
|
|
532
|
+
this.mainLastApiError = undefined;
|
|
533
|
+
this.mainQuotaErrorGeneration += 1;
|
|
534
|
+
this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, this.now());
|
|
535
|
+
return this.mainQuotaErrorGeneration;
|
|
536
|
+
}
|
|
363
537
|
// =========================================================================
|
|
364
538
|
// Private
|
|
365
539
|
// =========================================================================
|
|
366
540
|
/** Minimum gap between consecutive quota API calls (ms). */
|
|
367
541
|
static API_CALL_GAP_MS = 1_000;
|
|
368
|
-
static fallbackInflightKey(accountId
|
|
369
|
-
return
|
|
542
|
+
static fallbackInflightKey(accountId) {
|
|
543
|
+
return accountId;
|
|
370
544
|
}
|
|
371
545
|
static quotaLockName(accountId) {
|
|
372
546
|
const safeId = accountId.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
373
547
|
return `opencode-fallback-quota-refresh-${safeId || 'account'}`;
|
|
374
548
|
}
|
|
549
|
+
bindFallbackLineage(accountId, account) {
|
|
550
|
+
if (!account)
|
|
551
|
+
return false;
|
|
552
|
+
const hadLineage = this.fallbackAuthLineages.has(accountId);
|
|
553
|
+
const previousLineage = this.fallbackAuthLineages.get(accountId);
|
|
554
|
+
const lineageChanged = hadLineage &&
|
|
555
|
+
account.authLineageId !== undefined &&
|
|
556
|
+
previousLineage !== account.authLineageId;
|
|
557
|
+
if (lineageChanged)
|
|
558
|
+
this.clearFallback(accountId);
|
|
559
|
+
this.fallbackAuthLineages.set(accountId, account.authLineageId);
|
|
560
|
+
return lineageChanged;
|
|
561
|
+
}
|
|
562
|
+
acceptFallbackQuotaObservation(accountId, account) {
|
|
563
|
+
const hasLineage = this.fallbackAuthLineages.has(accountId);
|
|
564
|
+
const currentLineage = this.fallbackAuthLineages.get(accountId);
|
|
565
|
+
const observedLineage = account?.authLineageId;
|
|
566
|
+
if (hasLineage && currentLineage !== observedLineage) {
|
|
567
|
+
logger.trace('quota', 'skipped stale fallback quota observation after lineage change', {
|
|
568
|
+
accountId,
|
|
569
|
+
storedLineage: currentLineage,
|
|
570
|
+
servedLineage: observedLineage,
|
|
571
|
+
});
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
if (!hasLineage)
|
|
575
|
+
this.fallbackAuthLineages.set(accountId, observedLineage);
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
375
578
|
/**
|
|
376
579
|
* Serialize API calls through a shared gate so only one
|
|
377
580
|
* quota API request runs at a time, with a minimum gap
|
|
@@ -396,24 +599,34 @@ export class QuotaManager {
|
|
|
396
599
|
this.apiGate = queued.catch(() => { });
|
|
397
600
|
return queued;
|
|
398
601
|
}
|
|
399
|
-
async _fetchMain(accessToken) {
|
|
400
|
-
const thisFetchFp = tokenFingerprint(accessToken);
|
|
602
|
+
async _fetchMain(mainAccountId, accessToken, generation) {
|
|
401
603
|
return this._enqueueApiFetch(async () => {
|
|
402
604
|
try {
|
|
403
605
|
// Re-check backoff inside gate — may have been set by
|
|
404
606
|
// a preceding queued call while we waited
|
|
405
607
|
if (this.isBackedOff()) {
|
|
406
|
-
if (this.main)
|
|
608
|
+
if (this.main && this.mainAccountId === mainAccountId) {
|
|
407
609
|
return { quota: this.main.quota, fetched: false };
|
|
610
|
+
}
|
|
408
611
|
throw new Error('Quota API rate-limited — try again later');
|
|
409
612
|
}
|
|
613
|
+
if (this.mainQuotaIdentityKnown &&
|
|
614
|
+
this.mainQuotaIdentityStrict &&
|
|
615
|
+
this.mainGeneration !== generation) {
|
|
616
|
+
return {
|
|
617
|
+
quota: { accountIdentity: mainAccountId },
|
|
618
|
+
fetched: false,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
410
621
|
const fileLock = await acquireRefreshFileLock({
|
|
411
622
|
name: 'opencode-main-quota-refresh',
|
|
412
623
|
ttlMs: 30_000,
|
|
413
624
|
});
|
|
414
625
|
if (!fileLock) {
|
|
415
626
|
const cached = this.main;
|
|
416
|
-
if (cached &&
|
|
627
|
+
if (cached &&
|
|
628
|
+
this.mainAccountId === mainAccountId &&
|
|
629
|
+
this.now() < cached.refreshAfter) {
|
|
417
630
|
return { quota: cached.quota, fetched: false };
|
|
418
631
|
}
|
|
419
632
|
throw new Error('Quota refresh is already in progress');
|
|
@@ -426,19 +639,46 @@ export class QuotaManager {
|
|
|
426
639
|
now: this.now,
|
|
427
640
|
});
|
|
428
641
|
const now = this.now();
|
|
429
|
-
|
|
430
|
-
|
|
642
|
+
if (this.mainQuotaIdentityKnown &&
|
|
643
|
+
this.mainQuotaIdentityStrict &&
|
|
644
|
+
this.mainGeneration !== generation) {
|
|
645
|
+
return {
|
|
646
|
+
quota: {
|
|
647
|
+
...quota,
|
|
648
|
+
...(mainAccountId !== undefined && {
|
|
649
|
+
accountIdentity: mainAccountId,
|
|
650
|
+
}),
|
|
651
|
+
},
|
|
652
|
+
fetched: true,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const completedQuota = mergePollCompletionWithNewerHeaders(this.mainAccountId === mainAccountId &&
|
|
656
|
+
(!this.mainQuotaIdentityKnown ||
|
|
657
|
+
!this.mainQuotaIdentityStrict ||
|
|
658
|
+
this.main?.quota.accountIdentity === mainAccountId)
|
|
659
|
+
? this.main?.quota
|
|
660
|
+
: undefined, {
|
|
661
|
+
...quota,
|
|
662
|
+
...(mainAccountId !== undefined && {
|
|
663
|
+
accountIdentity: mainAccountId,
|
|
664
|
+
}),
|
|
665
|
+
});
|
|
666
|
+
this.mainAccountId = mainAccountId;
|
|
431
667
|
this.main = {
|
|
432
668
|
quota: completedQuota,
|
|
433
669
|
refreshAfter: getQuotaNextRefreshAt(completedQuota, this.storage, now),
|
|
434
670
|
checkedAt: completedQuota.checkedAt ?? now,
|
|
435
671
|
};
|
|
436
|
-
this.mainLastApiError
|
|
437
|
-
|
|
672
|
+
if (this.mainLastApiError) {
|
|
673
|
+
this.mainLastApiError = undefined;
|
|
674
|
+
this.mainQuotaErrorGeneration += 1;
|
|
675
|
+
this.mainQuotaErrorClearedAt = mergeMainQuotaErrorClearedAt(this.mainQuotaErrorClearedAt, now);
|
|
676
|
+
}
|
|
677
|
+
this.onMainQuotaFetched?.(completedQuota, now, tokenFingerprint(accessToken), fetchStartedAt, this.mainQuotaErrorGeneration, this.mainQuotaErrorClearedAt);
|
|
438
678
|
return { quota: completedQuota, fetched: true };
|
|
439
679
|
}
|
|
440
680
|
catch (error) {
|
|
441
|
-
this._handleMainFetchError(error);
|
|
681
|
+
this._handleMainFetchError(error, generation);
|
|
442
682
|
throw error;
|
|
443
683
|
}
|
|
444
684
|
finally {
|
|
@@ -446,19 +686,22 @@ export class QuotaManager {
|
|
|
446
686
|
}
|
|
447
687
|
}
|
|
448
688
|
finally {
|
|
449
|
-
if (this.
|
|
689
|
+
if (this.inflightMainAccountId === mainAccountId) {
|
|
450
690
|
this.inflightMain = null;
|
|
451
|
-
this.
|
|
691
|
+
this.inflightMainAccountId = undefined;
|
|
452
692
|
}
|
|
453
693
|
}
|
|
454
694
|
});
|
|
455
695
|
}
|
|
456
|
-
async _fetchFallback(accountId, accessToken) {
|
|
696
|
+
async _fetchFallback(accountId, accessToken, generation, account) {
|
|
457
697
|
return this._enqueueApiFetch(async () => {
|
|
458
698
|
try {
|
|
699
|
+
if ((this.fallbackGenerations.get(accountId) ?? 0) !== generation) {
|
|
700
|
+
return { quota: { accountIdentity: accountId }, fetched: false };
|
|
701
|
+
}
|
|
459
702
|
// Re-check backoff inside gate — scoped to this fallback account
|
|
460
|
-
if (this.isFallbackBackedOff(accountId
|
|
461
|
-
const cached = this.getFallback(accountId,
|
|
703
|
+
if (this.isFallbackBackedOff(accountId)) {
|
|
704
|
+
const cached = this.getFallback(accountId, account);
|
|
462
705
|
if (cached)
|
|
463
706
|
return { quota: cached.quota, fetched: false };
|
|
464
707
|
throw new Error('Quota API rate-limited — try again later');
|
|
@@ -468,27 +711,34 @@ export class QuotaManager {
|
|
|
468
711
|
ttlMs: 30_000,
|
|
469
712
|
});
|
|
470
713
|
if (!fileLock) {
|
|
471
|
-
const cached = this.getFallback(accountId,
|
|
714
|
+
const cached = this.getFallback(accountId, account);
|
|
472
715
|
if (cached && this.now() < cached.refreshAfter) {
|
|
473
716
|
return { quota: cached.quota, fetched: false };
|
|
474
717
|
}
|
|
475
718
|
throw new Error('Quota refresh is already in progress');
|
|
476
719
|
}
|
|
477
720
|
try {
|
|
721
|
+
const fetchStartedAt = this.now();
|
|
478
722
|
const quota = await fetchOAuthQuotaSnapshot({
|
|
479
723
|
accessToken,
|
|
480
724
|
fetchImpl: this.fetchImpl,
|
|
481
725
|
now: this.now,
|
|
482
726
|
});
|
|
483
727
|
const now = this.now();
|
|
484
|
-
|
|
728
|
+
if ((this.fallbackGenerations.get(accountId) ?? 0) !== generation) {
|
|
729
|
+
return {
|
|
730
|
+
quota: { ...quota, accountIdentity: accountId },
|
|
731
|
+
fetched: true,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const completedQuota = mergePollCompletionWithNewerHeaders(this.getFallback(accountId, account)?.quota, { ...quota, accountIdentity: accountId });
|
|
485
735
|
this.setFallback(accountId, {
|
|
486
736
|
quota: completedQuota,
|
|
487
737
|
refreshAfter: getQuotaNextRefreshAt(completedQuota, this.storage, now),
|
|
488
738
|
checkedAt: completedQuota.checkedAt ?? now,
|
|
489
|
-
},
|
|
739
|
+
}, account);
|
|
490
740
|
this.fallbackApiErrors.delete(accountId);
|
|
491
|
-
this.
|
|
741
|
+
this.onFallbackQuotaFetched?.(accountId, completedQuota, now, fetchStartedAt);
|
|
492
742
|
return { quota: completedQuota, fetched: true };
|
|
493
743
|
}
|
|
494
744
|
finally {
|
|
@@ -496,12 +746,9 @@ export class QuotaManager {
|
|
|
496
746
|
}
|
|
497
747
|
}
|
|
498
748
|
catch (error) {
|
|
499
|
-
this._handleFallbackFetchError(accountId,
|
|
749
|
+
this._handleFallbackFetchError(accountId, error, generation);
|
|
500
750
|
throw error;
|
|
501
751
|
}
|
|
502
|
-
finally {
|
|
503
|
-
this.inflightFallbacks.delete(QuotaManager.fallbackInflightKey(accountId, accessToken));
|
|
504
|
-
}
|
|
505
752
|
});
|
|
506
753
|
}
|
|
507
754
|
// A 401 is an auth/token problem and a 403 is an account/org policy problem,
|
|
@@ -515,15 +762,24 @@ export class QuotaManager {
|
|
|
515
762
|
return /quota check failed: 401\b/.test(message);
|
|
516
763
|
}
|
|
517
764
|
/** Main quota failure: arms main-only backoff and persists via onApiError. */
|
|
518
|
-
_handleMainFetchError(error) {
|
|
765
|
+
_handleMainFetchError(error, generation) {
|
|
766
|
+
if (this.mainGeneration !== generation)
|
|
767
|
+
return;
|
|
768
|
+
if (this.mainQuotaIdentityKnown &&
|
|
769
|
+
this.mainQuotaIdentityStrict &&
|
|
770
|
+
this.mainAccountId === undefined) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
519
773
|
if (QuotaManager.isAuthError(error))
|
|
520
774
|
return;
|
|
521
775
|
this.mainLastApiError = buildQuotaOperationError({
|
|
522
776
|
error,
|
|
523
777
|
now: this.now(),
|
|
778
|
+
accountIdentity: this.mainAccountId,
|
|
524
779
|
previous: this.mainLastApiError,
|
|
525
780
|
});
|
|
526
|
-
this.
|
|
781
|
+
this.mainQuotaErrorGeneration += 1;
|
|
782
|
+
this.onApiError?.(this.mainLastApiError, this.mainQuotaErrorGeneration);
|
|
527
783
|
}
|
|
528
784
|
/**
|
|
529
785
|
* Fallback quota failure: arms backoff for THIS account only. Never touches
|
|
@@ -531,18 +787,19 @@ export class QuotaManager {
|
|
|
531
787
|
* quota error) — the per-account error is recorded by the caller via the
|
|
532
788
|
* account's lastQuotaRefreshError.
|
|
533
789
|
*/
|
|
534
|
-
_handleFallbackFetchError(accountId,
|
|
790
|
+
_handleFallbackFetchError(accountId, error, generation) {
|
|
791
|
+
if ((this.fallbackGenerations.get(accountId) ?? 0) !== generation)
|
|
792
|
+
return;
|
|
535
793
|
if (QuotaManager.isAuthError(error))
|
|
536
794
|
return;
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
? this.fallbackApiErrors.get(accountId)
|
|
540
|
-
: undefined;
|
|
541
|
-
this.fallbackApiErrors.set(accountId, buildQuotaOperationError({
|
|
795
|
+
const previous = this.fallbackApiErrors.get(accountId);
|
|
796
|
+
const quotaError = buildQuotaOperationError({
|
|
542
797
|
error,
|
|
543
798
|
now: this.now(),
|
|
544
799
|
previous,
|
|
545
|
-
|
|
546
|
-
|
|
800
|
+
accountIdentity: accountId,
|
|
801
|
+
});
|
|
802
|
+
this.fallbackApiErrors.set(accountId, quotaError);
|
|
803
|
+
this.onFallbackApiError?.(accountId, quotaError);
|
|
547
804
|
}
|
|
548
805
|
}
|