sidekick-shared 0.28.1 → 0.28.3
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/CHANGELOG.md +25 -0
- package/README.md +29 -13
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +10 -1
- package/dist/codexQuota.js +17 -4
- package/dist/esm/browser.js +2 -0
- package/dist/esm/codexQuota.js +18 -5
- package/dist/esm/index.js +3 -3
- package/dist/esm/modelContext.js +32 -0
- package/dist/esm/modelInfo.js +186 -0
- package/dist/esm/multiProviderQuotaService.js +1 -1
- package/dist/esm/quota.js +356 -24
- package/dist/esm/quotaHistory.js +33 -8
- package/dist/esm/quotaPoller.js +5 -2
- package/dist/esm/quotaResolver.js +31 -6
- package/dist/esm/quotaSnapshots.js +65 -10
- package/dist/esm/quotaThresholds.js +7 -2
- package/dist/esm/schemas/index.js +2 -2
- package/dist/esm/schemas/quota.js +30 -0
- package/dist/esm/schemas/quotaHistory.js +10 -2
- package/dist/esm/schemas/stateFile.js +9 -0
- package/dist/esm/stateFile.js +18 -3
- package/dist/esm/statusline/claudeStatuslinePayload.js +16 -4
- package/dist/esm/statusline/formatter.js +57 -18
- package/dist/esm/statusline/index.js +2 -2
- package/dist/esm/zaiQuotaApi.js +4 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.js +18 -5
- package/dist/modelContext.js +32 -0
- package/dist/modelInfo.js +186 -0
- package/dist/multiProviderQuotaService.js +1 -1
- package/dist/quota.d.ts +131 -1
- package/dist/quota.js +366 -24
- package/dist/quotaHistory.d.ts +10 -0
- package/dist/quotaHistory.js +34 -8
- package/dist/quotaPoller.d.ts +11 -3
- package/dist/quotaPoller.js +5 -2
- package/dist/quotaResolver.js +30 -5
- package/dist/quotaSnapshots.js +64 -9
- package/dist/quotaThresholds.js +7 -2
- package/dist/schemas/index.d.ts +3 -3
- package/dist/schemas/index.js +5 -2
- package/dist/schemas/quota.d.ts +258 -0
- package/dist/schemas/quota.js +31 -1
- package/dist/schemas/quotaHistory.d.ts +2 -0
- package/dist/schemas/quotaHistory.js +10 -2
- package/dist/schemas/stateFile.d.ts +34 -0
- package/dist/schemas/stateFile.js +10 -1
- package/dist/stateFile.d.ts +18 -0
- package/dist/stateFile.js +17 -2
- package/dist/statusline/claudeStatuslinePayload.d.ts +4 -3
- package/dist/statusline/claudeStatuslinePayload.js +16 -4
- package/dist/statusline/formatter.d.ts +5 -0
- package/dist/statusline/formatter.js +56 -17
- package/dist/statusline/index.d.ts +4 -4
- package/dist/statusline/index.js +5 -1
- package/dist/zaiQuotaApi.js +3 -1
- package/package.json +1 -1
package/dist/quota.js
CHANGED
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QUOTA_AGING_MAX_AGE_MS = exports.QUOTA_FRESH_MAX_AGE_MS = exports.SEVEN_DAY_WINDOW_MS = exports.FIVE_HOUR_WINDOW_MS = exports.QUOTA_PROVIDER_IDS = void 0;
|
|
4
4
|
exports.classifyQuotaFreshness = classifyQuotaFreshness;
|
|
5
|
+
exports.quotaWindowApplies = quotaWindowApplies;
|
|
6
|
+
exports.quotaWindowHasData = quotaWindowHasData;
|
|
7
|
+
exports.quotaSampleExpired = quotaSampleExpired;
|
|
8
|
+
exports.quotaWindowLabel = quotaWindowLabel;
|
|
9
|
+
exports.quotaWindowShortLabel = quotaWindowShortLabel;
|
|
10
|
+
exports.extraQuotaRows = extraQuotaRows;
|
|
11
|
+
exports.claudePlanLabel = claudePlanLabel;
|
|
12
|
+
exports.formatExtraUsage = formatExtraUsage;
|
|
5
13
|
exports.formatQuotaAge = formatQuotaAge;
|
|
6
14
|
exports.projectQuotaWindow = projectQuotaWindow;
|
|
7
15
|
exports.withQuotaProjections = withQuotaProjections;
|
|
16
|
+
exports.quotaStateFromClaudeUsagePayload = quotaStateFromClaudeUsagePayload;
|
|
17
|
+
exports.withClaudePlan = withClaudePlan;
|
|
8
18
|
exports.fetchQuota = fetchQuota;
|
|
9
19
|
var providerIds_1 = require("./providerIds");
|
|
10
20
|
Object.defineProperty(exports, "QUOTA_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.QUOTA_PROVIDER_IDS; } });
|
|
@@ -27,6 +37,126 @@ function classifyQuotaFreshness(ageMs) {
|
|
|
27
37
|
return 'aging';
|
|
28
38
|
return 'stale';
|
|
29
39
|
}
|
|
40
|
+
const HOUR_MS = 3_600_000;
|
|
41
|
+
const DAY_MS = 86_400_000;
|
|
42
|
+
/** Whether the provider reports this window for the account at all. */
|
|
43
|
+
function quotaWindowApplies(window) {
|
|
44
|
+
return Boolean(window) && window?.absent !== true;
|
|
45
|
+
}
|
|
46
|
+
/** Whether the window applies and carries a reading worth showing (a reset time or any usage). */
|
|
47
|
+
function quotaWindowHasData(window) {
|
|
48
|
+
if (!window || window.absent === true)
|
|
49
|
+
return false;
|
|
50
|
+
return Boolean(window.resetsAt) || window.utilization > 0;
|
|
51
|
+
}
|
|
52
|
+
function usableWindowMs(windowMs) {
|
|
53
|
+
return typeof windowMs === 'number' && Number.isFinite(windowMs) && windowMs > 0
|
|
54
|
+
? windowMs
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* True when every window of a sample that has a known reset time has already
|
|
59
|
+
* reset by `nowMs`. Such a sample says nothing about current usage — showing
|
|
60
|
+
* it as live would print old percentages with "resets now". Samples without
|
|
61
|
+
* any reset time are never considered expired.
|
|
62
|
+
*/
|
|
63
|
+
function quotaSampleExpired(quota, nowMs = Date.now()) {
|
|
64
|
+
const resets = [quota.fiveHour, quota.sevenDay]
|
|
65
|
+
.filter((window) => !window.absent && window.resetsAt)
|
|
66
|
+
.map((window) => Date.parse(window.resetsAt))
|
|
67
|
+
.filter((ms) => Number.isFinite(ms));
|
|
68
|
+
return resets.length > 0 && resets.every((ms) => ms <= nowMs);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Human label for a window length: `5-Hour`, `Daily`, `Weekly`, `30-Day`, …
|
|
72
|
+
* Returns `fallback` when the length is unknown or not a whole number of hours.
|
|
73
|
+
*/
|
|
74
|
+
function quotaWindowLabel(windowMs, fallback) {
|
|
75
|
+
const ms = usableWindowMs(windowMs);
|
|
76
|
+
if (ms === undefined)
|
|
77
|
+
return fallback;
|
|
78
|
+
if (ms === DAY_MS)
|
|
79
|
+
return 'Daily';
|
|
80
|
+
if (ms === 7 * DAY_MS)
|
|
81
|
+
return 'Weekly';
|
|
82
|
+
if (ms % DAY_MS === 0)
|
|
83
|
+
return `${ms / DAY_MS}-Day`;
|
|
84
|
+
if (ms % HOUR_MS === 0)
|
|
85
|
+
return `${ms / HOUR_MS}-Hour`;
|
|
86
|
+
return fallback;
|
|
87
|
+
}
|
|
88
|
+
/** Compact label for a window length: `5h`, `1d`, `7d`. Returns `fallback` when unknown. */
|
|
89
|
+
function quotaWindowShortLabel(windowMs, fallback) {
|
|
90
|
+
const ms = usableWindowMs(windowMs);
|
|
91
|
+
if (ms === undefined)
|
|
92
|
+
return fallback;
|
|
93
|
+
if (ms % DAY_MS === 0)
|
|
94
|
+
return `${ms / DAY_MS}d`;
|
|
95
|
+
if (ms % HOUR_MS === 0)
|
|
96
|
+
return `${ms / HOUR_MS}h`;
|
|
97
|
+
if (ms % 60_000 === 0)
|
|
98
|
+
return `${ms / 60_000}m`;
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The reported meters that `fiveHour` and `sevenDay` do not already show —
|
|
103
|
+
* per-model or per-surface weekly caps and any future meter kind.
|
|
104
|
+
*/
|
|
105
|
+
function extraQuotaRows(quota) {
|
|
106
|
+
return (quota.limits ?? []).filter((row) => row.kind !== 'session' && row.kind !== 'weekly_all');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Display name for a Claude subscription. The 20× tier was renamed Ultra and
|
|
110
|
+
* the 5× tier Max on 2026-10-30; credentials keep the old tier ids.
|
|
111
|
+
*/
|
|
112
|
+
function claudePlanLabel(plan) {
|
|
113
|
+
const tier = plan?.rateLimitTier?.trim().toLowerCase() ?? '';
|
|
114
|
+
if (tier.includes('ultra') || tier.includes('max_20x'))
|
|
115
|
+
return 'Ultra';
|
|
116
|
+
if (tier.includes('max_5x'))
|
|
117
|
+
return 'Max';
|
|
118
|
+
const subscription = plan?.subscriptionType?.trim().toLowerCase();
|
|
119
|
+
if (!subscription)
|
|
120
|
+
return undefined;
|
|
121
|
+
return subscription.charAt(0).toUpperCase() + subscription.slice(1);
|
|
122
|
+
}
|
|
123
|
+
function formatMinorUnits(amount, currency, decimals) {
|
|
124
|
+
const value = amount / 10 ** decimals;
|
|
125
|
+
if (currency) {
|
|
126
|
+
try {
|
|
127
|
+
return new Intl.NumberFormat('en-US', {
|
|
128
|
+
style: 'currency',
|
|
129
|
+
currency,
|
|
130
|
+
minimumFractionDigits: decimals,
|
|
131
|
+
maximumFractionDigits: decimals,
|
|
132
|
+
}).format(value);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return `${value.toFixed(decimals)} ${currency}`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return value.toFixed(decimals);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* One-line extra-usage summary, e.g. `$12.40 of $200.00 used` or
|
|
142
|
+
* `$0.00 of $200.00 used · off (out of credits)`.
|
|
143
|
+
*/
|
|
144
|
+
function formatExtraUsage(extra) {
|
|
145
|
+
const decimals = typeof extra.decimalPlaces === 'number' &&
|
|
146
|
+
Number.isInteger(extra.decimalPlaces) &&
|
|
147
|
+
extra.decimalPlaces >= 0 &&
|
|
148
|
+
extra.decimalPlaces <= 6
|
|
149
|
+
? extra.decimalPlaces
|
|
150
|
+
: 2;
|
|
151
|
+
const used = formatMinorUnits(extra.usedCredits ?? 0, extra.currency, decimals);
|
|
152
|
+
const amount = extra.monthlyLimit !== null
|
|
153
|
+
? `${used} of ${formatMinorUnits(extra.monthlyLimit, extra.currency, decimals)} used`
|
|
154
|
+
: `${used} used`;
|
|
155
|
+
if (extra.enabled)
|
|
156
|
+
return amount;
|
|
157
|
+
const reason = extra.disabledReason ? ` (${extra.disabledReason.replace(/_/g, ' ')})` : '';
|
|
158
|
+
return `${amount} · off${reason}`;
|
|
159
|
+
}
|
|
30
160
|
/** Compact age label for status lines and table footers: `just now`, `3m ago`, `2h ago`, `3d ago`. */
|
|
31
161
|
function formatQuotaAge(ageMs) {
|
|
32
162
|
if (typeof ageMs !== 'number' || !Number.isFinite(ageMs) || ageMs < 0)
|
|
@@ -65,21 +195,21 @@ function withQuotaProjections(state, options = {}) {
|
|
|
65
195
|
return state;
|
|
66
196
|
const next = { ...state };
|
|
67
197
|
const capturedAt = options.capturedAt ?? state.capturedAt;
|
|
68
|
-
if (next.projectedFiveHour == null) {
|
|
198
|
+
if (next.projectedFiveHour == null && !next.fiveHour.absent) {
|
|
69
199
|
const projected = projectQuotaWindow({
|
|
70
200
|
utilization: next.fiveHour.utilization,
|
|
71
201
|
resetsAt: next.fiveHour.resetsAt,
|
|
72
|
-
windowMs: options.fiveHourWindowMs ?? exports.FIVE_HOUR_WINDOW_MS,
|
|
202
|
+
windowMs: options.fiveHourWindowMs ?? usableWindowMs(next.fiveHour.windowMs) ?? exports.FIVE_HOUR_WINDOW_MS,
|
|
73
203
|
capturedAt,
|
|
74
204
|
});
|
|
75
205
|
if (projected !== undefined)
|
|
76
206
|
next.projectedFiveHour = projected;
|
|
77
207
|
}
|
|
78
|
-
if (next.projectedSevenDay == null) {
|
|
208
|
+
if (next.projectedSevenDay == null && !next.sevenDay.absent) {
|
|
79
209
|
const projected = projectQuotaWindow({
|
|
80
210
|
utilization: next.sevenDay.utilization,
|
|
81
211
|
resetsAt: next.sevenDay.resetsAt,
|
|
82
|
-
windowMs: options.sevenDayWindowMs ?? exports.SEVEN_DAY_WINDOW_MS,
|
|
212
|
+
windowMs: options.sevenDayWindowMs ?? usableWindowMs(next.sevenDay.windowMs) ?? exports.SEVEN_DAY_WINDOW_MS,
|
|
83
213
|
capturedAt,
|
|
84
214
|
});
|
|
85
215
|
if (projected !== undefined)
|
|
@@ -87,6 +217,235 @@ function withQuotaProjections(state, options = {}) {
|
|
|
87
217
|
}
|
|
88
218
|
return next;
|
|
89
219
|
}
|
|
220
|
+
// ── Claude usage endpoint payload ──
|
|
221
|
+
/** Hard cap on parsed meter rows, matching Claude Code's own bound. */
|
|
222
|
+
const MAX_LIMIT_ROWS = 64;
|
|
223
|
+
const MAX_LABEL_LENGTH = 128;
|
|
224
|
+
function isRecord(value) {
|
|
225
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
226
|
+
}
|
|
227
|
+
function finiteNumber(value) {
|
|
228
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
229
|
+
}
|
|
230
|
+
function nullableFiniteNumber(value) {
|
|
231
|
+
return finiteNumber(value) ?? null;
|
|
232
|
+
}
|
|
233
|
+
function shortString(value) {
|
|
234
|
+
if (typeof value !== 'string')
|
|
235
|
+
return undefined;
|
|
236
|
+
const trimmed = value.trim();
|
|
237
|
+
return trimmed && trimmed.length <= MAX_LABEL_LENGTH ? trimmed : undefined;
|
|
238
|
+
}
|
|
239
|
+
function displayName(value) {
|
|
240
|
+
return isRecord(value) ? shortString(value.display_name) : undefined;
|
|
241
|
+
}
|
|
242
|
+
function usageWindow(value) {
|
|
243
|
+
if (!isRecord(value))
|
|
244
|
+
return null;
|
|
245
|
+
return {
|
|
246
|
+
utilization: finiteNumber(value.utilization) ?? 0,
|
|
247
|
+
resetsAt: typeof value.resets_at === 'string' ? value.resets_at : '',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function absentWindow() {
|
|
251
|
+
return { utilization: 0, resetsAt: '', absent: true };
|
|
252
|
+
}
|
|
253
|
+
function windowMsForMeter(kind, group) {
|
|
254
|
+
if (kind === 'session' || group === 'session')
|
|
255
|
+
return exports.FIVE_HOUR_WINDOW_MS;
|
|
256
|
+
if (group === 'weekly' || kind.startsWith('weekly'))
|
|
257
|
+
return exports.SEVEN_DAY_WINDOW_MS;
|
|
258
|
+
if (group === 'daily')
|
|
259
|
+
return DAY_MS;
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
function titleCase(value) {
|
|
263
|
+
return value
|
|
264
|
+
.split(/[_\s-]+/)
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
267
|
+
.join(' ');
|
|
268
|
+
}
|
|
269
|
+
function meterLabel(kind, group, scope) {
|
|
270
|
+
let base;
|
|
271
|
+
if (kind === 'session' || group === 'session')
|
|
272
|
+
base = '5-Hour';
|
|
273
|
+
else if (group === 'weekly' || kind.startsWith('weekly'))
|
|
274
|
+
base = '7-Day';
|
|
275
|
+
else if (group === 'daily')
|
|
276
|
+
base = 'Daily';
|
|
277
|
+
else
|
|
278
|
+
base = titleCase(group || kind) || kind;
|
|
279
|
+
return scope ? `${base} (${scope})` : base;
|
|
280
|
+
}
|
|
281
|
+
function weeklySharesByIndex(value) {
|
|
282
|
+
const shares = new Map();
|
|
283
|
+
if (!Array.isArray(value))
|
|
284
|
+
return shares;
|
|
285
|
+
for (const entry of value.slice(0, MAX_LIMIT_ROWS)) {
|
|
286
|
+
if (!isRecord(entry))
|
|
287
|
+
continue;
|
|
288
|
+
const index = finiteNumber(entry.limit_index);
|
|
289
|
+
if (index === undefined || !Number.isInteger(index) || index < 0)
|
|
290
|
+
continue;
|
|
291
|
+
shares.set(index, {
|
|
292
|
+
allowance: finiteNumber(entry.allowance_percent_of_weekly),
|
|
293
|
+
used: finiteNumber(entry.used_percent_of_weekly),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return shares;
|
|
297
|
+
}
|
|
298
|
+
function buildMeterRow(input, capturedAt) {
|
|
299
|
+
const windowMs = windowMsForMeter(input.kind, input.group);
|
|
300
|
+
const projected = windowMs !== undefined
|
|
301
|
+
? projectQuotaWindow({
|
|
302
|
+
utilization: input.utilization,
|
|
303
|
+
resetsAt: input.resetsAt,
|
|
304
|
+
windowMs,
|
|
305
|
+
capturedAt,
|
|
306
|
+
})
|
|
307
|
+
: undefined;
|
|
308
|
+
return {
|
|
309
|
+
kind: input.kind,
|
|
310
|
+
group: input.group,
|
|
311
|
+
label: meterLabel(input.kind, input.group, input.scopeModel ?? input.scopeSurface),
|
|
312
|
+
utilization: input.utilization,
|
|
313
|
+
resetsAt: input.resetsAt,
|
|
314
|
+
...(windowMs !== undefined ? { windowMs } : {}),
|
|
315
|
+
...(projected !== undefined ? { projected } : {}),
|
|
316
|
+
...(input.scopeModel ? { scopeModel: input.scopeModel } : {}),
|
|
317
|
+
...(input.scopeSurface ? { scopeSurface: input.scopeSurface } : {}),
|
|
318
|
+
...(input.severity ? { severity: input.severity } : {}),
|
|
319
|
+
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
|
320
|
+
...(input.share?.allowance !== undefined
|
|
321
|
+
? { allowancePercentOfWeekly: input.share.allowance }
|
|
322
|
+
: {}),
|
|
323
|
+
...(input.share?.used !== undefined ? { usedPercentOfWeekly: input.share.used } : {}),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function parseLimitRows(value, sharesValue, capturedAt) {
|
|
327
|
+
if (!Array.isArray(value))
|
|
328
|
+
return undefined;
|
|
329
|
+
const shares = weeklySharesByIndex(sharesValue);
|
|
330
|
+
const rows = [];
|
|
331
|
+
value.slice(0, MAX_LIMIT_ROWS).forEach((raw, index) => {
|
|
332
|
+
if (!isRecord(raw))
|
|
333
|
+
return;
|
|
334
|
+
const kind = shortString(raw.kind);
|
|
335
|
+
const percent = finiteNumber(raw.percent);
|
|
336
|
+
if (!kind || percent === undefined)
|
|
337
|
+
return;
|
|
338
|
+
const scope = isRecord(raw.scope) ? raw.scope : undefined;
|
|
339
|
+
rows.push(buildMeterRow({
|
|
340
|
+
kind,
|
|
341
|
+
group: shortString(raw.group) ?? '',
|
|
342
|
+
utilization: Math.max(0, percent),
|
|
343
|
+
resetsAt: typeof raw.resets_at === 'string' ? raw.resets_at : '',
|
|
344
|
+
scopeModel: displayName(scope?.model),
|
|
345
|
+
scopeSurface: displayName(scope?.surface),
|
|
346
|
+
severity: shortString(raw.severity),
|
|
347
|
+
isActive: typeof raw.is_active === 'boolean' ? raw.is_active : undefined,
|
|
348
|
+
share: shares.get(index),
|
|
349
|
+
}, capturedAt));
|
|
350
|
+
});
|
|
351
|
+
return rows;
|
|
352
|
+
}
|
|
353
|
+
/** Pre-`limits[]` per-model weekly fields, read only when the server sends no `limits[]`. */
|
|
354
|
+
const LEGACY_SCOPED_WEEKLY_FIELDS = [
|
|
355
|
+
['seven_day_opus', 'Opus'],
|
|
356
|
+
['seven_day_sonnet', 'Sonnet'],
|
|
357
|
+
['seven_day_overage_included', 'Fable'],
|
|
358
|
+
];
|
|
359
|
+
function legacyScopedRows(data, capturedAt) {
|
|
360
|
+
const rows = [];
|
|
361
|
+
for (const [field, model] of LEGACY_SCOPED_WEEKLY_FIELDS) {
|
|
362
|
+
const window = usageWindow(data[field]);
|
|
363
|
+
if (!window)
|
|
364
|
+
continue;
|
|
365
|
+
rows.push(buildMeterRow({
|
|
366
|
+
kind: 'weekly_scoped',
|
|
367
|
+
group: 'weekly',
|
|
368
|
+
utilization: window.utilization,
|
|
369
|
+
resetsAt: window.resetsAt,
|
|
370
|
+
scopeModel: model,
|
|
371
|
+
}, capturedAt));
|
|
372
|
+
}
|
|
373
|
+
return rows;
|
|
374
|
+
}
|
|
375
|
+
function parseExtraUsage(value) {
|
|
376
|
+
if (!isRecord(value) || typeof value.is_enabled !== 'boolean')
|
|
377
|
+
return undefined;
|
|
378
|
+
const decimalPlaces = finiteNumber(value.decimal_places);
|
|
379
|
+
return {
|
|
380
|
+
enabled: value.is_enabled,
|
|
381
|
+
monthlyLimit: nullableFiniteNumber(value.monthly_limit),
|
|
382
|
+
usedCredits: nullableFiniteNumber(value.used_credits),
|
|
383
|
+
utilization: nullableFiniteNumber(value.utilization),
|
|
384
|
+
...(shortString(value.currency) ? { currency: shortString(value.currency) } : {}),
|
|
385
|
+
...(decimalPlaces !== undefined ? { decimalPlaces } : {}),
|
|
386
|
+
...(shortString(value.disabled_reason)
|
|
387
|
+
? { disabledReason: shortString(value.disabled_reason) }
|
|
388
|
+
: {}),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function windowFromMeter(row) {
|
|
392
|
+
return row ? { utilization: row.utilization, resetsAt: row.resetsAt } : null;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Normalize a Claude OAuth usage-endpoint body into a `QuotaState`.
|
|
396
|
+
*
|
|
397
|
+
* - `five_hour`/`seven_day` that are null or missing become `absent` windows
|
|
398
|
+
* (Ultra plans have no five-hour session limit), unless `limits[]` carries
|
|
399
|
+
* the matching `session`/`weekly_all` meter.
|
|
400
|
+
* - `limits[]` is kept verbatim as `limits` (with derived labels, window
|
|
401
|
+
* lengths, and projections); older servers' `seven_day_opus` /
|
|
402
|
+
* `seven_day_sonnet` / `seven_day_overage_included` fill it instead.
|
|
403
|
+
* - `extra_usage` becomes `extraUsage`.
|
|
404
|
+
*
|
|
405
|
+
* Bodies in the original two-window shape produce exactly the state they did
|
|
406
|
+
* before these fields existed.
|
|
407
|
+
*/
|
|
408
|
+
function quotaStateFromClaudeUsagePayload(payload, options = {}) {
|
|
409
|
+
const data = isRecord(payload) ? payload : {};
|
|
410
|
+
const capturedAt = options.capturedAt;
|
|
411
|
+
const parsedLimits = parseLimitRows(data.limits, data.weekly_scoped_shares, capturedAt);
|
|
412
|
+
const legacyRows = parsedLimits === undefined ? legacyScopedRows(data, capturedAt) : [];
|
|
413
|
+
const limits = parsedLimits ?? (legacyRows.length > 0 ? legacyRows : undefined);
|
|
414
|
+
const fiveHour = usageWindow(data.five_hour) ??
|
|
415
|
+
windowFromMeter(parsedLimits?.find((row) => row.kind === 'session')) ??
|
|
416
|
+
absentWindow();
|
|
417
|
+
const sevenDay = usageWindow(data.seven_day) ??
|
|
418
|
+
windowFromMeter(parsedLimits?.find((row) => row.kind === 'weekly_all')) ??
|
|
419
|
+
absentWindow();
|
|
420
|
+
const extraUsage = parseExtraUsage(data.extra_usage);
|
|
421
|
+
const projectedFiveHour = fiveHour.absent
|
|
422
|
+
? undefined
|
|
423
|
+
: projectQuotaWindow({ ...fiveHour, windowMs: exports.FIVE_HOUR_WINDOW_MS, capturedAt });
|
|
424
|
+
const projectedSevenDay = sevenDay.absent
|
|
425
|
+
? undefined
|
|
426
|
+
: projectQuotaWindow({ ...sevenDay, windowMs: exports.SEVEN_DAY_WINDOW_MS, capturedAt });
|
|
427
|
+
return {
|
|
428
|
+
fiveHour,
|
|
429
|
+
sevenDay,
|
|
430
|
+
available: true,
|
|
431
|
+
projectedFiveHour,
|
|
432
|
+
projectedSevenDay,
|
|
433
|
+
...(limits ? { limits } : {}),
|
|
434
|
+
...(extraUsage ? { extraUsage } : {}),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/** Stamp `planType` / `planLabel` from Claude credential plan facts (no-op without them). */
|
|
438
|
+
function withClaudePlan(state, plan) {
|
|
439
|
+
const planLabel = claudePlanLabel(plan);
|
|
440
|
+
const planType = plan?.subscriptionType?.trim() || undefined;
|
|
441
|
+
if (!planLabel && !planType)
|
|
442
|
+
return state;
|
|
443
|
+
return {
|
|
444
|
+
...state,
|
|
445
|
+
...(planType ? { planType } : {}),
|
|
446
|
+
...(planLabel ? { planLabel } : {}),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
90
449
|
/**
|
|
91
450
|
* Helper to build an unavailable QuotaState.
|
|
92
451
|
*/
|
|
@@ -119,6 +478,7 @@ function parseRetryAfterMs(retryAfter) {
|
|
|
119
478
|
*
|
|
120
479
|
* @param accessToken - OAuth access token from `readClaudeMaxCredentials()`
|
|
121
480
|
* @param options.fetchImpl - Fetch implementation (defaults to the global `fetch`; tests inject a stub)
|
|
481
|
+
* @param options.plan - Plan facts from the same credentials; stamps `planType` and `planLabel`
|
|
122
482
|
* @returns QuotaState with utilization data and elapsed-time projections
|
|
123
483
|
*/
|
|
124
484
|
async function fetchQuota(accessToken, options = {}) {
|
|
@@ -161,26 +521,8 @@ async function fetchQuota(accessToken, options = {}) {
|
|
|
161
521
|
httpStatus: res.status,
|
|
162
522
|
});
|
|
163
523
|
}
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
const sevenUtil = data.seven_day?.utilization ?? 0;
|
|
167
|
-
const fiveResetsAt = data.five_hour?.resets_at ?? '';
|
|
168
|
-
const sevenResetsAt = data.seven_day?.resets_at ?? '';
|
|
169
|
-
return {
|
|
170
|
-
fiveHour: { utilization: fiveUtil, resetsAt: fiveResetsAt },
|
|
171
|
-
sevenDay: { utilization: sevenUtil, resetsAt: sevenResetsAt },
|
|
172
|
-
available: true,
|
|
173
|
-
projectedFiveHour: projectQuotaWindow({
|
|
174
|
-
utilization: fiveUtil,
|
|
175
|
-
resetsAt: fiveResetsAt,
|
|
176
|
-
windowMs: exports.FIVE_HOUR_WINDOW_MS,
|
|
177
|
-
}),
|
|
178
|
-
projectedSevenDay: projectQuotaWindow({
|
|
179
|
-
utilization: sevenUtil,
|
|
180
|
-
resetsAt: sevenResetsAt,
|
|
181
|
-
windowMs: exports.SEVEN_DAY_WINDOW_MS,
|
|
182
|
-
}),
|
|
183
|
-
};
|
|
524
|
+
const state = quotaStateFromClaudeUsagePayload(await res.json());
|
|
525
|
+
return withClaudePlan(state, options.plan);
|
|
184
526
|
}
|
|
185
527
|
catch {
|
|
186
528
|
return unavailableState('Network error', { failureKind: 'network' });
|
package/dist/quotaHistory.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* dashboard, the `sidekick quota history` CLI, external consumers) can render heatmaps and
|
|
7
7
|
* trend visualisations over a 13-week window.
|
|
8
8
|
*/
|
|
9
|
+
import type { QuotaState } from './quota';
|
|
9
10
|
import type { RuntimeQuotaProviderId } from './providerIds';
|
|
10
11
|
export type QuotaHistoryRuntimeProvider = RuntimeQuotaProviderId;
|
|
11
12
|
export interface QuotaHistorySample {
|
|
@@ -13,13 +14,16 @@ export interface QuotaHistorySample {
|
|
|
13
14
|
runtimeProvider: QuotaHistoryRuntimeProvider;
|
|
14
15
|
providerId: string;
|
|
15
16
|
workspaceId: string;
|
|
17
|
+
/** `absent` marks a window the plan does not have (e.g. Ultra's five-hour limit). */
|
|
16
18
|
fiveHour: {
|
|
17
19
|
utilization: number;
|
|
18
20
|
resetsAt: string;
|
|
21
|
+
absent?: boolean;
|
|
19
22
|
};
|
|
20
23
|
sevenDay: {
|
|
21
24
|
utilization: number;
|
|
22
25
|
resetsAt: string;
|
|
26
|
+
absent?: boolean;
|
|
23
27
|
};
|
|
24
28
|
available: boolean;
|
|
25
29
|
error?: string;
|
|
@@ -54,6 +58,12 @@ export interface QuotaHistoryDailyBucket {
|
|
|
54
58
|
* Shared between the VS Code extension (workspace folder fsPath) and the CLI (process.cwd()).
|
|
55
59
|
*/
|
|
56
60
|
export declare function getWorkspaceIdFromPath(workspacePath: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Project a quota window onto the history-sample shape, keeping the `absent`
|
|
63
|
+
* marker so summaries and the derived snapshot do not mistake "no such
|
|
64
|
+
* window" for "0% used".
|
|
65
|
+
*/
|
|
66
|
+
export declare function quotaHistoryWindow(window: QuotaState['fiveHour']): QuotaHistorySample['fiveHour'];
|
|
57
67
|
export declare function appendQuotaHistorySample(sample: QuotaHistorySample, options?: QuotaHistoryAppendOptions): Promise<void>;
|
|
58
68
|
export declare function readQuotaHistoryRange(options: QuotaHistoryRangeOptions): Promise<QuotaHistorySample[]>;
|
|
59
69
|
export declare function readQuotaHistoryDailyBuckets(options: QuotaHistoryRangeOptions): Promise<QuotaHistoryDailyBucket[]>;
|
package/dist/quotaHistory.js
CHANGED
|
@@ -42,6 +42,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
42
42
|
})();
|
|
43
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
44
|
exports.getWorkspaceIdFromPath = getWorkspaceIdFromPath;
|
|
45
|
+
exports.quotaHistoryWindow = quotaHistoryWindow;
|
|
45
46
|
exports.appendQuotaHistorySample = appendQuotaHistorySample;
|
|
46
47
|
exports.readQuotaHistoryRange = readQuotaHistoryRange;
|
|
47
48
|
exports.readQuotaHistoryDailyBuckets = readQuotaHistoryDailyBuckets;
|
|
@@ -200,11 +201,26 @@ function pruneFileSync(filePath, retentionDays) {
|
|
|
200
201
|
}
|
|
201
202
|
return { kept: keptLines.length, pruned };
|
|
202
203
|
}
|
|
204
|
+
function historyWindowToQuota(window) {
|
|
205
|
+
return {
|
|
206
|
+
utilization: window.utilization,
|
|
207
|
+
resetsAt: window.resetsAt,
|
|
208
|
+
...(window.absent ? { absent: true } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Project a quota window onto the history-sample shape, keeping the `absent`
|
|
213
|
+
* marker so summaries and the derived snapshot do not mistake "no such
|
|
214
|
+
* window" for "0% used".
|
|
215
|
+
*/
|
|
216
|
+
function quotaHistoryWindow(window) {
|
|
217
|
+
return historyWindowToQuota(window);
|
|
218
|
+
}
|
|
203
219
|
function sampleToQuotaState(sample) {
|
|
204
220
|
const providerId = runtimeProviderToSnapshotProvider(sample.runtimeProvider);
|
|
205
221
|
return {
|
|
206
|
-
fiveHour:
|
|
207
|
-
sevenDay:
|
|
222
|
+
fiveHour: historyWindowToQuota(sample.fiveHour),
|
|
223
|
+
sevenDay: historyWindowToQuota(sample.sevenDay),
|
|
208
224
|
available: sample.available,
|
|
209
225
|
error: sample.error,
|
|
210
226
|
providerId,
|
|
@@ -370,12 +386,22 @@ async function readQuotaHistoryDailyBuckets(options) {
|
|
|
370
386
|
let maxSeven = 0;
|
|
371
387
|
let sumFive = 0;
|
|
372
388
|
let sumSeven = 0;
|
|
389
|
+
// Averages count only samples where the window exists, so a plan without
|
|
390
|
+
// a five-hour limit does not drag the five-hour average toward zero.
|
|
391
|
+
let fiveCount = 0;
|
|
392
|
+
let sevenCount = 0;
|
|
373
393
|
let anyUnavailable = false;
|
|
374
394
|
for (const s of daySamples) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
395
|
+
if (!s.fiveHour.absent) {
|
|
396
|
+
maxFive = Math.max(maxFive, s.fiveHour.utilization);
|
|
397
|
+
sumFive += s.fiveHour.utilization;
|
|
398
|
+
fiveCount += 1;
|
|
399
|
+
}
|
|
400
|
+
if (!s.sevenDay.absent) {
|
|
401
|
+
maxSeven = Math.max(maxSeven, s.sevenDay.utilization);
|
|
402
|
+
sumSeven += s.sevenDay.utilization;
|
|
403
|
+
sevenCount += 1;
|
|
404
|
+
}
|
|
379
405
|
if (!s.available)
|
|
380
406
|
anyUnavailable = true;
|
|
381
407
|
}
|
|
@@ -385,8 +411,8 @@ async function readQuotaHistoryDailyBuckets(options) {
|
|
|
385
411
|
samples: n,
|
|
386
412
|
maxUtilizationFiveHour: maxFive,
|
|
387
413
|
maxUtilizationSevenDay: maxSeven,
|
|
388
|
-
avgUtilizationFiveHour: Math.round((sumFive /
|
|
389
|
-
avgUtilizationSevenDay: Math.round((sumSeven /
|
|
414
|
+
avgUtilizationFiveHour: fiveCount ? Math.round((sumFive / fiveCount) * 100) / 100 : 0,
|
|
415
|
+
avgUtilizationSevenDay: sevenCount ? Math.round((sumSeven / sevenCount) * 100) / 100 : 0,
|
|
390
416
|
anyUnavailable,
|
|
391
417
|
});
|
|
392
418
|
}
|
package/dist/quotaPoller.d.ts
CHANGED
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module quotaPoller
|
|
8
8
|
*/
|
|
9
|
-
import type { QuotaState } from './quota';
|
|
9
|
+
import type { ClaudePlanInfo, QuotaState } from './quota';
|
|
10
10
|
import { type AccountsChangedEvent } from './accountChanges';
|
|
11
11
|
/** Disposable subscription handle. */
|
|
12
12
|
export interface Disposable {
|
|
13
13
|
dispose(): void;
|
|
14
14
|
}
|
|
15
15
|
/** Options for QuotaPoller construction. */
|
|
16
|
+
/** Credential fields the poller reads when `getAccessToken` returns a record. */
|
|
17
|
+
export interface QuotaPollerCredentials extends ClaudePlanInfo {
|
|
18
|
+
accessToken: string;
|
|
19
|
+
}
|
|
16
20
|
export interface QuotaPollerOptions {
|
|
17
21
|
/** Polling interval when actively monitored (ms). Default: 60_000 (1 minute). */
|
|
18
22
|
activeIntervalMs?: number;
|
|
@@ -20,8 +24,12 @@ export interface QuotaPollerOptions {
|
|
|
20
24
|
idleIntervalMs?: number;
|
|
21
25
|
/** Maximum backoff delay for retries (ms). Default: 120_000 (2 minutes). */
|
|
22
26
|
maxBackoffMs?: number;
|
|
23
|
-
/**
|
|
24
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Returns the current access token, or the credentials record itself so the
|
|
29
|
+
* poller can also stamp the subscription plan (`planLabel`, e.g. `Ultra`).
|
|
30
|
+
* Called before each fetch.
|
|
31
|
+
*/
|
|
32
|
+
getAccessToken: () => Promise<string | QuotaPollerCredentials | null | undefined>;
|
|
25
33
|
/** Optional cheap account-presence check performed before credential access. */
|
|
26
34
|
hasAccount?: () => boolean | Promise<boolean>;
|
|
27
35
|
/** Override the process-wide login/logout signal (primarily for embedded hosts/tests). */
|
package/dist/quotaPoller.js
CHANGED
|
@@ -128,7 +128,8 @@ class QuotaPoller {
|
|
|
128
128
|
this.dormant = true;
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
|
-
const
|
|
131
|
+
const credentials = await this.getAccessToken();
|
|
132
|
+
const token = typeof credentials === 'string' ? credentials : credentials?.accessToken;
|
|
132
133
|
if (!token) {
|
|
133
134
|
if (this.hasAccount) {
|
|
134
135
|
// The account exists but its token was unreadable (locked keychain,
|
|
@@ -144,7 +145,9 @@ class QuotaPoller {
|
|
|
144
145
|
}
|
|
145
146
|
return;
|
|
146
147
|
}
|
|
147
|
-
const state = await (0, quota_1.fetchQuota)(token
|
|
148
|
+
const state = await (0, quota_1.fetchQuota)(token, {
|
|
149
|
+
plan: typeof credentials === 'object' && credentials !== null ? credentials : null,
|
|
150
|
+
});
|
|
148
151
|
if (state.available) {
|
|
149
152
|
this.latest = state;
|
|
150
153
|
this.consecutiveFailures = 0;
|
package/dist/quotaResolver.js
CHANGED
|
@@ -66,9 +66,18 @@ function resolveIdentity(options) {
|
|
|
66
66
|
return { accountId: options.accountId ?? ZAI_DEFAULT_ACCOUNT_ID, codexAccount: null };
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
/** Token plus the plan facts stored beside it (the plan label needs no extra read). */
|
|
70
|
+
async function claudeTokenAndPlan(options) {
|
|
71
|
+
if (options.getClaudeAccessToken) {
|
|
72
|
+
return { token: await options.getClaudeAccessToken(), plan: null };
|
|
73
|
+
}
|
|
70
74
|
const credentials = await (0, credentials_1.readClaudeMaxCredentials)();
|
|
71
|
-
return
|
|
75
|
+
return {
|
|
76
|
+
token: credentials?.accessToken ?? null,
|
|
77
|
+
plan: credentials
|
|
78
|
+
? { subscriptionType: credentials.subscriptionType, rateLimitTier: credentials.rateLimitTier }
|
|
79
|
+
: null,
|
|
80
|
+
};
|
|
72
81
|
}
|
|
73
82
|
function zeroWindows() {
|
|
74
83
|
return { fiveHour: { utilization: 0, resetsAt: '' }, sevenDay: { utilization: 0, resetsAt: '' } };
|
|
@@ -87,7 +96,7 @@ function unavailableState(providerId, error, capturedAt) {
|
|
|
87
96
|
async function fetchFromApi(options, identity, capturedAt) {
|
|
88
97
|
switch (options.providerId) {
|
|
89
98
|
case 'claude-code': {
|
|
90
|
-
const token = await (options
|
|
99
|
+
const { token, plan } = await claudeTokenAndPlan(options);
|
|
91
100
|
if (!token) {
|
|
92
101
|
return {
|
|
93
102
|
...zeroWindows(),
|
|
@@ -99,7 +108,7 @@ async function fetchFromApi(options, identity, capturedAt) {
|
|
|
99
108
|
capturedAt,
|
|
100
109
|
};
|
|
101
110
|
}
|
|
102
|
-
const state = await (0, quota_1.fetchQuota)(token, { fetchImpl: options.fetchImpl });
|
|
111
|
+
const state = await (0, quota_1.fetchQuota)(token, { fetchImpl: options.fetchImpl, plan });
|
|
103
112
|
return { ...state, providerId: 'claude-code', source: 'api', capturedAt };
|
|
104
113
|
}
|
|
105
114
|
case 'codex':
|
|
@@ -181,7 +190,10 @@ async function resolveQuota(options) {
|
|
|
181
190
|
const stored = identity.accountId
|
|
182
191
|
? readSnapshot(snapshotProviderId(providerId), identity.accountId, now)
|
|
183
192
|
: null;
|
|
184
|
-
|
|
193
|
+
// A sample whose every window has reset since says nothing about current
|
|
194
|
+
// usage; it is reported as expired instead of being shown as live numbers.
|
|
195
|
+
const storedExpired = Boolean(stored?.available && (0, quota_1.quotaSampleExpired)(stored, now.getTime()));
|
|
196
|
+
const snapshot = stored?.available && !storedExpired ? stored : null;
|
|
185
197
|
if (preferFresh && snapshot && snapshot.freshness === 'fresh') {
|
|
186
198
|
return finish(snapshot, 'snapshot-fresh');
|
|
187
199
|
}
|
|
@@ -227,8 +239,21 @@ async function resolveQuota(options) {
|
|
|
227
239
|
const fallback = finish(snapshot, snapshotResolution(snapshot));
|
|
228
240
|
return apiState ? { ...fallback, failure: (0, quotaPresentation_1.describeQuotaFailure)(apiState) } : fallback;
|
|
229
241
|
}
|
|
242
|
+
if (storedExpired && stored) {
|
|
243
|
+
const expired = unavailableState(providerId, expiredSampleMessage(stored, now), capturedAt);
|
|
244
|
+
return apiState
|
|
245
|
+
? { ...finish(expired, 'unavailable'), failure: (0, quotaPresentation_1.describeQuotaFailure)(apiState) }
|
|
246
|
+
: finish(expired, 'unavailable');
|
|
247
|
+
}
|
|
230
248
|
return finish(apiState ??
|
|
231
249
|
unavailableState(providerId, stored
|
|
232
250
|
? 'The last persisted quota sample was unavailable.'
|
|
233
251
|
: 'No quota data is available yet; run the provider once so a sample can be recorded.', capturedAt), 'unavailable');
|
|
234
252
|
}
|
|
253
|
+
function expiredSampleMessage(stored, now) {
|
|
254
|
+
const capturedMs = stored.capturedAt ? Date.parse(stored.capturedAt) : NaN;
|
|
255
|
+
const when = Number.isFinite(capturedMs)
|
|
256
|
+
? ` from ${new Date(capturedMs).toISOString().slice(0, 10)} (${(0, quota_1.formatQuotaAge)(now.getTime() - capturedMs)})`
|
|
257
|
+
: '';
|
|
258
|
+
return `The last quota sample${when} has expired: every window has reset since. No current data.`;
|
|
259
|
+
}
|