@cortexkit/anthropic-auth-core 1.12.2 → 1.14.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/dist/accounts.d.ts +37 -12
- package/dist/accounts.js +270 -36
- package/dist/cachekeep.d.ts +2 -0
- package/dist/cachekeep.js +1 -0
- package/dist/killswitch.d.ts +1 -0
- package/dist/killswitch.js +20 -15
- package/dist/quotas.d.ts +2 -2
- package/dist/quotas.js +10 -0
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type OAuthAccount = AccountBase & {
|
|
|
19
19
|
lastRefreshedAt?: number;
|
|
20
20
|
lastRefreshError?: AccountOperationError;
|
|
21
21
|
lastQuotaRefreshError?: AccountOperationError;
|
|
22
|
-
quota?:
|
|
22
|
+
quota?: OAuthQuotaSnapshot;
|
|
23
23
|
};
|
|
24
24
|
export type ApiKeyAccount = AccountBase & {
|
|
25
25
|
type: 'api';
|
|
@@ -60,8 +60,18 @@ export type AccountQuotaWindow = {
|
|
|
60
60
|
resetsAt?: string;
|
|
61
61
|
checkedAt: number;
|
|
62
62
|
};
|
|
63
|
+
export type AccountScopedQuotaWindow = AccountQuotaWindow & {
|
|
64
|
+
id: string;
|
|
65
|
+
title: string;
|
|
66
|
+
modelId?: string;
|
|
67
|
+
modelName: string;
|
|
68
|
+
};
|
|
69
|
+
export type OAuthQuotaSnapshot = Partial<Record<QuotaWindowName, AccountQuotaWindow>> & {
|
|
70
|
+
scoped?: AccountScopedQuotaWindow[];
|
|
71
|
+
checkedAt?: number;
|
|
72
|
+
};
|
|
63
73
|
export type RoutingMode = 'main-first' | 'fallback-first';
|
|
64
|
-
export type KillswitchThresholds = Partial<Record<QuotaWindowName | '5h' | '1w', number>>;
|
|
74
|
+
export type KillswitchThresholds = Partial<Record<QuotaWindowName | '5h' | '1w' | 'scoped', number>>;
|
|
65
75
|
export type KillswitchConfig = {
|
|
66
76
|
enabled?: boolean;
|
|
67
77
|
/** Thresholds for the main OAuth account (remaining % below which the account is killed). */
|
|
@@ -164,7 +174,6 @@ export type AccountStateSaveScope = {
|
|
|
164
174
|
mainRefresh?: boolean;
|
|
165
175
|
accounts?: true | string[];
|
|
166
176
|
};
|
|
167
|
-
export type OAuthQuotaSnapshot = Partial<Record<QuotaWindowName, AccountQuotaWindow>>;
|
|
168
177
|
export type AccountManagerOptions = {
|
|
169
178
|
now?: () => number;
|
|
170
179
|
fetchImpl?: typeof fetch;
|
|
@@ -253,21 +262,36 @@ export declare function getPersistedMainQuota(storage: AccountStorage | null): {
|
|
|
253
262
|
* Returns 0 when disabled (default).
|
|
254
263
|
*/
|
|
255
264
|
export declare function getQuotaRefreshEveryNRequests(storage: AccountStorage | null): number;
|
|
265
|
+
export declare function getScopedQuotaWindowForModel(quota: OAuthQuotaSnapshot | undefined, model: unknown): AccountScopedQuotaWindow | undefined;
|
|
266
|
+
export declare function quotaSnapshotModelScopeIsExhausted(quota: OAuthQuotaSnapshot | undefined, model: unknown): boolean;
|
|
267
|
+
export declare function quotaSnapshotPassesModelScope(quota: OAuthQuotaSnapshot | undefined, model: unknown): boolean;
|
|
256
268
|
export declare function quotaSnapshotPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null): boolean;
|
|
257
|
-
export declare const DEFAULT_KILLSWITCH_THRESHOLDS: Record<QuotaWindowName, number>;
|
|
269
|
+
export declare const DEFAULT_KILLSWITCH_THRESHOLDS: Record<QuotaWindowName | 'scoped', number>;
|
|
270
|
+
export declare function normalizeKillswitchThresholds(thresholds: KillswitchThresholds | undefined): Record<QuotaWindowName | 'scoped', number>;
|
|
258
271
|
export declare function isKillswitchEnabled(storage: AccountStorage | null): boolean;
|
|
272
|
+
export declare function getKillswitchThresholdsForAccount(storage: AccountStorage | null, accountId?: string): Record<QuotaWindowName | 'scoped', number>;
|
|
259
273
|
/**
|
|
260
274
|
* Returns true if the account's quota is above its killswitch threshold.
|
|
261
275
|
* When killswitch is disabled, always returns true.
|
|
276
|
+
*
|
|
277
|
+
* When `modelId` is provided, the per-account `scoped` threshold is also
|
|
278
|
+
* evaluated against the quota window matching that model — additive to the
|
|
279
|
+
* 5h/7d check. A model with no matching scoped window is unaffected.
|
|
262
280
|
*/
|
|
263
|
-
export declare function killswitchPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, accountId?: string): boolean;
|
|
281
|
+
export declare function killswitchPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, accountId?: string, modelId?: string): boolean;
|
|
264
282
|
/**
|
|
265
283
|
* Find the earliest reset time across all accounts' quota windows.
|
|
266
284
|
* Returns seconds from `now` until that reset, or 300 as a fallback.
|
|
285
|
+
*
|
|
286
|
+
* When `scopedModelId` is provided, ONLY the matched scoped window's
|
|
287
|
+
* `resetsAt` is considered — the 5h/7d resets are intentionally ignored
|
|
288
|
+
* so the retry hint reflects the weekly reset, not the sooner 5h reset
|
|
289
|
+
* (which would cause a retry-storm against a block that won't clear for
|
|
290
|
+
* days). With `scopedModelId` undefined, the 5h/7d behavior is unchanged.
|
|
267
291
|
*/
|
|
268
292
|
export declare function killswitchRetryAfterSeconds(mainQuota: OAuthQuotaSnapshot | undefined, fallbackAccounts: Array<{
|
|
269
293
|
quota?: OAuthQuotaSnapshot;
|
|
270
|
-
}>, now: number): number;
|
|
294
|
+
}>, now: number, scopedModelId?: string): number;
|
|
271
295
|
export declare function getKillswitchConfig(storage: AccountStorage | null): KillswitchConfig;
|
|
272
296
|
export declare function setKillswitchPersistent(config: KillswitchConfig, path?: string): Promise<AccountStorage>;
|
|
273
297
|
export declare function removeAccountPersistent(id: string, path?: string): Promise<boolean>;
|
|
@@ -279,10 +303,7 @@ export declare function fetchOAuthQuotaSnapshot(input: {
|
|
|
279
303
|
accessToken: string;
|
|
280
304
|
fetchImpl?: typeof fetch;
|
|
281
305
|
now?: () => number;
|
|
282
|
-
}): Promise<
|
|
283
|
-
five_hour: AccountQuotaWindow | undefined;
|
|
284
|
-
seven_day: AccountQuotaWindow | undefined;
|
|
285
|
-
}>;
|
|
306
|
+
}): Promise<OAuthQuotaSnapshot>;
|
|
286
307
|
export declare function upsertAccount(storage: AccountStorage, account: FallbackAccount): void;
|
|
287
308
|
export declare function removeAccount(storage: AccountStorage, id: string): boolean;
|
|
288
309
|
export declare function reorderAccounts(storage: AccountStorage, orderedIds: string[]): void;
|
|
@@ -306,9 +327,13 @@ export declare class FallbackAccountManager {
|
|
|
306
327
|
save(storage: AccountStorage, accountIds?: string[]): Promise<void>;
|
|
307
328
|
startBackgroundRefresh(): void;
|
|
308
329
|
stopBackgroundRefresh(): void;
|
|
309
|
-
getUsableFallbackAccounts(existingStorage?: AccountStorage | null
|
|
330
|
+
getUsableFallbackAccounts(existingStorage?: AccountStorage | null, options?: {
|
|
331
|
+
modelId?: string;
|
|
332
|
+
}): Promise<OAuthAccount[]>;
|
|
310
333
|
markUsed(account: FallbackAccount): Promise<void>;
|
|
311
|
-
accountPassesQuotaPolicy(account: OAuthAccount, storage: AccountStorage | null
|
|
334
|
+
accountPassesQuotaPolicy(account: OAuthAccount, storage: AccountStorage | null, options?: {
|
|
335
|
+
modelId?: string;
|
|
336
|
+
}): boolean;
|
|
312
337
|
/**
|
|
313
338
|
* Return the account with its quota overlaid from the unified QuotaManager
|
|
314
339
|
* cache (token-bound) when available, so quota-policy decisions use the same
|
package/dist/accounts.js
CHANGED
|
@@ -151,28 +151,74 @@ function normalizeOperationError(value) {
|
|
|
151
151
|
permanent: typeof value.permanent === 'boolean' ? value.permanent : undefined,
|
|
152
152
|
};
|
|
153
153
|
}
|
|
154
|
+
function normalizeQuotaWindow(value) {
|
|
155
|
+
if (!isRecord(value))
|
|
156
|
+
return undefined;
|
|
157
|
+
const usedPercent = Number(value.usedPercent);
|
|
158
|
+
const remainingPercent = Number(value.remainingPercent);
|
|
159
|
+
const checkedAt = Number(value.checkedAt);
|
|
160
|
+
if (!Number.isFinite(usedPercent) ||
|
|
161
|
+
!Number.isFinite(remainingPercent) ||
|
|
162
|
+
!Number.isFinite(checkedAt)) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
usedPercent,
|
|
167
|
+
remainingPercent,
|
|
168
|
+
checkedAt,
|
|
169
|
+
resetsAt: typeof value.resetsAt === 'string' ? value.resetsAt : undefined,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
154
172
|
function normalizeQuota(value) {
|
|
155
173
|
if (!isRecord(value))
|
|
156
174
|
return undefined;
|
|
157
175
|
const quota = {};
|
|
158
176
|
for (const key of ['five_hour', 'seven_day']) {
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
177
|
+
const normalized = normalizeQuotaWindow(value[key]);
|
|
178
|
+
if (normalized)
|
|
179
|
+
quota[key] = normalized;
|
|
180
|
+
}
|
|
181
|
+
// Persist a top-level snapshot checkedAt through normalize so the
|
|
182
|
+
// mergeAccountRuntimeState freshness comparison stays meaningful when the
|
|
183
|
+
// snapshot has no per-window checkedAt (e.g. {scoped:[]}). Pre-feature
|
|
184
|
+
// inputs without this key are unaffected — only on-disk snapshots that
|
|
185
|
+
// already carry it reach this branch.
|
|
186
|
+
if (typeof value.checkedAt === 'number' && Number.isFinite(value.checkedAt)) {
|
|
187
|
+
quota.checkedAt = value.checkedAt;
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(value.scoped)) {
|
|
190
|
+
const scoped = value.scoped
|
|
191
|
+
.map((entry) => {
|
|
192
|
+
if (!isRecord(entry))
|
|
193
|
+
return undefined;
|
|
194
|
+
const window = normalizeQuotaWindow(entry);
|
|
195
|
+
if (!window)
|
|
196
|
+
return undefined;
|
|
197
|
+
if (typeof entry.id !== 'string' || !entry.id.trim())
|
|
198
|
+
return undefined;
|
|
199
|
+
if (typeof entry.title !== 'string' || !entry.title.trim()) {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
if (typeof entry.modelName !== 'string' || !entry.modelName.trim()) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const modelId = typeof entry.modelId === 'string' && entry.modelId.trim()
|
|
206
|
+
? entry.modelId.trim()
|
|
207
|
+
: undefined;
|
|
208
|
+
return {
|
|
209
|
+
...window,
|
|
210
|
+
id: entry.id.trim(),
|
|
211
|
+
title: entry.title.trim(),
|
|
212
|
+
...(modelId && { modelId }),
|
|
213
|
+
modelName: entry.modelName.trim(),
|
|
214
|
+
};
|
|
215
|
+
})
|
|
216
|
+
.filter((entry) => entry != null);
|
|
217
|
+
// Preserve empty `[]` so a downstream reader can distinguish "scoped
|
|
218
|
+
// owned by anthropic-auth, none visible" from "no scoped data on this
|
|
219
|
+
// snapshot". Pre-feature inputs without a `scoped` key are not affected
|
|
220
|
+
// — only inputs that already carried an array reach this line.
|
|
221
|
+
quota.scoped = scoped;
|
|
176
222
|
}
|
|
177
223
|
return Object.keys(quota).length ? quota : undefined;
|
|
178
224
|
}
|
|
@@ -225,6 +271,43 @@ async function readJsonIfPresent(path) {
|
|
|
225
271
|
function objectWithDefinedEntries(value) {
|
|
226
272
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
227
273
|
}
|
|
274
|
+
function numericField(value) {
|
|
275
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
276
|
+
}
|
|
277
|
+
function accountCredentialTimestamp(value) {
|
|
278
|
+
return Math.max(numericField(value.lastRefreshedAt), numericField(value.lastUsed), numericField(value.addedAt));
|
|
279
|
+
}
|
|
280
|
+
function legacyConfigCredentialsAreNewer(account, stateAccount) {
|
|
281
|
+
if (account.type !== 'oauth')
|
|
282
|
+
return false;
|
|
283
|
+
if (typeof account.refresh !== 'string' || !account.refresh.trim()) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const tokenChanged = Boolean((typeof account.access === 'string' &&
|
|
287
|
+
typeof stateAccount.access === 'string' &&
|
|
288
|
+
account.access !== stateAccount.access) ||
|
|
289
|
+
(typeof account.refresh === 'string' &&
|
|
290
|
+
typeof stateAccount.refresh === 'string' &&
|
|
291
|
+
account.refresh !== stateAccount.refresh));
|
|
292
|
+
if (!tokenChanged)
|
|
293
|
+
return false;
|
|
294
|
+
return (accountCredentialTimestamp(account) >
|
|
295
|
+
accountCredentialTimestamp(stateAccount));
|
|
296
|
+
}
|
|
297
|
+
function mergeConfigAccountAndState(account, stateAccount) {
|
|
298
|
+
if (legacyConfigCredentialsAreNewer(account, stateAccount)) {
|
|
299
|
+
const merged = { ...stateAccount, ...account };
|
|
300
|
+
const configTimestamp = accountCredentialTimestamp(account);
|
|
301
|
+
if (configTimestamp > numericField(merged.lastRefreshedAt)) {
|
|
302
|
+
merged.lastRefreshedAt = configTimestamp;
|
|
303
|
+
}
|
|
304
|
+
delete merged.quota;
|
|
305
|
+
delete merged.lastRefreshError;
|
|
306
|
+
delete merged.lastQuotaRefreshError;
|
|
307
|
+
return merged;
|
|
308
|
+
}
|
|
309
|
+
return { ...account, ...stateAccount };
|
|
310
|
+
}
|
|
228
311
|
function mergeConfigAndState(configValue, stateValue) {
|
|
229
312
|
if (!isRecord(configValue))
|
|
230
313
|
return configValue;
|
|
@@ -242,7 +325,7 @@ function mergeConfigAndState(configValue, stateValue) {
|
|
|
242
325
|
const stateAccount = typeof account.id === 'string' && isRecord(stateAccounts[account.id])
|
|
243
326
|
? stateAccounts[account.id]
|
|
244
327
|
: {};
|
|
245
|
-
return
|
|
328
|
+
return mergeConfigAccountAndState(account, stateAccount);
|
|
246
329
|
})
|
|
247
330
|
: [];
|
|
248
331
|
return {
|
|
@@ -312,7 +395,7 @@ function accountRuntimeState(account) {
|
|
|
312
395
|
});
|
|
313
396
|
}
|
|
314
397
|
function quotaSnapshotCheckedAt(quota) {
|
|
315
|
-
return Math.max(quota?.five_hour?.checkedAt ?? 0, quota?.seven_day?.checkedAt ?? 0);
|
|
398
|
+
return Math.max(quota?.five_hour?.checkedAt ?? 0, quota?.seven_day?.checkedAt ?? 0, ...(quota?.scoped?.map((window) => window.checkedAt) ?? []), quota?.checkedAt ?? 0);
|
|
316
399
|
}
|
|
317
400
|
function mergeAccountRuntimeState(existing, incoming) {
|
|
318
401
|
if (!isRecord(existing))
|
|
@@ -320,10 +403,13 @@ function mergeAccountRuntimeState(existing, incoming) {
|
|
|
320
403
|
const existingEntry = existing;
|
|
321
404
|
const existingQuotaCheckedAt = quotaSnapshotCheckedAt(existingEntry.quota);
|
|
322
405
|
const incomingQuotaCheckedAt = quotaSnapshotCheckedAt(incoming.quota);
|
|
406
|
+
const tokenChanged = Boolean((existingEntry.access &&
|
|
407
|
+
incoming.access &&
|
|
408
|
+
existingEntry.access !== incoming.access) ||
|
|
409
|
+
(existingEntry.refresh &&
|
|
410
|
+
incoming.refresh &&
|
|
411
|
+
existingEntry.refresh !== incoming.refresh));
|
|
323
412
|
if (existingQuotaCheckedAt > incomingQuotaCheckedAt) {
|
|
324
|
-
const tokenChanged = Boolean(existingEntry.access &&
|
|
325
|
-
incoming.access &&
|
|
326
|
-
existingEntry.access !== incoming.access);
|
|
327
413
|
const existingRefreshAt = existingEntry.lastRefreshedAt ?? 0;
|
|
328
414
|
const incomingRefreshAt = incoming.lastRefreshedAt ?? 0;
|
|
329
415
|
if (tokenChanged && incomingRefreshAt <= existingRefreshAt) {
|
|
@@ -335,9 +421,27 @@ function mergeAccountRuntimeState(existing, incoming) {
|
|
|
335
421
|
}
|
|
336
422
|
return merged;
|
|
337
423
|
}
|
|
424
|
+
const merged = { ...existingEntry, ...incoming };
|
|
425
|
+
if (tokenChanged) {
|
|
426
|
+
if ('quota' in incoming) {
|
|
427
|
+
merged.quota = existingEntry.quota;
|
|
428
|
+
if ('lastQuotaRefreshError' in existingEntry) {
|
|
429
|
+
merged.lastQuotaRefreshError = existingEntry.lastQuotaRefreshError;
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
delete merged.lastQuotaRefreshError;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
delete merged.quota;
|
|
437
|
+
delete merged.lastQuotaRefreshError;
|
|
438
|
+
}
|
|
439
|
+
if (!('lastRefreshError' in incoming))
|
|
440
|
+
delete merged.lastRefreshError;
|
|
441
|
+
return merged;
|
|
442
|
+
}
|
|
338
443
|
return {
|
|
339
|
-
...
|
|
340
|
-
...incoming,
|
|
444
|
+
...merged,
|
|
341
445
|
quota: existingEntry.quota,
|
|
342
446
|
lastQuotaRefreshError: existingEntry.lastQuotaRefreshError,
|
|
343
447
|
};
|
|
@@ -1003,6 +1107,37 @@ function failClosedOnUnknownQuota(storage) {
|
|
|
1003
1107
|
return (storage?.quota?.failClosedOnUnknownQuota ??
|
|
1004
1108
|
DEFAULT_FAIL_CLOSED_ON_UNKNOWN_QUOTA);
|
|
1005
1109
|
}
|
|
1110
|
+
function scopedQuotaModelKey(model) {
|
|
1111
|
+
if (typeof model !== 'string')
|
|
1112
|
+
return null;
|
|
1113
|
+
const normalized = model.toLowerCase();
|
|
1114
|
+
if (normalized.includes('fable'))
|
|
1115
|
+
return 'fable';
|
|
1116
|
+
if (normalized.includes('mythos'))
|
|
1117
|
+
return 'mythos';
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
export function getScopedQuotaWindowForModel(quota, model) {
|
|
1121
|
+
const key = scopedQuotaModelKey(model);
|
|
1122
|
+
if (!key)
|
|
1123
|
+
return undefined;
|
|
1124
|
+
return quota?.scoped?.find((window) => {
|
|
1125
|
+
const haystack = [window.modelId, window.modelName, window.title]
|
|
1126
|
+
.filter((value) => typeof value === 'string')
|
|
1127
|
+
.join(' ')
|
|
1128
|
+
.toLowerCase();
|
|
1129
|
+
return haystack.includes(key);
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
export function quotaSnapshotModelScopeIsExhausted(quota, model) {
|
|
1133
|
+
const window = getScopedQuotaWindowForModel(quota, model);
|
|
1134
|
+
return Boolean(window &&
|
|
1135
|
+
Number.isFinite(window.remainingPercent) &&
|
|
1136
|
+
window.remainingPercent <= 0);
|
|
1137
|
+
}
|
|
1138
|
+
export function quotaSnapshotPassesModelScope(quota, model) {
|
|
1139
|
+
return !quotaSnapshotModelScopeIsExhausted(quota, model);
|
|
1140
|
+
}
|
|
1006
1141
|
export function quotaSnapshotPassesPolicy(quota, storage) {
|
|
1007
1142
|
if (!quotaEnabled(storage))
|
|
1008
1143
|
return true;
|
|
@@ -1026,10 +1161,12 @@ export function quotaSnapshotPassesPolicy(quota, storage) {
|
|
|
1026
1161
|
export const DEFAULT_KILLSWITCH_THRESHOLDS = {
|
|
1027
1162
|
five_hour: 5,
|
|
1028
1163
|
seven_day: 10,
|
|
1164
|
+
scoped: 0,
|
|
1029
1165
|
};
|
|
1030
|
-
function normalizeKillswitchThresholds(thresholds) {
|
|
1166
|
+
export function normalizeKillswitchThresholds(thresholds) {
|
|
1031
1167
|
const fiveHour = thresholds?.five_hour ?? thresholds?.['5h'];
|
|
1032
1168
|
const sevenDay = thresholds?.seven_day ?? thresholds?.['1w'];
|
|
1169
|
+
const scoped = thresholds?.scoped;
|
|
1033
1170
|
return {
|
|
1034
1171
|
five_hour: typeof fiveHour === 'number' && Number.isFinite(fiveHour)
|
|
1035
1172
|
? fiveHour
|
|
@@ -1037,12 +1174,15 @@ function normalizeKillswitchThresholds(thresholds) {
|
|
|
1037
1174
|
seven_day: typeof sevenDay === 'number' && Number.isFinite(sevenDay)
|
|
1038
1175
|
? sevenDay
|
|
1039
1176
|
: DEFAULT_KILLSWITCH_THRESHOLDS.seven_day,
|
|
1177
|
+
scoped: typeof scoped === 'number' && Number.isFinite(scoped)
|
|
1178
|
+
? scoped
|
|
1179
|
+
: DEFAULT_KILLSWITCH_THRESHOLDS.scoped,
|
|
1040
1180
|
};
|
|
1041
1181
|
}
|
|
1042
1182
|
export function isKillswitchEnabled(storage) {
|
|
1043
1183
|
return storage?.killswitch?.enabled === true;
|
|
1044
1184
|
}
|
|
1045
|
-
function getKillswitchThresholdsForAccount(storage, accountId) {
|
|
1185
|
+
export function getKillswitchThresholdsForAccount(storage, accountId) {
|
|
1046
1186
|
if (!storage?.killswitch)
|
|
1047
1187
|
return DEFAULT_KILLSWITCH_THRESHOLDS;
|
|
1048
1188
|
if (accountId && storage.killswitch.accounts?.[accountId]) {
|
|
@@ -1053,8 +1193,12 @@ function getKillswitchThresholdsForAccount(storage, accountId) {
|
|
|
1053
1193
|
/**
|
|
1054
1194
|
* Returns true if the account's quota is above its killswitch threshold.
|
|
1055
1195
|
* When killswitch is disabled, always returns true.
|
|
1196
|
+
*
|
|
1197
|
+
* When `modelId` is provided, the per-account `scoped` threshold is also
|
|
1198
|
+
* evaluated against the quota window matching that model — additive to the
|
|
1199
|
+
* 5h/7d check. A model with no matching scoped window is unaffected.
|
|
1056
1200
|
*/
|
|
1057
|
-
export function killswitchPassesPolicy(quota, storage, accountId) {
|
|
1201
|
+
export function killswitchPassesPolicy(quota, storage, accountId, modelId) {
|
|
1058
1202
|
if (!isKillswitchEnabled(storage))
|
|
1059
1203
|
return true;
|
|
1060
1204
|
const thresholds = getKillswitchThresholdsForAccount(storage, accountId);
|
|
@@ -1075,6 +1219,22 @@ export function killswitchPassesPolicy(quota, storage, accountId) {
|
|
|
1075
1219
|
if (window.remainingPercent < thresholds[key])
|
|
1076
1220
|
return false;
|
|
1077
1221
|
}
|
|
1222
|
+
// Scoped check is additive to the 5h/7d evaluation above and is an
|
|
1223
|
+
// INDEPENDENT block reason — it must run before the unknown-window
|
|
1224
|
+
// fail-closed decision, so an exhausted scoped window blocks even when
|
|
1225
|
+
// 5h/7d is missing/non-finite (the latter only changes the fall-through
|
|
1226
|
+
// for accounts that did not already block on scoped). A missing scoped
|
|
1227
|
+
// window (no carve-out for this model) is not "unknown quota" — only a
|
|
1228
|
+
// PRESENT window at/below threshold blocks. The comparison is inclusive
|
|
1229
|
+
// (`<=`) so the default 0 fires at exhaustion.
|
|
1230
|
+
if (modelId) {
|
|
1231
|
+
const scopedWindow = getScopedQuotaWindowForModel(quota, modelId);
|
|
1232
|
+
if (scopedWindow &&
|
|
1233
|
+
Number.isFinite(scopedWindow.remainingPercent) &&
|
|
1234
|
+
scopedWindow.remainingPercent <= thresholds.scoped) {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1078
1238
|
if (sawUnknownWindow)
|
|
1079
1239
|
return !failClosedOnUnknownQuota(storage);
|
|
1080
1240
|
return true;
|
|
@@ -1082,13 +1242,20 @@ export function killswitchPassesPolicy(quota, storage, accountId) {
|
|
|
1082
1242
|
/**
|
|
1083
1243
|
* Find the earliest reset time across all accounts' quota windows.
|
|
1084
1244
|
* Returns seconds from `now` until that reset, or 300 as a fallback.
|
|
1245
|
+
*
|
|
1246
|
+
* When `scopedModelId` is provided, ONLY the matched scoped window's
|
|
1247
|
+
* `resetsAt` is considered — the 5h/7d resets are intentionally ignored
|
|
1248
|
+
* so the retry hint reflects the weekly reset, not the sooner 5h reset
|
|
1249
|
+
* (which would cause a retry-storm against a block that won't clear for
|
|
1250
|
+
* days). With `scopedModelId` undefined, the 5h/7d behavior is unchanged.
|
|
1085
1251
|
*/
|
|
1086
|
-
export function killswitchRetryAfterSeconds(mainQuota, fallbackAccounts, now) {
|
|
1252
|
+
export function killswitchRetryAfterSeconds(mainQuota, fallbackAccounts, now, scopedModelId) {
|
|
1087
1253
|
const resetTimes = [];
|
|
1088
1254
|
const allQuotas = [mainQuota, ...fallbackAccounts.map((a) => a.quota)];
|
|
1089
1255
|
for (const quota of allQuotas) {
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1256
|
+
if (scopedModelId) {
|
|
1257
|
+
const scopedWindow = getScopedQuotaWindowForModel(quota, scopedModelId);
|
|
1258
|
+
const resetStr = scopedWindow?.resetsAt;
|
|
1092
1259
|
if (!resetStr)
|
|
1093
1260
|
continue;
|
|
1094
1261
|
const resetTime = Date.parse(resetStr);
|
|
@@ -1096,6 +1263,17 @@ export function killswitchRetryAfterSeconds(mainQuota, fallbackAccounts, now) {
|
|
|
1096
1263
|
resetTimes.push(resetTime);
|
|
1097
1264
|
}
|
|
1098
1265
|
}
|
|
1266
|
+
else {
|
|
1267
|
+
for (const key of ['five_hour', 'seven_day']) {
|
|
1268
|
+
const resetStr = quota?.[key]?.resetsAt;
|
|
1269
|
+
if (!resetStr)
|
|
1270
|
+
continue;
|
|
1271
|
+
const resetTime = Date.parse(resetStr);
|
|
1272
|
+
if (Number.isFinite(resetTime) && resetTime > now) {
|
|
1273
|
+
resetTimes.push(resetTime);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1099
1277
|
}
|
|
1100
1278
|
if (!resetTimes.length)
|
|
1101
1279
|
return 300;
|
|
@@ -1226,6 +1404,52 @@ function clampPercent(value) {
|
|
|
1226
1404
|
return 100;
|
|
1227
1405
|
return value;
|
|
1228
1406
|
}
|
|
1407
|
+
function nonEmptyString(value) {
|
|
1408
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
1409
|
+
}
|
|
1410
|
+
function slugForQuotaIdentity(value) {
|
|
1411
|
+
return value
|
|
1412
|
+
.toLowerCase()
|
|
1413
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1414
|
+
.replace(/^-+|-+$/g, '');
|
|
1415
|
+
}
|
|
1416
|
+
function mapScopedWeeklyLimits(limits, checkedAt) {
|
|
1417
|
+
if (!Array.isArray(limits))
|
|
1418
|
+
return [];
|
|
1419
|
+
const seen = new Set();
|
|
1420
|
+
const scoped = [];
|
|
1421
|
+
for (const limit of limits) {
|
|
1422
|
+
if (limit?.kind !== 'weekly_scoped' || limit.group !== 'weekly')
|
|
1423
|
+
continue;
|
|
1424
|
+
if (typeof limit.percent !== 'number' || !Number.isFinite(limit.percent)) {
|
|
1425
|
+
continue;
|
|
1426
|
+
}
|
|
1427
|
+
const modelName = nonEmptyString(limit.scope?.model?.display_name);
|
|
1428
|
+
if (!modelName)
|
|
1429
|
+
continue;
|
|
1430
|
+
const identity = nonEmptyString(limit.scope?.model?.id) ?? modelName;
|
|
1431
|
+
const slug = slugForQuotaIdentity(identity);
|
|
1432
|
+
if (!slug)
|
|
1433
|
+
continue;
|
|
1434
|
+
const id = `claude-weekly-scoped-${slug}`;
|
|
1435
|
+
if (seen.has(id))
|
|
1436
|
+
continue;
|
|
1437
|
+
seen.add(id);
|
|
1438
|
+
const usedPercent = clampPercent(limit.percent);
|
|
1439
|
+
const modelId = nonEmptyString(limit.scope?.model?.id);
|
|
1440
|
+
scoped.push({
|
|
1441
|
+
id,
|
|
1442
|
+
title: `${modelName} only`,
|
|
1443
|
+
...(modelId && { modelId }),
|
|
1444
|
+
modelName,
|
|
1445
|
+
usedPercent,
|
|
1446
|
+
remainingPercent: clampPercent(100 - usedPercent),
|
|
1447
|
+
resetsAt: limit.resets_at,
|
|
1448
|
+
checkedAt,
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
return scoped;
|
|
1452
|
+
}
|
|
1229
1453
|
function mapUsageWindow(window, checkedAt) {
|
|
1230
1454
|
if (typeof window?.utilization !== 'number')
|
|
1231
1455
|
return undefined;
|
|
@@ -1264,6 +1488,8 @@ export async function fetchOAuthQuotaSnapshot(input) {
|
|
|
1264
1488
|
return {
|
|
1265
1489
|
five_hour: mapUsageWindow(usage.five_hour, checkedAt),
|
|
1266
1490
|
seven_day: mapUsageWindow(usage.seven_day, checkedAt),
|
|
1491
|
+
scoped: mapScopedWeeklyLimits(usage.limits, checkedAt),
|
|
1492
|
+
checkedAt,
|
|
1267
1493
|
};
|
|
1268
1494
|
}
|
|
1269
1495
|
function updateStoredAccount(storage, account) {
|
|
@@ -1408,7 +1634,7 @@ export class FallbackAccountManager {
|
|
|
1408
1634
|
this.refreshTimer = null;
|
|
1409
1635
|
this.quotaTimer = null;
|
|
1410
1636
|
}
|
|
1411
|
-
async getUsableFallbackAccounts(existingStorage) {
|
|
1637
|
+
async getUsableFallbackAccounts(existingStorage, options = {}) {
|
|
1412
1638
|
const storage = existingStorage !== undefined ? existingStorage : await this.load();
|
|
1413
1639
|
if (!storage)
|
|
1414
1640
|
return [];
|
|
@@ -1444,7 +1670,9 @@ export class FallbackAccountManager {
|
|
|
1444
1670
|
// Single source of truth: evaluate quota policy from the unified
|
|
1445
1671
|
// QuotaManager cache (the same source as the staleness check above) so
|
|
1446
1672
|
// an active-route refresh that updated only the cache is not ignored.
|
|
1447
|
-
if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(next), storage
|
|
1673
|
+
if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(next), storage, {
|
|
1674
|
+
modelId: options.modelId,
|
|
1675
|
+
}))
|
|
1448
1676
|
usable.push(next);
|
|
1449
1677
|
}
|
|
1450
1678
|
catch (error) {
|
|
@@ -1453,9 +1681,14 @@ export class FallbackAccountManager {
|
|
|
1453
1681
|
accountId: account.id,
|
|
1454
1682
|
error: formatErrorMessage(error),
|
|
1455
1683
|
});
|
|
1456
|
-
|
|
1684
|
+
if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(account), storage, {
|
|
1685
|
+
modelId: options.modelId,
|
|
1686
|
+
})) {
|
|
1687
|
+
usable.push(account);
|
|
1688
|
+
}
|
|
1457
1689
|
}
|
|
1458
|
-
else if (!failClosedOnUnknownQuota(storage)
|
|
1690
|
+
else if (!failClosedOnUnknownQuota(storage) &&
|
|
1691
|
+
quotaSnapshotPassesModelScope(account.quota, options.modelId)) {
|
|
1459
1692
|
usable.push(account);
|
|
1460
1693
|
}
|
|
1461
1694
|
}
|
|
@@ -1474,8 +1707,9 @@ export class FallbackAccountManager {
|
|
|
1474
1707
|
stored.lastUsed = this.now();
|
|
1475
1708
|
await this.save(storage);
|
|
1476
1709
|
}
|
|
1477
|
-
accountPassesQuotaPolicy(account, storage) {
|
|
1478
|
-
return quotaSnapshotPassesPolicy(account.quota, storage)
|
|
1710
|
+
accountPassesQuotaPolicy(account, storage, options = {}) {
|
|
1711
|
+
return (quotaSnapshotPassesPolicy(account.quota, storage) &&
|
|
1712
|
+
quotaSnapshotPassesModelScope(account.quota, options.modelId));
|
|
1479
1713
|
}
|
|
1480
1714
|
/**
|
|
1481
1715
|
* Return the account with its quota overlaid from the unified QuotaManager
|
package/dist/cachekeep.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ export type CacheKeepTarget = {
|
|
|
61
61
|
bodyText: string;
|
|
62
62
|
cacheExpiresAt: number;
|
|
63
63
|
dayKey: string;
|
|
64
|
+
oauthAccountId?: string;
|
|
64
65
|
};
|
|
65
66
|
export declare class CacheKeepManager {
|
|
66
67
|
private readonly options;
|
|
@@ -89,6 +90,7 @@ export declare class CacheKeepManager {
|
|
|
89
90
|
bodyText: string;
|
|
90
91
|
storage: AccountStorage | null;
|
|
91
92
|
cacheMode: string;
|
|
93
|
+
oauthAccountId?: string;
|
|
92
94
|
}): {
|
|
93
95
|
tracked: boolean;
|
|
94
96
|
reason: string;
|
package/dist/cachekeep.js
CHANGED
package/dist/killswitch.d.ts
CHANGED
package/dist/killswitch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_KILLSWITCH_THRESHOLDS as DEFAULT_THRESHOLDS } from "./accounts.js";
|
|
1
|
+
import { DEFAULT_KILLSWITCH_THRESHOLDS as DEFAULT_THRESHOLDS, normalizeKillswitchThresholds, } from "./accounts.js";
|
|
2
2
|
export const KILLSWITCH_COMMAND_NAME = 'claude-killswitch';
|
|
3
3
|
export function parseKillswitchCommandAction(argumentsText) {
|
|
4
4
|
const parts = argumentsText.trim().split(/\s+/).filter(Boolean);
|
|
@@ -11,15 +11,21 @@ export function parseKillswitchCommandAction(argumentsText) {
|
|
|
11
11
|
if (parts[0] === 'set') {
|
|
12
12
|
const entries = [];
|
|
13
13
|
for (let i = 1; i < parts.length; i++) {
|
|
14
|
-
const match = parts[i]?.match(/^([^:]+):(\d+),(\d+)
|
|
14
|
+
const match = parts[i]?.match(/^([^:]+):(\d+),(\d+)(?:,(\d+))?$/);
|
|
15
15
|
if (!match)
|
|
16
16
|
return { type: 'usage' };
|
|
17
|
-
const [, account, fhStr, sdStr] = match;
|
|
18
|
-
|
|
17
|
+
const [, account, fhStr, sdStr, scopedStr] = match;
|
|
18
|
+
const scoped = typeof scopedStr === 'string' && scopedStr.length > 0
|
|
19
|
+
? Number.parseInt(scopedStr, 10)
|
|
20
|
+
: undefined;
|
|
21
|
+
const entry = {
|
|
19
22
|
account,
|
|
20
23
|
fh: Number.parseInt(fhStr, 10),
|
|
21
24
|
sd: Number.parseInt(sdStr, 10),
|
|
22
|
-
}
|
|
25
|
+
};
|
|
26
|
+
if (scoped !== undefined)
|
|
27
|
+
entry.scoped = scoped;
|
|
28
|
+
entries.push(entry);
|
|
23
29
|
}
|
|
24
30
|
if (entries.length === 0)
|
|
25
31
|
return { type: 'usage' };
|
|
@@ -36,17 +42,13 @@ function buildStatusTable(config, accountIds) {
|
|
|
36
42
|
];
|
|
37
43
|
if (enabled) {
|
|
38
44
|
lines.push('');
|
|
39
|
-
lines.push('| Account | 5h threshold | 1w threshold |');
|
|
40
|
-
lines.push('| ------- | ------------ | ------------ |');
|
|
41
|
-
const mainT = config.main
|
|
42
|
-
|
|
43
|
-
const sd = mainT.seven_day ?? mainT['1w'] ?? DEFAULT_THRESHOLDS.seven_day;
|
|
44
|
-
lines.push(`| main | \u2265 ${fh}% | \u2265 ${sd}% |`);
|
|
45
|
+
lines.push('| Account | 5h threshold | 1w threshold | Scoped |');
|
|
46
|
+
lines.push('| ------- | ------------ | ------------ | ------ |');
|
|
47
|
+
const mainT = normalizeKillswitchThresholds(config.main);
|
|
48
|
+
lines.push(`| main | \u2265 ${mainT.five_hour}% | \u2265 ${mainT.seven_day}% | \u2264 ${mainT.scoped}% |`);
|
|
45
49
|
for (const id of accountIds) {
|
|
46
|
-
const t = config.accounts?.[id] ?? config.main
|
|
47
|
-
|
|
48
|
-
const asd = t.seven_day ?? t['1w'] ?? DEFAULT_THRESHOLDS.seven_day;
|
|
49
|
-
lines.push(`| ${id} | \u2265 ${afh}% | \u2265 ${asd}% |`);
|
|
50
|
+
const t = normalizeKillswitchThresholds(config.accounts?.[id] ?? config.main);
|
|
51
|
+
lines.push(`| ${id} | \u2265 ${t.five_hour}% | \u2265 ${t.seven_day}% | \u2264 ${t.scoped}% |`);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
return lines.join('\n');
|
|
@@ -59,6 +61,7 @@ const USAGE_TEXT = [
|
|
|
59
61
|
`/${KILLSWITCH_COMMAND_NAME} on — enable with current or default thresholds`,
|
|
60
62
|
`/${KILLSWITCH_COMMAND_NAME} off — disable`,
|
|
61
63
|
`/${KILLSWITCH_COMMAND_NAME} set all:5,10 — set all accounts to 5h≥5%, 1w≥10%`,
|
|
64
|
+
`/${KILLSWITCH_COMMAND_NAME} set main:3,8,0 — set 5h≥3%, 1w≥8%, scoped≤0%`,
|
|
62
65
|
`/${KILLSWITCH_COMMAND_NAME} set main:3,8 work-alt:5,10 — per-account`,
|
|
63
66
|
'```',
|
|
64
67
|
].join('\n');
|
|
@@ -102,6 +105,8 @@ export function executeKillswitchCommand(input) {
|
|
|
102
105
|
five_hour: entry.fh,
|
|
103
106
|
seven_day: entry.sd,
|
|
104
107
|
};
|
|
108
|
+
if (entry.scoped !== undefined)
|
|
109
|
+
thresholds.scoped = entry.scoped;
|
|
105
110
|
if (entry.account === 'main') {
|
|
106
111
|
updated.main = thresholds;
|
|
107
112
|
}
|
package/dist/quotas.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AccountStorage, OAuthQuotaSnapshot } from './accounts.ts';
|
|
2
2
|
export declare const CLAUDE_QUOTAS_COMMAND_NAME = "claude-quota";
|
|
3
3
|
export type QuotaAccountSummary = {
|
|
4
4
|
name: string;
|
|
@@ -13,7 +13,7 @@ export declare function buildFallbackQuotaSummaries(storage: AccountStorage | nu
|
|
|
13
13
|
name: string;
|
|
14
14
|
role: "fallback";
|
|
15
15
|
enabled: boolean;
|
|
16
|
-
quota:
|
|
16
|
+
quota: OAuthQuotaSnapshot | undefined;
|
|
17
17
|
lastRefreshedAt: number | undefined;
|
|
18
18
|
}[];
|
|
19
19
|
export declare function buildClaudeQuotaSummary(input: {
|
package/dist/quotas.js
CHANGED
|
@@ -52,6 +52,13 @@ function formatWindow(key, window, now) {
|
|
|
52
52
|
`${formatReset(window.resetsAt, now)}, checked ${formatAge(window.checkedAt, now)})`,
|
|
53
53
|
].join('');
|
|
54
54
|
}
|
|
55
|
+
function formatScopedWindow(window, now) {
|
|
56
|
+
return [
|
|
57
|
+
` - ${window.title}: ${formatPercent(window.remainingPercent)} remaining`,
|
|
58
|
+
` (${formatPercent(window.usedPercent)} used`,
|
|
59
|
+
`${formatReset(window.resetsAt, now)}, checked ${formatAge(window.checkedAt, now)})`,
|
|
60
|
+
].join('');
|
|
61
|
+
}
|
|
55
62
|
function accountName(account) {
|
|
56
63
|
return account.label?.trim() || account.id;
|
|
57
64
|
}
|
|
@@ -95,6 +102,9 @@ export function buildClaudeQuotaSummary(input) {
|
|
|
95
102
|
}
|
|
96
103
|
lines.push(formatWindow('five_hour', account.quota?.five_hour, now));
|
|
97
104
|
lines.push(formatWindow('seven_day', account.quota?.seven_day, now));
|
|
105
|
+
for (const window of account.quota?.scoped ?? []) {
|
|
106
|
+
lines.push(formatScopedWindow(window, now));
|
|
107
|
+
}
|
|
98
108
|
lines.push('');
|
|
99
109
|
}
|
|
100
110
|
return lines.join('\n').trimEnd();
|