@cortexkit/anthropic-auth-core 1.12.2 → 1.13.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 +36 -12
- package/dist/accounts.js +258 -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,17 @@ 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
|
+
};
|
|
63
72
|
export type RoutingMode = 'main-first' | 'fallback-first';
|
|
64
|
-
export type KillswitchThresholds = Partial<Record<QuotaWindowName | '5h' | '1w', number>>;
|
|
73
|
+
export type KillswitchThresholds = Partial<Record<QuotaWindowName | '5h' | '1w' | 'scoped', number>>;
|
|
65
74
|
export type KillswitchConfig = {
|
|
66
75
|
enabled?: boolean;
|
|
67
76
|
/** Thresholds for the main OAuth account (remaining % below which the account is killed). */
|
|
@@ -164,7 +173,6 @@ export type AccountStateSaveScope = {
|
|
|
164
173
|
mainRefresh?: boolean;
|
|
165
174
|
accounts?: true | string[];
|
|
166
175
|
};
|
|
167
|
-
export type OAuthQuotaSnapshot = Partial<Record<QuotaWindowName, AccountQuotaWindow>>;
|
|
168
176
|
export type AccountManagerOptions = {
|
|
169
177
|
now?: () => number;
|
|
170
178
|
fetchImpl?: typeof fetch;
|
|
@@ -253,21 +261,36 @@ export declare function getPersistedMainQuota(storage: AccountStorage | null): {
|
|
|
253
261
|
* Returns 0 when disabled (default).
|
|
254
262
|
*/
|
|
255
263
|
export declare function getQuotaRefreshEveryNRequests(storage: AccountStorage | null): number;
|
|
264
|
+
export declare function getScopedQuotaWindowForModel(quota: OAuthQuotaSnapshot | undefined, model: unknown): AccountScopedQuotaWindow | undefined;
|
|
265
|
+
export declare function quotaSnapshotModelScopeIsExhausted(quota: OAuthQuotaSnapshot | undefined, model: unknown): boolean;
|
|
266
|
+
export declare function quotaSnapshotPassesModelScope(quota: OAuthQuotaSnapshot | undefined, model: unknown): boolean;
|
|
256
267
|
export declare function quotaSnapshotPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null): boolean;
|
|
257
|
-
export declare const DEFAULT_KILLSWITCH_THRESHOLDS: Record<QuotaWindowName, number>;
|
|
268
|
+
export declare const DEFAULT_KILLSWITCH_THRESHOLDS: Record<QuotaWindowName | 'scoped', number>;
|
|
269
|
+
export declare function normalizeKillswitchThresholds(thresholds: KillswitchThresholds | undefined): Record<QuotaWindowName | 'scoped', number>;
|
|
258
270
|
export declare function isKillswitchEnabled(storage: AccountStorage | null): boolean;
|
|
271
|
+
export declare function getKillswitchThresholdsForAccount(storage: AccountStorage | null, accountId?: string): Record<QuotaWindowName | 'scoped', number>;
|
|
259
272
|
/**
|
|
260
273
|
* Returns true if the account's quota is above its killswitch threshold.
|
|
261
274
|
* When killswitch is disabled, always returns true.
|
|
275
|
+
*
|
|
276
|
+
* When `modelId` is provided, the per-account `scoped` threshold is also
|
|
277
|
+
* evaluated against the quota window matching that model — additive to the
|
|
278
|
+
* 5h/7d check. A model with no matching scoped window is unaffected.
|
|
262
279
|
*/
|
|
263
|
-
export declare function killswitchPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, accountId?: string): boolean;
|
|
280
|
+
export declare function killswitchPassesPolicy(quota: OAuthQuotaSnapshot | undefined, storage: AccountStorage | null, accountId?: string, modelId?: string): boolean;
|
|
264
281
|
/**
|
|
265
282
|
* Find the earliest reset time across all accounts' quota windows.
|
|
266
283
|
* Returns seconds from `now` until that reset, or 300 as a fallback.
|
|
284
|
+
*
|
|
285
|
+
* When `scopedModelId` is provided, ONLY the matched scoped window's
|
|
286
|
+
* `resetsAt` is considered — the 5h/7d resets are intentionally ignored
|
|
287
|
+
* so the retry hint reflects the weekly reset, not the sooner 5h reset
|
|
288
|
+
* (which would cause a retry-storm against a block that won't clear for
|
|
289
|
+
* days). With `scopedModelId` undefined, the 5h/7d behavior is unchanged.
|
|
267
290
|
*/
|
|
268
291
|
export declare function killswitchRetryAfterSeconds(mainQuota: OAuthQuotaSnapshot | undefined, fallbackAccounts: Array<{
|
|
269
292
|
quota?: OAuthQuotaSnapshot;
|
|
270
|
-
}>, now: number): number;
|
|
293
|
+
}>, now: number, scopedModelId?: string): number;
|
|
271
294
|
export declare function getKillswitchConfig(storage: AccountStorage | null): KillswitchConfig;
|
|
272
295
|
export declare function setKillswitchPersistent(config: KillswitchConfig, path?: string): Promise<AccountStorage>;
|
|
273
296
|
export declare function removeAccountPersistent(id: string, path?: string): Promise<boolean>;
|
|
@@ -279,10 +302,7 @@ export declare function fetchOAuthQuotaSnapshot(input: {
|
|
|
279
302
|
accessToken: string;
|
|
280
303
|
fetchImpl?: typeof fetch;
|
|
281
304
|
now?: () => number;
|
|
282
|
-
}): Promise<
|
|
283
|
-
five_hour: AccountQuotaWindow | undefined;
|
|
284
|
-
seven_day: AccountQuotaWindow | undefined;
|
|
285
|
-
}>;
|
|
305
|
+
}): Promise<OAuthQuotaSnapshot>;
|
|
286
306
|
export declare function upsertAccount(storage: AccountStorage, account: FallbackAccount): void;
|
|
287
307
|
export declare function removeAccount(storage: AccountStorage, id: string): boolean;
|
|
288
308
|
export declare function reorderAccounts(storage: AccountStorage, orderedIds: string[]): void;
|
|
@@ -306,9 +326,13 @@ export declare class FallbackAccountManager {
|
|
|
306
326
|
save(storage: AccountStorage, accountIds?: string[]): Promise<void>;
|
|
307
327
|
startBackgroundRefresh(): void;
|
|
308
328
|
stopBackgroundRefresh(): void;
|
|
309
|
-
getUsableFallbackAccounts(existingStorage?: AccountStorage | null
|
|
329
|
+
getUsableFallbackAccounts(existingStorage?: AccountStorage | null, options?: {
|
|
330
|
+
modelId?: string;
|
|
331
|
+
}): Promise<OAuthAccount[]>;
|
|
310
332
|
markUsed(account: FallbackAccount): Promise<void>;
|
|
311
|
-
accountPassesQuotaPolicy(account: OAuthAccount, storage: AccountStorage | null
|
|
333
|
+
accountPassesQuotaPolicy(account: OAuthAccount, storage: AccountStorage | null, options?: {
|
|
334
|
+
modelId?: string;
|
|
335
|
+
}): boolean;
|
|
312
336
|
/**
|
|
313
337
|
* Return the account with its quota overlaid from the unified QuotaManager
|
|
314
338
|
* cache (token-bound) when available, so quota-policy decisions use the same
|
package/dist/accounts.js
CHANGED
|
@@ -151,28 +151,63 @@ 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
|
-
const
|
|
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
|
+
if (Array.isArray(value.scoped)) {
|
|
182
|
+
const scoped = value.scoped
|
|
183
|
+
.map((entry) => {
|
|
184
|
+
if (!isRecord(entry))
|
|
185
|
+
return undefined;
|
|
186
|
+
const window = normalizeQuotaWindow(entry);
|
|
187
|
+
if (!window)
|
|
188
|
+
return undefined;
|
|
189
|
+
if (typeof entry.id !== 'string' || !entry.id.trim())
|
|
190
|
+
return undefined;
|
|
191
|
+
if (typeof entry.title !== 'string' || !entry.title.trim()) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
if (typeof entry.modelName !== 'string' || !entry.modelName.trim()) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
const modelId = typeof entry.modelId === 'string' && entry.modelId.trim()
|
|
198
|
+
? entry.modelId.trim()
|
|
199
|
+
: undefined;
|
|
200
|
+
return {
|
|
201
|
+
...window,
|
|
202
|
+
id: entry.id.trim(),
|
|
203
|
+
title: entry.title.trim(),
|
|
204
|
+
...(modelId && { modelId }),
|
|
205
|
+
modelName: entry.modelName.trim(),
|
|
206
|
+
};
|
|
207
|
+
})
|
|
208
|
+
.filter((entry) => entry != null);
|
|
209
|
+
if (scoped.length)
|
|
210
|
+
quota.scoped = scoped;
|
|
176
211
|
}
|
|
177
212
|
return Object.keys(quota).length ? quota : undefined;
|
|
178
213
|
}
|
|
@@ -225,6 +260,43 @@ async function readJsonIfPresent(path) {
|
|
|
225
260
|
function objectWithDefinedEntries(value) {
|
|
226
261
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
227
262
|
}
|
|
263
|
+
function numericField(value) {
|
|
264
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
265
|
+
}
|
|
266
|
+
function accountCredentialTimestamp(value) {
|
|
267
|
+
return Math.max(numericField(value.lastRefreshedAt), numericField(value.lastUsed), numericField(value.addedAt));
|
|
268
|
+
}
|
|
269
|
+
function legacyConfigCredentialsAreNewer(account, stateAccount) {
|
|
270
|
+
if (account.type !== 'oauth')
|
|
271
|
+
return false;
|
|
272
|
+
if (typeof account.refresh !== 'string' || !account.refresh.trim()) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
const tokenChanged = Boolean((typeof account.access === 'string' &&
|
|
276
|
+
typeof stateAccount.access === 'string' &&
|
|
277
|
+
account.access !== stateAccount.access) ||
|
|
278
|
+
(typeof account.refresh === 'string' &&
|
|
279
|
+
typeof stateAccount.refresh === 'string' &&
|
|
280
|
+
account.refresh !== stateAccount.refresh));
|
|
281
|
+
if (!tokenChanged)
|
|
282
|
+
return false;
|
|
283
|
+
return (accountCredentialTimestamp(account) >
|
|
284
|
+
accountCredentialTimestamp(stateAccount));
|
|
285
|
+
}
|
|
286
|
+
function mergeConfigAccountAndState(account, stateAccount) {
|
|
287
|
+
if (legacyConfigCredentialsAreNewer(account, stateAccount)) {
|
|
288
|
+
const merged = { ...stateAccount, ...account };
|
|
289
|
+
const configTimestamp = accountCredentialTimestamp(account);
|
|
290
|
+
if (configTimestamp > numericField(merged.lastRefreshedAt)) {
|
|
291
|
+
merged.lastRefreshedAt = configTimestamp;
|
|
292
|
+
}
|
|
293
|
+
delete merged.quota;
|
|
294
|
+
delete merged.lastRefreshError;
|
|
295
|
+
delete merged.lastQuotaRefreshError;
|
|
296
|
+
return merged;
|
|
297
|
+
}
|
|
298
|
+
return { ...account, ...stateAccount };
|
|
299
|
+
}
|
|
228
300
|
function mergeConfigAndState(configValue, stateValue) {
|
|
229
301
|
if (!isRecord(configValue))
|
|
230
302
|
return configValue;
|
|
@@ -242,7 +314,7 @@ function mergeConfigAndState(configValue, stateValue) {
|
|
|
242
314
|
const stateAccount = typeof account.id === 'string' && isRecord(stateAccounts[account.id])
|
|
243
315
|
? stateAccounts[account.id]
|
|
244
316
|
: {};
|
|
245
|
-
return
|
|
317
|
+
return mergeConfigAccountAndState(account, stateAccount);
|
|
246
318
|
})
|
|
247
319
|
: [];
|
|
248
320
|
return {
|
|
@@ -312,7 +384,7 @@ function accountRuntimeState(account) {
|
|
|
312
384
|
});
|
|
313
385
|
}
|
|
314
386
|
function quotaSnapshotCheckedAt(quota) {
|
|
315
|
-
return Math.max(quota?.five_hour?.checkedAt ?? 0, quota?.seven_day?.checkedAt ?? 0);
|
|
387
|
+
return Math.max(quota?.five_hour?.checkedAt ?? 0, quota?.seven_day?.checkedAt ?? 0, ...(quota?.scoped?.map((window) => window.checkedAt) ?? []));
|
|
316
388
|
}
|
|
317
389
|
function mergeAccountRuntimeState(existing, incoming) {
|
|
318
390
|
if (!isRecord(existing))
|
|
@@ -320,10 +392,13 @@ function mergeAccountRuntimeState(existing, incoming) {
|
|
|
320
392
|
const existingEntry = existing;
|
|
321
393
|
const existingQuotaCheckedAt = quotaSnapshotCheckedAt(existingEntry.quota);
|
|
322
394
|
const incomingQuotaCheckedAt = quotaSnapshotCheckedAt(incoming.quota);
|
|
395
|
+
const tokenChanged = Boolean((existingEntry.access &&
|
|
396
|
+
incoming.access &&
|
|
397
|
+
existingEntry.access !== incoming.access) ||
|
|
398
|
+
(existingEntry.refresh &&
|
|
399
|
+
incoming.refresh &&
|
|
400
|
+
existingEntry.refresh !== incoming.refresh));
|
|
323
401
|
if (existingQuotaCheckedAt > incomingQuotaCheckedAt) {
|
|
324
|
-
const tokenChanged = Boolean(existingEntry.access &&
|
|
325
|
-
incoming.access &&
|
|
326
|
-
existingEntry.access !== incoming.access);
|
|
327
402
|
const existingRefreshAt = existingEntry.lastRefreshedAt ?? 0;
|
|
328
403
|
const incomingRefreshAt = incoming.lastRefreshedAt ?? 0;
|
|
329
404
|
if (tokenChanged && incomingRefreshAt <= existingRefreshAt) {
|
|
@@ -335,9 +410,27 @@ function mergeAccountRuntimeState(existing, incoming) {
|
|
|
335
410
|
}
|
|
336
411
|
return merged;
|
|
337
412
|
}
|
|
413
|
+
const merged = { ...existingEntry, ...incoming };
|
|
414
|
+
if (tokenChanged) {
|
|
415
|
+
if ('quota' in incoming) {
|
|
416
|
+
merged.quota = existingEntry.quota;
|
|
417
|
+
if ('lastQuotaRefreshError' in existingEntry) {
|
|
418
|
+
merged.lastQuotaRefreshError = existingEntry.lastQuotaRefreshError;
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
delete merged.lastQuotaRefreshError;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
delete merged.quota;
|
|
426
|
+
delete merged.lastQuotaRefreshError;
|
|
427
|
+
}
|
|
428
|
+
if (!('lastRefreshError' in incoming))
|
|
429
|
+
delete merged.lastRefreshError;
|
|
430
|
+
return merged;
|
|
431
|
+
}
|
|
338
432
|
return {
|
|
339
|
-
...
|
|
340
|
-
...incoming,
|
|
433
|
+
...merged,
|
|
341
434
|
quota: existingEntry.quota,
|
|
342
435
|
lastQuotaRefreshError: existingEntry.lastQuotaRefreshError,
|
|
343
436
|
};
|
|
@@ -1003,6 +1096,37 @@ function failClosedOnUnknownQuota(storage) {
|
|
|
1003
1096
|
return (storage?.quota?.failClosedOnUnknownQuota ??
|
|
1004
1097
|
DEFAULT_FAIL_CLOSED_ON_UNKNOWN_QUOTA);
|
|
1005
1098
|
}
|
|
1099
|
+
function scopedQuotaModelKey(model) {
|
|
1100
|
+
if (typeof model !== 'string')
|
|
1101
|
+
return null;
|
|
1102
|
+
const normalized = model.toLowerCase();
|
|
1103
|
+
if (normalized.includes('fable'))
|
|
1104
|
+
return 'fable';
|
|
1105
|
+
if (normalized.includes('mythos'))
|
|
1106
|
+
return 'mythos';
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
export function getScopedQuotaWindowForModel(quota, model) {
|
|
1110
|
+
const key = scopedQuotaModelKey(model);
|
|
1111
|
+
if (!key)
|
|
1112
|
+
return undefined;
|
|
1113
|
+
return quota?.scoped?.find((window) => {
|
|
1114
|
+
const haystack = [window.modelId, window.modelName, window.title]
|
|
1115
|
+
.filter((value) => typeof value === 'string')
|
|
1116
|
+
.join(' ')
|
|
1117
|
+
.toLowerCase();
|
|
1118
|
+
return haystack.includes(key);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
export function quotaSnapshotModelScopeIsExhausted(quota, model) {
|
|
1122
|
+
const window = getScopedQuotaWindowForModel(quota, model);
|
|
1123
|
+
return Boolean(window &&
|
|
1124
|
+
Number.isFinite(window.remainingPercent) &&
|
|
1125
|
+
window.remainingPercent <= 0);
|
|
1126
|
+
}
|
|
1127
|
+
export function quotaSnapshotPassesModelScope(quota, model) {
|
|
1128
|
+
return !quotaSnapshotModelScopeIsExhausted(quota, model);
|
|
1129
|
+
}
|
|
1006
1130
|
export function quotaSnapshotPassesPolicy(quota, storage) {
|
|
1007
1131
|
if (!quotaEnabled(storage))
|
|
1008
1132
|
return true;
|
|
@@ -1026,10 +1150,12 @@ export function quotaSnapshotPassesPolicy(quota, storage) {
|
|
|
1026
1150
|
export const DEFAULT_KILLSWITCH_THRESHOLDS = {
|
|
1027
1151
|
five_hour: 5,
|
|
1028
1152
|
seven_day: 10,
|
|
1153
|
+
scoped: 0,
|
|
1029
1154
|
};
|
|
1030
|
-
function normalizeKillswitchThresholds(thresholds) {
|
|
1155
|
+
export function normalizeKillswitchThresholds(thresholds) {
|
|
1031
1156
|
const fiveHour = thresholds?.five_hour ?? thresholds?.['5h'];
|
|
1032
1157
|
const sevenDay = thresholds?.seven_day ?? thresholds?.['1w'];
|
|
1158
|
+
const scoped = thresholds?.scoped;
|
|
1033
1159
|
return {
|
|
1034
1160
|
five_hour: typeof fiveHour === 'number' && Number.isFinite(fiveHour)
|
|
1035
1161
|
? fiveHour
|
|
@@ -1037,12 +1163,15 @@ function normalizeKillswitchThresholds(thresholds) {
|
|
|
1037
1163
|
seven_day: typeof sevenDay === 'number' && Number.isFinite(sevenDay)
|
|
1038
1164
|
? sevenDay
|
|
1039
1165
|
: DEFAULT_KILLSWITCH_THRESHOLDS.seven_day,
|
|
1166
|
+
scoped: typeof scoped === 'number' && Number.isFinite(scoped)
|
|
1167
|
+
? scoped
|
|
1168
|
+
: DEFAULT_KILLSWITCH_THRESHOLDS.scoped,
|
|
1040
1169
|
};
|
|
1041
1170
|
}
|
|
1042
1171
|
export function isKillswitchEnabled(storage) {
|
|
1043
1172
|
return storage?.killswitch?.enabled === true;
|
|
1044
1173
|
}
|
|
1045
|
-
function getKillswitchThresholdsForAccount(storage, accountId) {
|
|
1174
|
+
export function getKillswitchThresholdsForAccount(storage, accountId) {
|
|
1046
1175
|
if (!storage?.killswitch)
|
|
1047
1176
|
return DEFAULT_KILLSWITCH_THRESHOLDS;
|
|
1048
1177
|
if (accountId && storage.killswitch.accounts?.[accountId]) {
|
|
@@ -1053,8 +1182,12 @@ function getKillswitchThresholdsForAccount(storage, accountId) {
|
|
|
1053
1182
|
/**
|
|
1054
1183
|
* Returns true if the account's quota is above its killswitch threshold.
|
|
1055
1184
|
* When killswitch is disabled, always returns true.
|
|
1185
|
+
*
|
|
1186
|
+
* When `modelId` is provided, the per-account `scoped` threshold is also
|
|
1187
|
+
* evaluated against the quota window matching that model — additive to the
|
|
1188
|
+
* 5h/7d check. A model with no matching scoped window is unaffected.
|
|
1056
1189
|
*/
|
|
1057
|
-
export function killswitchPassesPolicy(quota, storage, accountId) {
|
|
1190
|
+
export function killswitchPassesPolicy(quota, storage, accountId, modelId) {
|
|
1058
1191
|
if (!isKillswitchEnabled(storage))
|
|
1059
1192
|
return true;
|
|
1060
1193
|
const thresholds = getKillswitchThresholdsForAccount(storage, accountId);
|
|
@@ -1075,6 +1208,22 @@ export function killswitchPassesPolicy(quota, storage, accountId) {
|
|
|
1075
1208
|
if (window.remainingPercent < thresholds[key])
|
|
1076
1209
|
return false;
|
|
1077
1210
|
}
|
|
1211
|
+
// Scoped check is additive to the 5h/7d evaluation above and is an
|
|
1212
|
+
// INDEPENDENT block reason — it must run before the unknown-window
|
|
1213
|
+
// fail-closed decision, so an exhausted scoped window blocks even when
|
|
1214
|
+
// 5h/7d is missing/non-finite (the latter only changes the fall-through
|
|
1215
|
+
// for accounts that did not already block on scoped). A missing scoped
|
|
1216
|
+
// window (no carve-out for this model) is not "unknown quota" — only a
|
|
1217
|
+
// PRESENT window at/below threshold blocks. The comparison is inclusive
|
|
1218
|
+
// (`<=`) so the default 0 fires at exhaustion.
|
|
1219
|
+
if (modelId) {
|
|
1220
|
+
const scopedWindow = getScopedQuotaWindowForModel(quota, modelId);
|
|
1221
|
+
if (scopedWindow &&
|
|
1222
|
+
Number.isFinite(scopedWindow.remainingPercent) &&
|
|
1223
|
+
scopedWindow.remainingPercent <= thresholds.scoped) {
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1078
1227
|
if (sawUnknownWindow)
|
|
1079
1228
|
return !failClosedOnUnknownQuota(storage);
|
|
1080
1229
|
return true;
|
|
@@ -1082,13 +1231,20 @@ export function killswitchPassesPolicy(quota, storage, accountId) {
|
|
|
1082
1231
|
/**
|
|
1083
1232
|
* Find the earliest reset time across all accounts' quota windows.
|
|
1084
1233
|
* Returns seconds from `now` until that reset, or 300 as a fallback.
|
|
1234
|
+
*
|
|
1235
|
+
* When `scopedModelId` is provided, ONLY the matched scoped window's
|
|
1236
|
+
* `resetsAt` is considered — the 5h/7d resets are intentionally ignored
|
|
1237
|
+
* so the retry hint reflects the weekly reset, not the sooner 5h reset
|
|
1238
|
+
* (which would cause a retry-storm against a block that won't clear for
|
|
1239
|
+
* days). With `scopedModelId` undefined, the 5h/7d behavior is unchanged.
|
|
1085
1240
|
*/
|
|
1086
|
-
export function killswitchRetryAfterSeconds(mainQuota, fallbackAccounts, now) {
|
|
1241
|
+
export function killswitchRetryAfterSeconds(mainQuota, fallbackAccounts, now, scopedModelId) {
|
|
1087
1242
|
const resetTimes = [];
|
|
1088
1243
|
const allQuotas = [mainQuota, ...fallbackAccounts.map((a) => a.quota)];
|
|
1089
1244
|
for (const quota of allQuotas) {
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1245
|
+
if (scopedModelId) {
|
|
1246
|
+
const scopedWindow = getScopedQuotaWindowForModel(quota, scopedModelId);
|
|
1247
|
+
const resetStr = scopedWindow?.resetsAt;
|
|
1092
1248
|
if (!resetStr)
|
|
1093
1249
|
continue;
|
|
1094
1250
|
const resetTime = Date.parse(resetStr);
|
|
@@ -1096,6 +1252,17 @@ export function killswitchRetryAfterSeconds(mainQuota, fallbackAccounts, now) {
|
|
|
1096
1252
|
resetTimes.push(resetTime);
|
|
1097
1253
|
}
|
|
1098
1254
|
}
|
|
1255
|
+
else {
|
|
1256
|
+
for (const key of ['five_hour', 'seven_day']) {
|
|
1257
|
+
const resetStr = quota?.[key]?.resetsAt;
|
|
1258
|
+
if (!resetStr)
|
|
1259
|
+
continue;
|
|
1260
|
+
const resetTime = Date.parse(resetStr);
|
|
1261
|
+
if (Number.isFinite(resetTime) && resetTime > now) {
|
|
1262
|
+
resetTimes.push(resetTime);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1099
1266
|
}
|
|
1100
1267
|
if (!resetTimes.length)
|
|
1101
1268
|
return 300;
|
|
@@ -1226,6 +1393,52 @@ function clampPercent(value) {
|
|
|
1226
1393
|
return 100;
|
|
1227
1394
|
return value;
|
|
1228
1395
|
}
|
|
1396
|
+
function nonEmptyString(value) {
|
|
1397
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
1398
|
+
}
|
|
1399
|
+
function slugForQuotaIdentity(value) {
|
|
1400
|
+
return value
|
|
1401
|
+
.toLowerCase()
|
|
1402
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
1403
|
+
.replace(/^-+|-+$/g, '');
|
|
1404
|
+
}
|
|
1405
|
+
function mapScopedWeeklyLimits(limits, checkedAt) {
|
|
1406
|
+
if (!Array.isArray(limits))
|
|
1407
|
+
return undefined;
|
|
1408
|
+
const seen = new Set();
|
|
1409
|
+
const scoped = [];
|
|
1410
|
+
for (const limit of limits) {
|
|
1411
|
+
if (limit?.kind !== 'weekly_scoped' || limit.group !== 'weekly')
|
|
1412
|
+
continue;
|
|
1413
|
+
if (typeof limit.percent !== 'number' || !Number.isFinite(limit.percent)) {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const modelName = nonEmptyString(limit.scope?.model?.display_name);
|
|
1417
|
+
if (!modelName)
|
|
1418
|
+
continue;
|
|
1419
|
+
const identity = nonEmptyString(limit.scope?.model?.id) ?? modelName;
|
|
1420
|
+
const slug = slugForQuotaIdentity(identity);
|
|
1421
|
+
if (!slug)
|
|
1422
|
+
continue;
|
|
1423
|
+
const id = `claude-weekly-scoped-${slug}`;
|
|
1424
|
+
if (seen.has(id))
|
|
1425
|
+
continue;
|
|
1426
|
+
seen.add(id);
|
|
1427
|
+
const usedPercent = clampPercent(limit.percent);
|
|
1428
|
+
const modelId = nonEmptyString(limit.scope?.model?.id);
|
|
1429
|
+
scoped.push({
|
|
1430
|
+
id,
|
|
1431
|
+
title: `${modelName} only`,
|
|
1432
|
+
...(modelId && { modelId }),
|
|
1433
|
+
modelName,
|
|
1434
|
+
usedPercent,
|
|
1435
|
+
remainingPercent: clampPercent(100 - usedPercent),
|
|
1436
|
+
resetsAt: limit.resets_at,
|
|
1437
|
+
checkedAt,
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
return scoped.length ? scoped : undefined;
|
|
1441
|
+
}
|
|
1229
1442
|
function mapUsageWindow(window, checkedAt) {
|
|
1230
1443
|
if (typeof window?.utilization !== 'number')
|
|
1231
1444
|
return undefined;
|
|
@@ -1264,6 +1477,7 @@ export async function fetchOAuthQuotaSnapshot(input) {
|
|
|
1264
1477
|
return {
|
|
1265
1478
|
five_hour: mapUsageWindow(usage.five_hour, checkedAt),
|
|
1266
1479
|
seven_day: mapUsageWindow(usage.seven_day, checkedAt),
|
|
1480
|
+
scoped: mapScopedWeeklyLimits(usage.limits, checkedAt),
|
|
1267
1481
|
};
|
|
1268
1482
|
}
|
|
1269
1483
|
function updateStoredAccount(storage, account) {
|
|
@@ -1408,7 +1622,7 @@ export class FallbackAccountManager {
|
|
|
1408
1622
|
this.refreshTimer = null;
|
|
1409
1623
|
this.quotaTimer = null;
|
|
1410
1624
|
}
|
|
1411
|
-
async getUsableFallbackAccounts(existingStorage) {
|
|
1625
|
+
async getUsableFallbackAccounts(existingStorage, options = {}) {
|
|
1412
1626
|
const storage = existingStorage !== undefined ? existingStorage : await this.load();
|
|
1413
1627
|
if (!storage)
|
|
1414
1628
|
return [];
|
|
@@ -1444,7 +1658,9 @@ export class FallbackAccountManager {
|
|
|
1444
1658
|
// Single source of truth: evaluate quota policy from the unified
|
|
1445
1659
|
// QuotaManager cache (the same source as the staleness check above) so
|
|
1446
1660
|
// an active-route refresh that updated only the cache is not ignored.
|
|
1447
|
-
if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(next), storage
|
|
1661
|
+
if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(next), storage, {
|
|
1662
|
+
modelId: options.modelId,
|
|
1663
|
+
}))
|
|
1448
1664
|
usable.push(next);
|
|
1449
1665
|
}
|
|
1450
1666
|
catch (error) {
|
|
@@ -1453,9 +1669,14 @@ export class FallbackAccountManager {
|
|
|
1453
1669
|
accountId: account.id,
|
|
1454
1670
|
error: formatErrorMessage(error),
|
|
1455
1671
|
});
|
|
1456
|
-
|
|
1672
|
+
if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(account), storage, {
|
|
1673
|
+
modelId: options.modelId,
|
|
1674
|
+
})) {
|
|
1675
|
+
usable.push(account);
|
|
1676
|
+
}
|
|
1457
1677
|
}
|
|
1458
|
-
else if (!failClosedOnUnknownQuota(storage)
|
|
1678
|
+
else if (!failClosedOnUnknownQuota(storage) &&
|
|
1679
|
+
quotaSnapshotPassesModelScope(account.quota, options.modelId)) {
|
|
1459
1680
|
usable.push(account);
|
|
1460
1681
|
}
|
|
1461
1682
|
}
|
|
@@ -1474,8 +1695,9 @@ export class FallbackAccountManager {
|
|
|
1474
1695
|
stored.lastUsed = this.now();
|
|
1475
1696
|
await this.save(storage);
|
|
1476
1697
|
}
|
|
1477
|
-
accountPassesQuotaPolicy(account, storage) {
|
|
1478
|
-
return quotaSnapshotPassesPolicy(account.quota, storage)
|
|
1698
|
+
accountPassesQuotaPolicy(account, storage, options = {}) {
|
|
1699
|
+
return (quotaSnapshotPassesPolicy(account.quota, storage) &&
|
|
1700
|
+
quotaSnapshotPassesModelScope(account.quota, options.modelId));
|
|
1479
1701
|
}
|
|
1480
1702
|
/**
|
|
1481
1703
|
* 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();
|