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/esm/modelInfo.js
CHANGED
|
@@ -37,6 +37,192 @@ export { MODEL_PROVIDER_IDS } from './providerIds';
|
|
|
37
37
|
* prefix matching cannot bridge the two, so both are needed.
|
|
38
38
|
*/
|
|
39
39
|
const PRICING_TABLE = {
|
|
40
|
+
// Current coding models verified 2026-10-30. Standard short-context rates;
|
|
41
|
+
// DeepSeek uses peak rates. Gemini promotions end 2026-11-30 (Argon) and
|
|
42
|
+
// 2026-12-31 (Flash). Haiku 5.5 costs more above 100K prompt tokens.
|
|
43
|
+
// Sources and estimation limits: docs/configuration/model-resolution.md.
|
|
44
|
+
// Explicit direct-provider keys support offline OpenCode model IDs without
|
|
45
|
+
// stripping arbitrary deployment prefixes or changing alias resolution.
|
|
46
|
+
'gpt-6.1-sol': {
|
|
47
|
+
inputCostPerMillion: 2,
|
|
48
|
+
outputCostPerMillion: 10,
|
|
49
|
+
cacheWriteCostPerMillion: 2.5,
|
|
50
|
+
cacheReadCostPerMillion: 0.1,
|
|
51
|
+
},
|
|
52
|
+
'openai/gpt-6.1-sol': {
|
|
53
|
+
inputCostPerMillion: 2,
|
|
54
|
+
outputCostPerMillion: 10,
|
|
55
|
+
cacheWriteCostPerMillion: 2.5,
|
|
56
|
+
cacheReadCostPerMillion: 0.1,
|
|
57
|
+
},
|
|
58
|
+
'gpt-6.1-terra': {
|
|
59
|
+
inputCostPerMillion: 1,
|
|
60
|
+
outputCostPerMillion: 5,
|
|
61
|
+
cacheWriteCostPerMillion: 1.25,
|
|
62
|
+
cacheReadCostPerMillion: 0.05,
|
|
63
|
+
},
|
|
64
|
+
'openai/gpt-6.1-terra': {
|
|
65
|
+
inputCostPerMillion: 1,
|
|
66
|
+
outputCostPerMillion: 5,
|
|
67
|
+
cacheWriteCostPerMillion: 1.25,
|
|
68
|
+
cacheReadCostPerMillion: 0.05,
|
|
69
|
+
},
|
|
70
|
+
'claude-sonnet-5-5': {
|
|
71
|
+
inputCostPerMillion: 2,
|
|
72
|
+
outputCostPerMillion: 10,
|
|
73
|
+
cacheWriteCostPerMillion: 2.5,
|
|
74
|
+
cacheReadCostPerMillion: 0.1,
|
|
75
|
+
},
|
|
76
|
+
'anthropic/claude-sonnet-5-5': {
|
|
77
|
+
inputCostPerMillion: 2,
|
|
78
|
+
outputCostPerMillion: 10,
|
|
79
|
+
cacheWriteCostPerMillion: 2.5,
|
|
80
|
+
cacheReadCostPerMillion: 0.1,
|
|
81
|
+
},
|
|
82
|
+
'claude-sonnet-5.5': {
|
|
83
|
+
inputCostPerMillion: 2,
|
|
84
|
+
outputCostPerMillion: 10,
|
|
85
|
+
cacheWriteCostPerMillion: 2.5,
|
|
86
|
+
cacheReadCostPerMillion: 0.1,
|
|
87
|
+
},
|
|
88
|
+
'anthropic/claude-sonnet-5.5': {
|
|
89
|
+
inputCostPerMillion: 2,
|
|
90
|
+
outputCostPerMillion: 10,
|
|
91
|
+
cacheWriteCostPerMillion: 2.5,
|
|
92
|
+
cacheReadCostPerMillion: 0.1,
|
|
93
|
+
},
|
|
94
|
+
'claude-haiku-5-5': {
|
|
95
|
+
inputCostPerMillion: 0.1,
|
|
96
|
+
outputCostPerMillion: 0.5,
|
|
97
|
+
cacheWriteCostPerMillion: 0.125,
|
|
98
|
+
cacheReadCostPerMillion: 0.01,
|
|
99
|
+
},
|
|
100
|
+
'anthropic/claude-haiku-5-5': {
|
|
101
|
+
inputCostPerMillion: 0.1,
|
|
102
|
+
outputCostPerMillion: 0.5,
|
|
103
|
+
cacheWriteCostPerMillion: 0.125,
|
|
104
|
+
cacheReadCostPerMillion: 0.01,
|
|
105
|
+
},
|
|
106
|
+
'claude-haiku-5.5': {
|
|
107
|
+
inputCostPerMillion: 0.1,
|
|
108
|
+
outputCostPerMillion: 0.5,
|
|
109
|
+
cacheWriteCostPerMillion: 0.125,
|
|
110
|
+
cacheReadCostPerMillion: 0.01,
|
|
111
|
+
},
|
|
112
|
+
'anthropic/claude-haiku-5.5': {
|
|
113
|
+
inputCostPerMillion: 0.1,
|
|
114
|
+
outputCostPerMillion: 0.5,
|
|
115
|
+
cacheWriteCostPerMillion: 0.125,
|
|
116
|
+
cacheReadCostPerMillion: 0.01,
|
|
117
|
+
},
|
|
118
|
+
'gemini-4-argon': {
|
|
119
|
+
inputCostPerMillion: 2,
|
|
120
|
+
outputCostPerMillion: 10,
|
|
121
|
+
cacheWriteCostPerMillion: 0,
|
|
122
|
+
cacheReadCostPerMillion: 0.1,
|
|
123
|
+
},
|
|
124
|
+
'gemini/gemini-4-argon': {
|
|
125
|
+
inputCostPerMillion: 2,
|
|
126
|
+
outputCostPerMillion: 10,
|
|
127
|
+
cacheWriteCostPerMillion: 0,
|
|
128
|
+
cacheReadCostPerMillion: 0.1,
|
|
129
|
+
},
|
|
130
|
+
'gemini-3.8-flash': {
|
|
131
|
+
inputCostPerMillion: 0.75,
|
|
132
|
+
outputCostPerMillion: 3.75,
|
|
133
|
+
cacheWriteCostPerMillion: 0,
|
|
134
|
+
cacheReadCostPerMillion: 0.075,
|
|
135
|
+
},
|
|
136
|
+
'gemini/gemini-3.8-flash': {
|
|
137
|
+
inputCostPerMillion: 0.75,
|
|
138
|
+
outputCostPerMillion: 3.75,
|
|
139
|
+
cacheWriteCostPerMillion: 0,
|
|
140
|
+
cacheReadCostPerMillion: 0.075,
|
|
141
|
+
},
|
|
142
|
+
'deepseek-flash': {
|
|
143
|
+
inputCostPerMillion: 0.3,
|
|
144
|
+
outputCostPerMillion: 1.2,
|
|
145
|
+
cacheWriteCostPerMillion: 0,
|
|
146
|
+
cacheReadCostPerMillion: 0.006,
|
|
147
|
+
},
|
|
148
|
+
'deepseek/deepseek-flash': {
|
|
149
|
+
inputCostPerMillion: 0.3,
|
|
150
|
+
outputCostPerMillion: 1.2,
|
|
151
|
+
cacheWriteCostPerMillion: 0,
|
|
152
|
+
cacheReadCostPerMillion: 0.006,
|
|
153
|
+
},
|
|
154
|
+
'deepseek-v4-flash': {
|
|
155
|
+
inputCostPerMillion: 0.3,
|
|
156
|
+
outputCostPerMillion: 1.2,
|
|
157
|
+
cacheWriteCostPerMillion: 0,
|
|
158
|
+
cacheReadCostPerMillion: 0.006,
|
|
159
|
+
},
|
|
160
|
+
'deepseek/deepseek-v4-flash': {
|
|
161
|
+
inputCostPerMillion: 0.3,
|
|
162
|
+
outputCostPerMillion: 1.2,
|
|
163
|
+
cacheWriteCostPerMillion: 0,
|
|
164
|
+
cacheReadCostPerMillion: 0.006,
|
|
165
|
+
},
|
|
166
|
+
'deepseek-v4-flash-vision-exp': {
|
|
167
|
+
inputCostPerMillion: 0.3,
|
|
168
|
+
outputCostPerMillion: 1.2,
|
|
169
|
+
cacheWriteCostPerMillion: 0,
|
|
170
|
+
cacheReadCostPerMillion: 0.006,
|
|
171
|
+
},
|
|
172
|
+
'deepseek/deepseek-v4-flash-vision-exp': {
|
|
173
|
+
inputCostPerMillion: 0.3,
|
|
174
|
+
outputCostPerMillion: 1.2,
|
|
175
|
+
cacheWriteCostPerMillion: 0,
|
|
176
|
+
cacheReadCostPerMillion: 0.006,
|
|
177
|
+
},
|
|
178
|
+
'deepseek-v4-pro': {
|
|
179
|
+
inputCostPerMillion: 1.32,
|
|
180
|
+
outputCostPerMillion: 3.96,
|
|
181
|
+
cacheWriteCostPerMillion: 0,
|
|
182
|
+
cacheReadCostPerMillion: 0.044,
|
|
183
|
+
},
|
|
184
|
+
'deepseek/deepseek-v4-pro': {
|
|
185
|
+
inputCostPerMillion: 1.32,
|
|
186
|
+
outputCostPerMillion: 3.96,
|
|
187
|
+
cacheWriteCostPerMillion: 0,
|
|
188
|
+
cacheReadCostPerMillion: 0.044,
|
|
189
|
+
},
|
|
190
|
+
'glm-5.3': {
|
|
191
|
+
inputCostPerMillion: 1.4,
|
|
192
|
+
outputCostPerMillion: 4.4,
|
|
193
|
+
cacheWriteCostPerMillion: 0,
|
|
194
|
+
cacheReadCostPerMillion: 0.26,
|
|
195
|
+
},
|
|
196
|
+
'zai/glm-5.3': {
|
|
197
|
+
inputCostPerMillion: 1.4,
|
|
198
|
+
outputCostPerMillion: 4.4,
|
|
199
|
+
cacheWriteCostPerMillion: 0,
|
|
200
|
+
cacheReadCostPerMillion: 0.26,
|
|
201
|
+
},
|
|
202
|
+
'glm-5.3-flash': {
|
|
203
|
+
inputCostPerMillion: 0.15,
|
|
204
|
+
outputCostPerMillion: 0.5,
|
|
205
|
+
cacheWriteCostPerMillion: 0,
|
|
206
|
+
cacheReadCostPerMillion: 0.03,
|
|
207
|
+
},
|
|
208
|
+
'zai/glm-5.3-flash': {
|
|
209
|
+
inputCostPerMillion: 0.15,
|
|
210
|
+
outputCostPerMillion: 0.5,
|
|
211
|
+
cacheWriteCostPerMillion: 0,
|
|
212
|
+
cacheReadCostPerMillion: 0.03,
|
|
213
|
+
},
|
|
214
|
+
'glm-5.3-flashx': {
|
|
215
|
+
inputCostPerMillion: 0.37,
|
|
216
|
+
outputCostPerMillion: 1.25,
|
|
217
|
+
cacheWriteCostPerMillion: 0,
|
|
218
|
+
cacheReadCostPerMillion: 0.075,
|
|
219
|
+
},
|
|
220
|
+
'zai/glm-5.3-flashx': {
|
|
221
|
+
inputCostPerMillion: 0.37,
|
|
222
|
+
outputCostPerMillion: 1.25,
|
|
223
|
+
cacheWriteCostPerMillion: 0,
|
|
224
|
+
cacheReadCostPerMillion: 0.075,
|
|
225
|
+
},
|
|
40
226
|
// ── Anthropic: Claude ──
|
|
41
227
|
// Fable 5.1 cache reads cost 0.025x input; Fable 5 retains its 0.1x rate.
|
|
42
228
|
'claude-fable-5-1': {
|
|
@@ -290,7 +290,7 @@ export class MultiProviderQuotaService {
|
|
|
290
290
|
await this.refreshPeakHours();
|
|
291
291
|
const creds = await this.readClaudeCredentials();
|
|
292
292
|
if (!creds) {
|
|
293
|
-
this.handleUnavailableState(makeUnavailableClaudeState('Renew your Claude
|
|
293
|
+
this.handleUnavailableState(makeUnavailableClaudeState('Renew your Claude login to view quota', 'auth'));
|
|
294
294
|
return;
|
|
295
295
|
}
|
|
296
296
|
const state = await this.fetchClaudeQuota(creds.accessToken);
|
package/dist/esm/quota.js
CHANGED
|
@@ -18,6 +18,126 @@ export function classifyQuotaFreshness(ageMs) {
|
|
|
18
18
|
return 'aging';
|
|
19
19
|
return 'stale';
|
|
20
20
|
}
|
|
21
|
+
const HOUR_MS = 3_600_000;
|
|
22
|
+
const DAY_MS = 86_400_000;
|
|
23
|
+
/** Whether the provider reports this window for the account at all. */
|
|
24
|
+
export function quotaWindowApplies(window) {
|
|
25
|
+
return Boolean(window) && window?.absent !== true;
|
|
26
|
+
}
|
|
27
|
+
/** Whether the window applies and carries a reading worth showing (a reset time or any usage). */
|
|
28
|
+
export function quotaWindowHasData(window) {
|
|
29
|
+
if (!window || window.absent === true)
|
|
30
|
+
return false;
|
|
31
|
+
return Boolean(window.resetsAt) || window.utilization > 0;
|
|
32
|
+
}
|
|
33
|
+
function usableWindowMs(windowMs) {
|
|
34
|
+
return typeof windowMs === 'number' && Number.isFinite(windowMs) && windowMs > 0
|
|
35
|
+
? windowMs
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* True when every window of a sample that has a known reset time has already
|
|
40
|
+
* reset by `nowMs`. Such a sample says nothing about current usage — showing
|
|
41
|
+
* it as live would print old percentages with "resets now". Samples without
|
|
42
|
+
* any reset time are never considered expired.
|
|
43
|
+
*/
|
|
44
|
+
export function quotaSampleExpired(quota, nowMs = Date.now()) {
|
|
45
|
+
const resets = [quota.fiveHour, quota.sevenDay]
|
|
46
|
+
.filter((window) => !window.absent && window.resetsAt)
|
|
47
|
+
.map((window) => Date.parse(window.resetsAt))
|
|
48
|
+
.filter((ms) => Number.isFinite(ms));
|
|
49
|
+
return resets.length > 0 && resets.every((ms) => ms <= nowMs);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Human label for a window length: `5-Hour`, `Daily`, `Weekly`, `30-Day`, …
|
|
53
|
+
* Returns `fallback` when the length is unknown or not a whole number of hours.
|
|
54
|
+
*/
|
|
55
|
+
export function quotaWindowLabel(windowMs, fallback) {
|
|
56
|
+
const ms = usableWindowMs(windowMs);
|
|
57
|
+
if (ms === undefined)
|
|
58
|
+
return fallback;
|
|
59
|
+
if (ms === DAY_MS)
|
|
60
|
+
return 'Daily';
|
|
61
|
+
if (ms === 7 * DAY_MS)
|
|
62
|
+
return 'Weekly';
|
|
63
|
+
if (ms % DAY_MS === 0)
|
|
64
|
+
return `${ms / DAY_MS}-Day`;
|
|
65
|
+
if (ms % HOUR_MS === 0)
|
|
66
|
+
return `${ms / HOUR_MS}-Hour`;
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
/** Compact label for a window length: `5h`, `1d`, `7d`. Returns `fallback` when unknown. */
|
|
70
|
+
export function quotaWindowShortLabel(windowMs, fallback) {
|
|
71
|
+
const ms = usableWindowMs(windowMs);
|
|
72
|
+
if (ms === undefined)
|
|
73
|
+
return fallback;
|
|
74
|
+
if (ms % DAY_MS === 0)
|
|
75
|
+
return `${ms / DAY_MS}d`;
|
|
76
|
+
if (ms % HOUR_MS === 0)
|
|
77
|
+
return `${ms / HOUR_MS}h`;
|
|
78
|
+
if (ms % 60_000 === 0)
|
|
79
|
+
return `${ms / 60_000}m`;
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The reported meters that `fiveHour` and `sevenDay` do not already show —
|
|
84
|
+
* per-model or per-surface weekly caps and any future meter kind.
|
|
85
|
+
*/
|
|
86
|
+
export function extraQuotaRows(quota) {
|
|
87
|
+
return (quota.limits ?? []).filter((row) => row.kind !== 'session' && row.kind !== 'weekly_all');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Display name for a Claude subscription. The 20× tier was renamed Ultra and
|
|
91
|
+
* the 5× tier Max on 2026-10-30; credentials keep the old tier ids.
|
|
92
|
+
*/
|
|
93
|
+
export function claudePlanLabel(plan) {
|
|
94
|
+
const tier = plan?.rateLimitTier?.trim().toLowerCase() ?? '';
|
|
95
|
+
if (tier.includes('ultra') || tier.includes('max_20x'))
|
|
96
|
+
return 'Ultra';
|
|
97
|
+
if (tier.includes('max_5x'))
|
|
98
|
+
return 'Max';
|
|
99
|
+
const subscription = plan?.subscriptionType?.trim().toLowerCase();
|
|
100
|
+
if (!subscription)
|
|
101
|
+
return undefined;
|
|
102
|
+
return subscription.charAt(0).toUpperCase() + subscription.slice(1);
|
|
103
|
+
}
|
|
104
|
+
function formatMinorUnits(amount, currency, decimals) {
|
|
105
|
+
const value = amount / 10 ** decimals;
|
|
106
|
+
if (currency) {
|
|
107
|
+
try {
|
|
108
|
+
return new Intl.NumberFormat('en-US', {
|
|
109
|
+
style: 'currency',
|
|
110
|
+
currency,
|
|
111
|
+
minimumFractionDigits: decimals,
|
|
112
|
+
maximumFractionDigits: decimals,
|
|
113
|
+
}).format(value);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return `${value.toFixed(decimals)} ${currency}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return value.toFixed(decimals);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* One-line extra-usage summary, e.g. `$12.40 of $200.00 used` or
|
|
123
|
+
* `$0.00 of $200.00 used · off (out of credits)`.
|
|
124
|
+
*/
|
|
125
|
+
export function formatExtraUsage(extra) {
|
|
126
|
+
const decimals = typeof extra.decimalPlaces === 'number' &&
|
|
127
|
+
Number.isInteger(extra.decimalPlaces) &&
|
|
128
|
+
extra.decimalPlaces >= 0 &&
|
|
129
|
+
extra.decimalPlaces <= 6
|
|
130
|
+
? extra.decimalPlaces
|
|
131
|
+
: 2;
|
|
132
|
+
const used = formatMinorUnits(extra.usedCredits ?? 0, extra.currency, decimals);
|
|
133
|
+
const amount = extra.monthlyLimit !== null
|
|
134
|
+
? `${used} of ${formatMinorUnits(extra.monthlyLimit, extra.currency, decimals)} used`
|
|
135
|
+
: `${used} used`;
|
|
136
|
+
if (extra.enabled)
|
|
137
|
+
return amount;
|
|
138
|
+
const reason = extra.disabledReason ? ` (${extra.disabledReason.replace(/_/g, ' ')})` : '';
|
|
139
|
+
return `${amount} · off${reason}`;
|
|
140
|
+
}
|
|
21
141
|
/** Compact age label for status lines and table footers: `just now`, `3m ago`, `2h ago`, `3d ago`. */
|
|
22
142
|
export function formatQuotaAge(ageMs) {
|
|
23
143
|
if (typeof ageMs !== 'number' || !Number.isFinite(ageMs) || ageMs < 0)
|
|
@@ -56,21 +176,21 @@ export function withQuotaProjections(state, options = {}) {
|
|
|
56
176
|
return state;
|
|
57
177
|
const next = { ...state };
|
|
58
178
|
const capturedAt = options.capturedAt ?? state.capturedAt;
|
|
59
|
-
if (next.projectedFiveHour == null) {
|
|
179
|
+
if (next.projectedFiveHour == null && !next.fiveHour.absent) {
|
|
60
180
|
const projected = projectQuotaWindow({
|
|
61
181
|
utilization: next.fiveHour.utilization,
|
|
62
182
|
resetsAt: next.fiveHour.resetsAt,
|
|
63
|
-
windowMs: options.fiveHourWindowMs ?? FIVE_HOUR_WINDOW_MS,
|
|
183
|
+
windowMs: options.fiveHourWindowMs ?? usableWindowMs(next.fiveHour.windowMs) ?? FIVE_HOUR_WINDOW_MS,
|
|
64
184
|
capturedAt,
|
|
65
185
|
});
|
|
66
186
|
if (projected !== undefined)
|
|
67
187
|
next.projectedFiveHour = projected;
|
|
68
188
|
}
|
|
69
|
-
if (next.projectedSevenDay == null) {
|
|
189
|
+
if (next.projectedSevenDay == null && !next.sevenDay.absent) {
|
|
70
190
|
const projected = projectQuotaWindow({
|
|
71
191
|
utilization: next.sevenDay.utilization,
|
|
72
192
|
resetsAt: next.sevenDay.resetsAt,
|
|
73
|
-
windowMs: options.sevenDayWindowMs ?? SEVEN_DAY_WINDOW_MS,
|
|
193
|
+
windowMs: options.sevenDayWindowMs ?? usableWindowMs(next.sevenDay.windowMs) ?? SEVEN_DAY_WINDOW_MS,
|
|
74
194
|
capturedAt,
|
|
75
195
|
});
|
|
76
196
|
if (projected !== undefined)
|
|
@@ -78,6 +198,235 @@ export function withQuotaProjections(state, options = {}) {
|
|
|
78
198
|
}
|
|
79
199
|
return next;
|
|
80
200
|
}
|
|
201
|
+
// ── Claude usage endpoint payload ──
|
|
202
|
+
/** Hard cap on parsed meter rows, matching Claude Code's own bound. */
|
|
203
|
+
const MAX_LIMIT_ROWS = 64;
|
|
204
|
+
const MAX_LABEL_LENGTH = 128;
|
|
205
|
+
function isRecord(value) {
|
|
206
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
207
|
+
}
|
|
208
|
+
function finiteNumber(value) {
|
|
209
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
210
|
+
}
|
|
211
|
+
function nullableFiniteNumber(value) {
|
|
212
|
+
return finiteNumber(value) ?? null;
|
|
213
|
+
}
|
|
214
|
+
function shortString(value) {
|
|
215
|
+
if (typeof value !== 'string')
|
|
216
|
+
return undefined;
|
|
217
|
+
const trimmed = value.trim();
|
|
218
|
+
return trimmed && trimmed.length <= MAX_LABEL_LENGTH ? trimmed : undefined;
|
|
219
|
+
}
|
|
220
|
+
function displayName(value) {
|
|
221
|
+
return isRecord(value) ? shortString(value.display_name) : undefined;
|
|
222
|
+
}
|
|
223
|
+
function usageWindow(value) {
|
|
224
|
+
if (!isRecord(value))
|
|
225
|
+
return null;
|
|
226
|
+
return {
|
|
227
|
+
utilization: finiteNumber(value.utilization) ?? 0,
|
|
228
|
+
resetsAt: typeof value.resets_at === 'string' ? value.resets_at : '',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function absentWindow() {
|
|
232
|
+
return { utilization: 0, resetsAt: '', absent: true };
|
|
233
|
+
}
|
|
234
|
+
function windowMsForMeter(kind, group) {
|
|
235
|
+
if (kind === 'session' || group === 'session')
|
|
236
|
+
return FIVE_HOUR_WINDOW_MS;
|
|
237
|
+
if (group === 'weekly' || kind.startsWith('weekly'))
|
|
238
|
+
return SEVEN_DAY_WINDOW_MS;
|
|
239
|
+
if (group === 'daily')
|
|
240
|
+
return DAY_MS;
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
function titleCase(value) {
|
|
244
|
+
return value
|
|
245
|
+
.split(/[_\s-]+/)
|
|
246
|
+
.filter(Boolean)
|
|
247
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
248
|
+
.join(' ');
|
|
249
|
+
}
|
|
250
|
+
function meterLabel(kind, group, scope) {
|
|
251
|
+
let base;
|
|
252
|
+
if (kind === 'session' || group === 'session')
|
|
253
|
+
base = '5-Hour';
|
|
254
|
+
else if (group === 'weekly' || kind.startsWith('weekly'))
|
|
255
|
+
base = '7-Day';
|
|
256
|
+
else if (group === 'daily')
|
|
257
|
+
base = 'Daily';
|
|
258
|
+
else
|
|
259
|
+
base = titleCase(group || kind) || kind;
|
|
260
|
+
return scope ? `${base} (${scope})` : base;
|
|
261
|
+
}
|
|
262
|
+
function weeklySharesByIndex(value) {
|
|
263
|
+
const shares = new Map();
|
|
264
|
+
if (!Array.isArray(value))
|
|
265
|
+
return shares;
|
|
266
|
+
for (const entry of value.slice(0, MAX_LIMIT_ROWS)) {
|
|
267
|
+
if (!isRecord(entry))
|
|
268
|
+
continue;
|
|
269
|
+
const index = finiteNumber(entry.limit_index);
|
|
270
|
+
if (index === undefined || !Number.isInteger(index) || index < 0)
|
|
271
|
+
continue;
|
|
272
|
+
shares.set(index, {
|
|
273
|
+
allowance: finiteNumber(entry.allowance_percent_of_weekly),
|
|
274
|
+
used: finiteNumber(entry.used_percent_of_weekly),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return shares;
|
|
278
|
+
}
|
|
279
|
+
function buildMeterRow(input, capturedAt) {
|
|
280
|
+
const windowMs = windowMsForMeter(input.kind, input.group);
|
|
281
|
+
const projected = windowMs !== undefined
|
|
282
|
+
? projectQuotaWindow({
|
|
283
|
+
utilization: input.utilization,
|
|
284
|
+
resetsAt: input.resetsAt,
|
|
285
|
+
windowMs,
|
|
286
|
+
capturedAt,
|
|
287
|
+
})
|
|
288
|
+
: undefined;
|
|
289
|
+
return {
|
|
290
|
+
kind: input.kind,
|
|
291
|
+
group: input.group,
|
|
292
|
+
label: meterLabel(input.kind, input.group, input.scopeModel ?? input.scopeSurface),
|
|
293
|
+
utilization: input.utilization,
|
|
294
|
+
resetsAt: input.resetsAt,
|
|
295
|
+
...(windowMs !== undefined ? { windowMs } : {}),
|
|
296
|
+
...(projected !== undefined ? { projected } : {}),
|
|
297
|
+
...(input.scopeModel ? { scopeModel: input.scopeModel } : {}),
|
|
298
|
+
...(input.scopeSurface ? { scopeSurface: input.scopeSurface } : {}),
|
|
299
|
+
...(input.severity ? { severity: input.severity } : {}),
|
|
300
|
+
...(input.isActive !== undefined ? { isActive: input.isActive } : {}),
|
|
301
|
+
...(input.share?.allowance !== undefined
|
|
302
|
+
? { allowancePercentOfWeekly: input.share.allowance }
|
|
303
|
+
: {}),
|
|
304
|
+
...(input.share?.used !== undefined ? { usedPercentOfWeekly: input.share.used } : {}),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function parseLimitRows(value, sharesValue, capturedAt) {
|
|
308
|
+
if (!Array.isArray(value))
|
|
309
|
+
return undefined;
|
|
310
|
+
const shares = weeklySharesByIndex(sharesValue);
|
|
311
|
+
const rows = [];
|
|
312
|
+
value.slice(0, MAX_LIMIT_ROWS).forEach((raw, index) => {
|
|
313
|
+
if (!isRecord(raw))
|
|
314
|
+
return;
|
|
315
|
+
const kind = shortString(raw.kind);
|
|
316
|
+
const percent = finiteNumber(raw.percent);
|
|
317
|
+
if (!kind || percent === undefined)
|
|
318
|
+
return;
|
|
319
|
+
const scope = isRecord(raw.scope) ? raw.scope : undefined;
|
|
320
|
+
rows.push(buildMeterRow({
|
|
321
|
+
kind,
|
|
322
|
+
group: shortString(raw.group) ?? '',
|
|
323
|
+
utilization: Math.max(0, percent),
|
|
324
|
+
resetsAt: typeof raw.resets_at === 'string' ? raw.resets_at : '',
|
|
325
|
+
scopeModel: displayName(scope?.model),
|
|
326
|
+
scopeSurface: displayName(scope?.surface),
|
|
327
|
+
severity: shortString(raw.severity),
|
|
328
|
+
isActive: typeof raw.is_active === 'boolean' ? raw.is_active : undefined,
|
|
329
|
+
share: shares.get(index),
|
|
330
|
+
}, capturedAt));
|
|
331
|
+
});
|
|
332
|
+
return rows;
|
|
333
|
+
}
|
|
334
|
+
/** Pre-`limits[]` per-model weekly fields, read only when the server sends no `limits[]`. */
|
|
335
|
+
const LEGACY_SCOPED_WEEKLY_FIELDS = [
|
|
336
|
+
['seven_day_opus', 'Opus'],
|
|
337
|
+
['seven_day_sonnet', 'Sonnet'],
|
|
338
|
+
['seven_day_overage_included', 'Fable'],
|
|
339
|
+
];
|
|
340
|
+
function legacyScopedRows(data, capturedAt) {
|
|
341
|
+
const rows = [];
|
|
342
|
+
for (const [field, model] of LEGACY_SCOPED_WEEKLY_FIELDS) {
|
|
343
|
+
const window = usageWindow(data[field]);
|
|
344
|
+
if (!window)
|
|
345
|
+
continue;
|
|
346
|
+
rows.push(buildMeterRow({
|
|
347
|
+
kind: 'weekly_scoped',
|
|
348
|
+
group: 'weekly',
|
|
349
|
+
utilization: window.utilization,
|
|
350
|
+
resetsAt: window.resetsAt,
|
|
351
|
+
scopeModel: model,
|
|
352
|
+
}, capturedAt));
|
|
353
|
+
}
|
|
354
|
+
return rows;
|
|
355
|
+
}
|
|
356
|
+
function parseExtraUsage(value) {
|
|
357
|
+
if (!isRecord(value) || typeof value.is_enabled !== 'boolean')
|
|
358
|
+
return undefined;
|
|
359
|
+
const decimalPlaces = finiteNumber(value.decimal_places);
|
|
360
|
+
return {
|
|
361
|
+
enabled: value.is_enabled,
|
|
362
|
+
monthlyLimit: nullableFiniteNumber(value.monthly_limit),
|
|
363
|
+
usedCredits: nullableFiniteNumber(value.used_credits),
|
|
364
|
+
utilization: nullableFiniteNumber(value.utilization),
|
|
365
|
+
...(shortString(value.currency) ? { currency: shortString(value.currency) } : {}),
|
|
366
|
+
...(decimalPlaces !== undefined ? { decimalPlaces } : {}),
|
|
367
|
+
...(shortString(value.disabled_reason)
|
|
368
|
+
? { disabledReason: shortString(value.disabled_reason) }
|
|
369
|
+
: {}),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function windowFromMeter(row) {
|
|
373
|
+
return row ? { utilization: row.utilization, resetsAt: row.resetsAt } : null;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Normalize a Claude OAuth usage-endpoint body into a `QuotaState`.
|
|
377
|
+
*
|
|
378
|
+
* - `five_hour`/`seven_day` that are null or missing become `absent` windows
|
|
379
|
+
* (Ultra plans have no five-hour session limit), unless `limits[]` carries
|
|
380
|
+
* the matching `session`/`weekly_all` meter.
|
|
381
|
+
* - `limits[]` is kept verbatim as `limits` (with derived labels, window
|
|
382
|
+
* lengths, and projections); older servers' `seven_day_opus` /
|
|
383
|
+
* `seven_day_sonnet` / `seven_day_overage_included` fill it instead.
|
|
384
|
+
* - `extra_usage` becomes `extraUsage`.
|
|
385
|
+
*
|
|
386
|
+
* Bodies in the original two-window shape produce exactly the state they did
|
|
387
|
+
* before these fields existed.
|
|
388
|
+
*/
|
|
389
|
+
export function quotaStateFromClaudeUsagePayload(payload, options = {}) {
|
|
390
|
+
const data = isRecord(payload) ? payload : {};
|
|
391
|
+
const capturedAt = options.capturedAt;
|
|
392
|
+
const parsedLimits = parseLimitRows(data.limits, data.weekly_scoped_shares, capturedAt);
|
|
393
|
+
const legacyRows = parsedLimits === undefined ? legacyScopedRows(data, capturedAt) : [];
|
|
394
|
+
const limits = parsedLimits ?? (legacyRows.length > 0 ? legacyRows : undefined);
|
|
395
|
+
const fiveHour = usageWindow(data.five_hour) ??
|
|
396
|
+
windowFromMeter(parsedLimits?.find((row) => row.kind === 'session')) ??
|
|
397
|
+
absentWindow();
|
|
398
|
+
const sevenDay = usageWindow(data.seven_day) ??
|
|
399
|
+
windowFromMeter(parsedLimits?.find((row) => row.kind === 'weekly_all')) ??
|
|
400
|
+
absentWindow();
|
|
401
|
+
const extraUsage = parseExtraUsage(data.extra_usage);
|
|
402
|
+
const projectedFiveHour = fiveHour.absent
|
|
403
|
+
? undefined
|
|
404
|
+
: projectQuotaWindow({ ...fiveHour, windowMs: FIVE_HOUR_WINDOW_MS, capturedAt });
|
|
405
|
+
const projectedSevenDay = sevenDay.absent
|
|
406
|
+
? undefined
|
|
407
|
+
: projectQuotaWindow({ ...sevenDay, windowMs: SEVEN_DAY_WINDOW_MS, capturedAt });
|
|
408
|
+
return {
|
|
409
|
+
fiveHour,
|
|
410
|
+
sevenDay,
|
|
411
|
+
available: true,
|
|
412
|
+
projectedFiveHour,
|
|
413
|
+
projectedSevenDay,
|
|
414
|
+
...(limits ? { limits } : {}),
|
|
415
|
+
...(extraUsage ? { extraUsage } : {}),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/** Stamp `planType` / `planLabel` from Claude credential plan facts (no-op without them). */
|
|
419
|
+
export function withClaudePlan(state, plan) {
|
|
420
|
+
const planLabel = claudePlanLabel(plan);
|
|
421
|
+
const planType = plan?.subscriptionType?.trim() || undefined;
|
|
422
|
+
if (!planLabel && !planType)
|
|
423
|
+
return state;
|
|
424
|
+
return {
|
|
425
|
+
...state,
|
|
426
|
+
...(planType ? { planType } : {}),
|
|
427
|
+
...(planLabel ? { planLabel } : {}),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
81
430
|
/**
|
|
82
431
|
* Helper to build an unavailable QuotaState.
|
|
83
432
|
*/
|
|
@@ -110,6 +459,7 @@ function parseRetryAfterMs(retryAfter) {
|
|
|
110
459
|
*
|
|
111
460
|
* @param accessToken - OAuth access token from `readClaudeMaxCredentials()`
|
|
112
461
|
* @param options.fetchImpl - Fetch implementation (defaults to the global `fetch`; tests inject a stub)
|
|
462
|
+
* @param options.plan - Plan facts from the same credentials; stamps `planType` and `planLabel`
|
|
113
463
|
* @returns QuotaState with utilization data and elapsed-time projections
|
|
114
464
|
*/
|
|
115
465
|
export async function fetchQuota(accessToken, options = {}) {
|
|
@@ -152,26 +502,8 @@ export async function fetchQuota(accessToken, options = {}) {
|
|
|
152
502
|
httpStatus: res.status,
|
|
153
503
|
});
|
|
154
504
|
}
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
const sevenUtil = data.seven_day?.utilization ?? 0;
|
|
158
|
-
const fiveResetsAt = data.five_hour?.resets_at ?? '';
|
|
159
|
-
const sevenResetsAt = data.seven_day?.resets_at ?? '';
|
|
160
|
-
return {
|
|
161
|
-
fiveHour: { utilization: fiveUtil, resetsAt: fiveResetsAt },
|
|
162
|
-
sevenDay: { utilization: sevenUtil, resetsAt: sevenResetsAt },
|
|
163
|
-
available: true,
|
|
164
|
-
projectedFiveHour: projectQuotaWindow({
|
|
165
|
-
utilization: fiveUtil,
|
|
166
|
-
resetsAt: fiveResetsAt,
|
|
167
|
-
windowMs: FIVE_HOUR_WINDOW_MS,
|
|
168
|
-
}),
|
|
169
|
-
projectedSevenDay: projectQuotaWindow({
|
|
170
|
-
utilization: sevenUtil,
|
|
171
|
-
resetsAt: sevenResetsAt,
|
|
172
|
-
windowMs: SEVEN_DAY_WINDOW_MS,
|
|
173
|
-
}),
|
|
174
|
-
};
|
|
505
|
+
const state = quotaStateFromClaudeUsagePayload(await res.json());
|
|
506
|
+
return withClaudePlan(state, options.plan);
|
|
175
507
|
}
|
|
176
508
|
catch {
|
|
177
509
|
return unavailableState('Network error', { failureKind: 'network' });
|
package/dist/esm/quotaHistory.js
CHANGED
|
@@ -159,11 +159,26 @@ function pruneFileSync(filePath, retentionDays) {
|
|
|
159
159
|
}
|
|
160
160
|
return { kept: keptLines.length, pruned };
|
|
161
161
|
}
|
|
162
|
+
function historyWindowToQuota(window) {
|
|
163
|
+
return {
|
|
164
|
+
utilization: window.utilization,
|
|
165
|
+
resetsAt: window.resetsAt,
|
|
166
|
+
...(window.absent ? { absent: true } : {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Project a quota window onto the history-sample shape, keeping the `absent`
|
|
171
|
+
* marker so summaries and the derived snapshot do not mistake "no such
|
|
172
|
+
* window" for "0% used".
|
|
173
|
+
*/
|
|
174
|
+
export function quotaHistoryWindow(window) {
|
|
175
|
+
return historyWindowToQuota(window);
|
|
176
|
+
}
|
|
162
177
|
function sampleToQuotaState(sample) {
|
|
163
178
|
const providerId = runtimeProviderToSnapshotProvider(sample.runtimeProvider);
|
|
164
179
|
return {
|
|
165
|
-
fiveHour:
|
|
166
|
-
sevenDay:
|
|
180
|
+
fiveHour: historyWindowToQuota(sample.fiveHour),
|
|
181
|
+
sevenDay: historyWindowToQuota(sample.sevenDay),
|
|
167
182
|
available: sample.available,
|
|
168
183
|
error: sample.error,
|
|
169
184
|
providerId,
|
|
@@ -329,12 +344,22 @@ export async function readQuotaHistoryDailyBuckets(options) {
|
|
|
329
344
|
let maxSeven = 0;
|
|
330
345
|
let sumFive = 0;
|
|
331
346
|
let sumSeven = 0;
|
|
347
|
+
// Averages count only samples where the window exists, so a plan without
|
|
348
|
+
// a five-hour limit does not drag the five-hour average toward zero.
|
|
349
|
+
let fiveCount = 0;
|
|
350
|
+
let sevenCount = 0;
|
|
332
351
|
let anyUnavailable = false;
|
|
333
352
|
for (const s of daySamples) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
353
|
+
if (!s.fiveHour.absent) {
|
|
354
|
+
maxFive = Math.max(maxFive, s.fiveHour.utilization);
|
|
355
|
+
sumFive += s.fiveHour.utilization;
|
|
356
|
+
fiveCount += 1;
|
|
357
|
+
}
|
|
358
|
+
if (!s.sevenDay.absent) {
|
|
359
|
+
maxSeven = Math.max(maxSeven, s.sevenDay.utilization);
|
|
360
|
+
sumSeven += s.sevenDay.utilization;
|
|
361
|
+
sevenCount += 1;
|
|
362
|
+
}
|
|
338
363
|
if (!s.available)
|
|
339
364
|
anyUnavailable = true;
|
|
340
365
|
}
|
|
@@ -344,8 +369,8 @@ export async function readQuotaHistoryDailyBuckets(options) {
|
|
|
344
369
|
samples: n,
|
|
345
370
|
maxUtilizationFiveHour: maxFive,
|
|
346
371
|
maxUtilizationSevenDay: maxSeven,
|
|
347
|
-
avgUtilizationFiveHour: Math.round((sumFive /
|
|
348
|
-
avgUtilizationSevenDay: Math.round((sumSeven /
|
|
372
|
+
avgUtilizationFiveHour: fiveCount ? Math.round((sumFive / fiveCount) * 100) / 100 : 0,
|
|
373
|
+
avgUtilizationSevenDay: sevenCount ? Math.round((sumSeven / sevenCount) * 100) / 100 : 0,
|
|
349
374
|
anyUnavailable,
|
|
350
375
|
});
|
|
351
376
|
}
|