@maci0/dsh-quota-check 0.12.2

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/lib/probes.js ADDED
@@ -0,0 +1,848 @@
1
+ /**
2
+ * Provider quota/balance probes: which endpoint answers for a provider route,
3
+ * and how its payload becomes one statusbar line plus tooltip detail.
4
+ *
5
+ * A probe resolves from a provider id and an optional configured base URL, and
6
+ * a parser turns a decoded JSON payload into a {@link ProbeReading}. Host-only
7
+ * concerns (credentials, HTTP, caching) live in `index.ts`, which is what
8
+ * makes these rules testable without a network. One exception: an endpoint whose
9
+ * ids the configuration cannot name (OmniRoute asks per upstream connection)
10
+ * declares `requests`, which reads the listing before the host asks each id.
11
+ *
12
+ * Four endpoints report a *server-side* figure: DeepSeek's `/user/balance`,
13
+ * OpenRouter's `/credits`, the z.ai / BigModel Coding Plan quota, and LiteLLM's
14
+ * `/key/info` (spend and remaining budget for the calling key). LiteLLM is also
15
+ * the fallback for any otherwise-unknown route with a configured base URL,
16
+ * because a proxy deployment names its routes after the models it serves, not
17
+ * after the proxy: an unknown host that answers `/key/info` is a LiteLLM. A
18
+ * route whose host publishes neither resolves no probe and the statusbar stays
19
+ * empty.
20
+ *
21
+ * @module dsh-quota-check/probes
22
+ */
23
+ import { isoToMs, numberOf, record, stringOf } from './util.js';
24
+ /** Remaining percent of a used-percent reading, for countdown chips. */
25
+ function remainingOf(usedPercent) {
26
+ return Math.round(100 - usedPercent);
27
+ }
28
+ /** Currency symbols the statusbar spells instead of the ISO code. */
29
+ const CURRENCY_SYMBOLS = {
30
+ USD: '$',
31
+ CNY: '¥',
32
+ RMB: '¥',
33
+ EUR: '€',
34
+ GBP: '£',
35
+ JPY: '¥',
36
+ };
37
+ /** Model-facing label per z.ai limit type. */
38
+ const ZAI_LIMIT_LABELS = {
39
+ TOKENS_LIMIT: 'Tokens',
40
+ CREDIT_LIMIT: 'Credits',
41
+ TIME_LIMIT: 'MCP tools',
42
+ };
43
+ /** z.ai window unit codes, as the quota endpoint reports them. */
44
+ const ZAI_WINDOW_UNITS = {
45
+ 1: 's',
46
+ 2: 'm',
47
+ 3: 'h',
48
+ 4: 'd',
49
+ 5: 'mo',
50
+ 6: 'w',
51
+ };
52
+ /** Provider ids that resolve to the z.ai / BigModel Coding Plan quota. */
53
+ const ZAI_IDS = new Set(['zai', 'z-ai', 'z_ai', 'zhipu', 'zhipuai', 'bigmodel', 'glm', 'glm-coding', 'zai-coding']);
54
+ /**
55
+ * Format an amount for the statusbar.
56
+ * @param amount - absolute amount.
57
+ * @param currency - ISO currency code, when the provider reports one.
58
+ * @returns symbol-prefixed amount with two decimals, or the amount with its code.
59
+ */
60
+ export function formatMoney(amount, currency) {
61
+ // Cent scaling overflows for an amount near the double ceiling, which would
62
+ // spell an unbounded balance (`Infinity`) rather than a large one.
63
+ const scaled = amount * 100;
64
+ const digits = (Number.isFinite(scaled) ? (Math.round(scaled) / 100).toFixed(2) : String(amount));
65
+ const code = currency?.toUpperCase() ?? '';
66
+ const symbol = CURRENCY_SYMBOLS[code];
67
+ if (symbol !== undefined)
68
+ return `${symbol}${digits}`;
69
+ return code === '' ? digits : `${digits} ${code}`;
70
+ }
71
+ /** Origin of a configured base URL, or the provider's own default. */
72
+ function originOf(baseURL, fallback) {
73
+ if (baseURL === undefined)
74
+ return fallback;
75
+ try {
76
+ return new URL(baseURL).origin;
77
+ }
78
+ catch {
79
+ return fallback;
80
+ }
81
+ }
82
+ /** Host of a configured base URL, lowercased and dot-stripped. */
83
+ function hostOf(baseURL) {
84
+ if (baseURL === undefined)
85
+ return '';
86
+ try {
87
+ return new URL(baseURL).hostname.toLowerCase();
88
+ }
89
+ catch {
90
+ return '';
91
+ }
92
+ }
93
+ /**
94
+ * Whether a host is a domain itself or a subdomain of it, never merely a
95
+ * longer name that happens to end with the same text. `evilopenrouter.ai`
96
+ * shares no DNS authority with `openrouter.ai`, so reading one as the other
97
+ * would send a vendor credential to a lookalike host.
98
+ * @param host - lowercased hostname.
99
+ * @param domain - the vendor domain or one of its own subdomains.
100
+ * @returns true only on a label boundary.
101
+ */
102
+ function hostIs(host, domain) {
103
+ return host === domain || host.endsWith(`.${domain}`);
104
+ }
105
+ /** Parse DeepSeek's `/user/balance` payload: `balance_infos[]`, one per currency. */
106
+ function parseDeepSeekBalance(payload) {
107
+ const infos = record(payload)?.['balance_infos'];
108
+ if (!Array.isArray(infos))
109
+ return null;
110
+ const lines = [];
111
+ let text;
112
+ for (const entry of infos) {
113
+ const info = record(entry);
114
+ if (info === undefined)
115
+ continue;
116
+ const total = numberOf(info['total_balance']);
117
+ if (total === undefined)
118
+ continue;
119
+ const currency = stringOf(info['currency']) ?? 'CNY';
120
+ const shown = formatMoney(total, currency);
121
+ text ??= shown;
122
+ lines.push(`Balance ${shown}`);
123
+ const granted = numberOf(info['granted_balance']);
124
+ if (granted !== undefined)
125
+ lines.push(`Granted ${formatMoney(granted, currency)}`);
126
+ const toppedUp = numberOf(info['topped_up_balance']);
127
+ if (toppedUp !== undefined)
128
+ lines.push(`Topped up ${formatMoney(toppedUp, currency)}`);
129
+ }
130
+ return text === undefined ? null : { text, lines };
131
+ }
132
+ /** Parse OpenRouter's `/credits` payload: credits bought minus usage. */
133
+ function parseOpenRouterCredits(payload) {
134
+ const data = record(record(payload)?.['data']);
135
+ const total = numberOf(data?.['total_credits']);
136
+ const used = numberOf(data?.['total_usage']);
137
+ if (total === undefined || used === undefined)
138
+ return null;
139
+ const remaining = total - used;
140
+ return {
141
+ text: formatMoney(remaining, 'USD'),
142
+ lines: [
143
+ `Credits ${formatMoney(total, 'USD')}`,
144
+ `Used ${formatMoney(used, 'USD')}`,
145
+ `Remaining ${formatMoney(remaining, 'USD')}`,
146
+ ],
147
+ };
148
+ }
149
+ /** Window length one z.ai limit covers, e.g. `5h` or `1w`; empty when unreported. */
150
+ function windowSuffix(limit) {
151
+ const number = numberOf(limit['number']);
152
+ const unit = numberOf(limit['unit']);
153
+ if (number === undefined || unit === undefined)
154
+ return '';
155
+ const suffix = ZAI_WINDOW_UNITS[unit];
156
+ return suffix === undefined ? '' : ` ${String(number)}${suffix}`;
157
+ }
158
+ /** Parse a z.ai / BigModel `quota/limit` payload: `data.limits[]` windows. */
159
+ function parseZaiQuota(payload) {
160
+ const data = record(record(payload)?.['data']);
161
+ const limits = data?.['limits'];
162
+ if (!Array.isArray(limits))
163
+ return null;
164
+ const windows = [];
165
+ for (const entry of limits) {
166
+ const limit = record(entry);
167
+ if (limit === undefined)
168
+ continue;
169
+ const percent = percentOf(limit['percentage']);
170
+ if (percent === undefined)
171
+ continue;
172
+ const type = stringOf(limit['type']) ?? 'Quota';
173
+ const label = `${ZAI_LIMIT_LABELS[type] ?? type}${windowSuffix(limit)}`;
174
+ windows.push({ label, percent, reset: resetSuffixFrom(numberOf(limit['nextResetTime'])) });
175
+ }
176
+ if (windows.length === 0)
177
+ return null;
178
+ const worst = windows.reduce((left, right) => (right.percent > left.percent ? right : left));
179
+ const plan = stringOf(data?.['planName'])
180
+ ?? stringOf(data?.['plan'])
181
+ ?? stringOf(data?.['plan_type'])
182
+ ?? stringOf(data?.['level']);
183
+ const lines = windows.map(window => `${window.label} ${Math.round(window.percent)}% used${window.reset}`);
184
+ return {
185
+ text: `GLM ${remainingOf(worst.percent)}%`,
186
+ remaining: remainingOf(worst.percent),
187
+ lines: plan === undefined ? lines : [`Plan ${plan}`, ...lines],
188
+ };
189
+ }
190
+ /** Parse a LiteLLM `/key/info` payload: spend and the key's budget ceiling. */
191
+ function parseLiteLlmKeyInfo(payload) {
192
+ const info = record(record(payload)?.['info']);
193
+ if (info === undefined)
194
+ return null;
195
+ const spend = numberOf(info['spend']);
196
+ const budget = numberOf(info['max_budget']);
197
+ if (spend === undefined && budget === undefined)
198
+ return null;
199
+ const spent = spend ?? 0;
200
+ const remaining = budget === undefined ? undefined : budget - spent;
201
+ const lines = [];
202
+ if (budget !== undefined)
203
+ lines.push(`Budget ${formatMoney(budget, 'USD')}`);
204
+ lines.push(`Spent ${formatMoney(spent, 'USD')}`);
205
+ if (remaining !== undefined)
206
+ lines.push(`Remaining ${formatMoney(remaining, 'USD')}`);
207
+ const reset = stringOf(info['budget_reset_at']);
208
+ if (reset !== undefined)
209
+ lines.push(`Resets ${reset}`);
210
+ return {
211
+ text: remaining === undefined ? `${formatMoney(spent, 'USD')} spent` : formatMoney(remaining, 'USD'),
212
+ lines,
213
+ };
214
+ }
215
+ /** Most meter lines the OmniRoute tooltip lists before it summarizes the rest. */
216
+ const MAX_OMNIROUTE_LINES = 12;
217
+ /** OmniRoute's connection listing, and the usage path one listed id appends to. */
218
+ const OMNIROUTE_CONNECTIONS_PATH = '/api/providers';
219
+ const OMNIROUTE_USAGE_PATH = '/api/usage/';
220
+ /** Window names OmniRoute spells in snake_case, as the tooltip should read them. */
221
+ const OMNIROUTE_WINDOW_LABELS = {
222
+ credits_usd: 'Credits',
223
+ };
224
+ /** A window name as the tooltip spells it, e.g. `Session (5h)`. */
225
+ function omnirouteWindow(window) {
226
+ const named = OMNIROUTE_WINDOW_LABELS[window];
227
+ if (named !== undefined)
228
+ return named;
229
+ const label = window.replaceAll('_', ' ');
230
+ return label.charAt(0).toUpperCase() + label.slice(1);
231
+ }
232
+ /** A reset instant from either an ISO string or an epoch-millisecond number. */
233
+ function resetMsOf(value) {
234
+ return isoToMs(value) ?? numberOf(value);
235
+ }
236
+ /**
237
+ * Parse OmniRoute's per-connection usage answers, one payload per connection
238
+ * it was asked about (a connection that failed arrives `undefined` and is
239
+ * skipped).
240
+ *
241
+ * OmniRoute routes to accounts it holds, so it publishes no figure for the
242
+ * route itself: every upstream connection carries its own plan and its own
243
+ * windows. The reading is therefore the fullest window across them, and every
244
+ * line names the plan it belongs to.
245
+ * @param payloads - one `/api/usage/<id>` body per asked connection.
246
+ * @returns the reading, or `null` when no connection reported a meter.
247
+ */
248
+ function parseOmniRouteUsage(payloads) {
249
+ const lines = [];
250
+ const percents = [];
251
+ let money;
252
+ for (const payload of payloads) {
253
+ const usage = record(payload);
254
+ if (usage === undefined)
255
+ continue;
256
+ const plan = stringOf(usage['plan']) ?? 'OmniRoute';
257
+ const quotas = record(usage['quotas']);
258
+ if (quotas !== undefined) {
259
+ for (const [window, raw] of Object.entries(quotas)) {
260
+ const meter = record(raw);
261
+ if (meter === undefined)
262
+ continue;
263
+ const remaining = numberOf(meter['remaining']);
264
+ const total = numberOf(meter['total']);
265
+ const used = numberOf(meter['used']);
266
+ const left = numberOf(meter['remainingPercentage']);
267
+ // A ceiling is what makes a percentage mean anything: DeepSeek's
268
+ // balance reports `remainingPercentage: 100` over a `total` of zero,
269
+ // which is a wallet, and an `unlimited` meter has no ceiling at all.
270
+ const ceiling = meter['unlimited'] === true ? undefined : total;
271
+ const capped = ceiling !== undefined && ceiling > 0;
272
+ let percent = capped && left !== undefined ? percentOf(100 - left) : undefined;
273
+ if (percent === undefined && capped && used !== undefined) {
274
+ percent = percentOf(100 * used / ceiling);
275
+ }
276
+ const reset = resetSuffixFrom(resetMsOf(meter['resetAt']));
277
+ if (percent === undefined) {
278
+ // A wallet, not a meter: DeepSeek's balance reports a spendable
279
+ // amount with no ceiling, so money is the only honest figure.
280
+ if (remaining === undefined)
281
+ continue;
282
+ const shown = formatMoney(remaining, stringOf(meter['currency']) ?? 'USD');
283
+ money ??= shown;
284
+ lines.push(`${plan} · ${omnirouteWindow(window)} ${shown} left${reset}`);
285
+ continue;
286
+ }
287
+ percents.push(percent);
288
+ lines.push(`${plan} · ${omnirouteWindow(window)} ${String(Math.round(percent))}% used${reset}`);
289
+ }
290
+ }
291
+ if (usage['limitReached'] === true)
292
+ lines.push(`${plan} limit reached`);
293
+ }
294
+ if (lines.length === 0)
295
+ return null;
296
+ const headline = percents.length === 0 ? undefined : Math.max(...percents);
297
+ const shown = lines.slice(0, MAX_OMNIROUTE_LINES);
298
+ if (shown.length < lines.length)
299
+ shown.push(`+${String(lines.length - shown.length)} more`);
300
+ return {
301
+ text: headline === undefined ? money ?? 'OmniRoute' : `OmniRoute ${remainingOf(headline)}%`,
302
+ ...headline === undefined ? {} : { remaining: remainingOf(headline) },
303
+ lines: shown,
304
+ };
305
+ }
306
+ /** GET one JSON body, or `undefined` when the request fails or is refused. */
307
+ async function getJson(url, headers, timeoutMs) {
308
+ try {
309
+ const response = await fetch(url, { headers, signal: AbortSignal.timeout(timeoutMs) });
310
+ return response.ok ? await response.json() : undefined;
311
+ }
312
+ catch {
313
+ return undefined;
314
+ }
315
+ }
316
+ /**
317
+ * OmniRoute quota probe: it publishes per connection, not per route, so the
318
+ * listing is read first and one usage request is built per listed connection.
319
+ * A connection that is switched off or hides its quota is skipped rather than
320
+ * asked, and one that fails simply contributes no line.
321
+ * @param origin - the route's configured origin, without its `/v1` path.
322
+ * @returns the probe.
323
+ */
324
+ function omnirouteProbe(origin) {
325
+ return {
326
+ kind: 'quota',
327
+ envNames: ['OMNIROUTE_API_KEY'],
328
+ requests: async ({ key, timeoutMs }) => {
329
+ const headers = { authorization: `Bearer ${key}`, accept: 'application/json' };
330
+ const listing = await getJson(`${origin}${OMNIROUTE_CONNECTIONS_PATH}`, headers, timeoutMs);
331
+ const connections = record(listing)?.['connections'];
332
+ if (!Array.isArray(connections))
333
+ return [];
334
+ const requests = [];
335
+ for (const entry of connections) {
336
+ const connection = record(entry);
337
+ if (connection === undefined)
338
+ continue;
339
+ if (connection['quotaVisible'] === false || connection['isActive'] === false)
340
+ continue;
341
+ const id = stringOf(connection['id']);
342
+ // `.` and `..` survive `encodeURIComponent`, and the URL parser then
343
+ // climbs out of the usage path into another endpoint, so no request
344
+ // can address them.
345
+ if (id === undefined || id === '.' || id === '..')
346
+ continue;
347
+ requests.push({ url: `${origin}${OMNIROUTE_USAGE_PATH}${encodeURIComponent(id)}`, headers });
348
+ }
349
+ return requests;
350
+ },
351
+ parse: parseOmniRouteUsage,
352
+ };
353
+ }
354
+ /** A percentage clamped to the range a meter can report. */
355
+ function percentOf(value) {
356
+ const parsed = numberOf(value);
357
+ return parsed === undefined ? undefined : Math.min(100, Math.max(0, parsed));
358
+ }
359
+ /** `, resets <local time>` for an epoch-millisecond reset, or nothing. */
360
+ function resetSuffixFrom(ms) {
361
+ if (ms === undefined)
362
+ return '';
363
+ const reset = new Date(ms);
364
+ // An instant outside the Date range is absent data, not the literal
365
+ // `Invalid Date` printed into the tooltip.
366
+ return Number.isNaN(reset.getTime()) ? '' : `, resets ${reset.toLocaleString()}`;
367
+ }
368
+ /** A cents amount as a dollar figure, or nothing. */
369
+ function centsOf(value) {
370
+ const parsed = numberOf(value);
371
+ return parsed === undefined ? undefined : formatMoney(parsed / 100, 'USD');
372
+ }
373
+ /** One plan window as the tooltip spells it. */
374
+ function windowLine(label, percent, resetMs) {
375
+ return `${label} ${String(Math.round(percent))}% used${resetSuffixFrom(resetMs)}`;
376
+ }
377
+ /** The fullest window of a meter set, which is what the chip shows. */
378
+ function fullest(windows) {
379
+ return windows.length === 0 ? undefined : Math.max(...windows.map(window => window.percent));
380
+ }
381
+ /** Parse Claude Code's `/api/oauth/usage` payload: session, weekly, extras. */
382
+ function parseClaudeUsage(payload) {
383
+ const data = record(payload);
384
+ if (data === undefined)
385
+ return null;
386
+ const windows = [];
387
+ let session;
388
+ const limits = data['limits'];
389
+ if (Array.isArray(limits) && limits.length > 0) {
390
+ for (const entry of limits) {
391
+ const limit = record(entry);
392
+ if (limit === undefined)
393
+ continue;
394
+ const percent = percentOf(limit['percent']);
395
+ if (percent === undefined)
396
+ continue;
397
+ const kind = stringOf(limit['kind']) ?? '';
398
+ const group = stringOf(limit['group']) ?? '';
399
+ const resetMs = isoToMs(limit['resets_at']);
400
+ if (kind === 'session' || group === 'session') {
401
+ session = { percent, ...resetMs === undefined ? {} : { resetMs } };
402
+ continue;
403
+ }
404
+ const scope = record(limit['scope']);
405
+ const model = record(scope?.['model']);
406
+ const label = stringOf(scope?.['surface'])
407
+ ?? stringOf(model?.['display_name'])
408
+ ?? 'All models';
409
+ windows.push({ label, percent, ...resetMs === undefined ? {} : { resetMs } });
410
+ }
411
+ }
412
+ else {
413
+ for (const [key, label] of [
414
+ ['seven_day', 'All models'],
415
+ ['seven_day_opus', 'Opus'],
416
+ ['seven_day_sonnet', 'Sonnet'],
417
+ ['seven_day_cowork', 'Cowork'],
418
+ ]) {
419
+ const block = record(data[key]);
420
+ if (block === undefined)
421
+ continue;
422
+ const percent = percentOf(block['utilization']);
423
+ if (percent === undefined)
424
+ continue;
425
+ const resetMs = isoToMs(block['resets_at']);
426
+ windows.push({ label, percent, ...resetMs === undefined ? {} : { resetMs } });
427
+ }
428
+ const five = record(data['five_hour']);
429
+ const percent = percentOf(five?.['utilization']);
430
+ if (percent !== undefined) {
431
+ const resetMs = isoToMs(five?.['resets_at']);
432
+ session = { percent, ...resetMs === undefined ? {} : { resetMs } };
433
+ }
434
+ }
435
+ // The chip answers "how much is left", so the fullest meter names it: a
436
+ // weekly cap at 100% is the whole story even when the 5-hour session is idle.
437
+ const headline = fullest([
438
+ ...session === undefined ? [] : [{ percent: session.percent }],
439
+ ...windows,
440
+ ]);
441
+ if (headline === undefined)
442
+ return null;
443
+ const lines = [];
444
+ if (session !== undefined) {
445
+ lines.push(`Session ${String(Math.round(session.percent))}% used${resetSuffixFrom(session.resetMs)}`);
446
+ }
447
+ for (const window of windows)
448
+ lines.push(windowLine(window.label, window.percent, window.resetMs));
449
+ const extra = record(data['extra_usage']);
450
+ if (extra?.['is_enabled'] === true) {
451
+ const used = centsOf(extra['used_credits']);
452
+ const limit = centsOf(extra['monthly_limit']);
453
+ lines.push(limit === undefined
454
+ ? `Extra usage enabled${used === undefined ? '' : `, ${used} used`}`
455
+ : `Extra usage ${used ?? '$0.00'} of ${limit}`);
456
+ }
457
+ return { text: `Claude ${remainingOf(headline)}%`, remaining: remainingOf(headline), lines };
458
+ }
459
+ /** Codex window label, from the window's own duration. */
460
+ function codexWindowLabel(seconds, name) {
461
+ if (seconds === undefined || seconds === 0)
462
+ return name.replaceAll('_', ' ');
463
+ if (seconds <= 6 * 3_600)
464
+ return 'Current session';
465
+ if (seconds <= 2 * 86_400)
466
+ return `${String(Math.max(1, Math.round(seconds / 3_600)))}-hour`;
467
+ if (seconds >= 6 * 86_400 && seconds <= 8 * 86_400)
468
+ return 'Weekly';
469
+ if (seconds >= 28 * 86_400 && seconds <= 32 * 86_400)
470
+ return 'Monthly';
471
+ return `${String(Math.max(1, Math.round(seconds / 86_400)))}-day`;
472
+ }
473
+ /** One Codex rate-limit window, or `undefined` when it reports no percentage. */
474
+ function codexWindow(block, name) {
475
+ const window = record(block);
476
+ if (window === undefined)
477
+ return undefined;
478
+ const percent = percentOf(window['used_percent']);
479
+ if (percent === undefined)
480
+ return undefined;
481
+ const seconds = numberOf(window['limit_window_seconds']);
482
+ const resetAt = numberOf(window['reset_at']);
483
+ const resetAfter = numberOf(window['reset_after_seconds']);
484
+ const resetMs = resetAt !== undefined
485
+ ? resetAt * 1_000
486
+ : resetAfter === undefined ? undefined : Date.now() + resetAfter * 1_000;
487
+ return {
488
+ label: codexWindowLabel(seconds, name),
489
+ percent,
490
+ ...resetMs === undefined ? {} : { resetMs },
491
+ };
492
+ }
493
+ /** Parse Codex's `/backend-api/wham/usage` payload: plan, windows, credits. */
494
+ function parseCodexUsage(payload) {
495
+ const data = record(payload);
496
+ if (data === undefined)
497
+ return null;
498
+ const plan = stringOf(data['plan_type'])?.replaceAll('_', ' ') ?? 'Codex';
499
+ const rate = record(data['rate_limit']) ?? {};
500
+ const windows = [];
501
+ for (const key of ['primary_window', 'secondary_window']) {
502
+ const window = codexWindow(rate[key], key);
503
+ if (window !== undefined)
504
+ windows.push(window);
505
+ }
506
+ const review = record(data['code_review_rate_limit']);
507
+ if (review !== undefined) {
508
+ if (review['primary_window'] !== undefined || review['secondary_window'] !== undefined) {
509
+ for (const key of ['primary_window', 'secondary_window']) {
510
+ const window = codexWindow(review[key], `code_review_${key}`);
511
+ if (window !== undefined)
512
+ windows.push({ ...window, label: `Code review · ${window.label}` });
513
+ }
514
+ }
515
+ else {
516
+ const window = codexWindow(review, 'code_review');
517
+ if (window !== undefined)
518
+ windows.push({ label: 'Code review', percent: window.percent, ...window.resetMs === undefined ? {} : { resetMs: window.resetMs } });
519
+ }
520
+ }
521
+ const credits = record(data['credits']) ?? {};
522
+ const balance = numberOf(credits['balance']);
523
+ const hasCredits = credits['has_credits'] === true && balance !== undefined;
524
+ const headline = fullest(windows);
525
+ if (headline === undefined && !hasCredits)
526
+ return null;
527
+ const lines = [`Plan ${plan.replace(/\b\w/g, letter => letter.toUpperCase())}`];
528
+ for (const window of windows)
529
+ lines.push(windowLine(window.label, window.percent, window.resetMs));
530
+ if (hasCredits)
531
+ lines.push(`Credits ${String(balance)}`);
532
+ if (rate['limit_reached'] === true)
533
+ lines.push('Limit reached');
534
+ return {
535
+ text: headline === undefined ? `${String(balance)} credits` : `Codex ${remainingOf(headline)}%`,
536
+ ...headline === undefined ? {} : { remaining: remainingOf(headline) },
537
+ lines,
538
+ };
539
+ }
540
+ /** One Grok billing config as a period, or `undefined` when it reports none. */
541
+ function grokPeriod(payload) {
542
+ const body = record(payload);
543
+ if (body === undefined)
544
+ return undefined;
545
+ const config = record(body['config']) ?? body;
546
+ const period = record(config['currentPeriod']) ?? {};
547
+ const type = stringOf(period['type']) ?? '';
548
+ let label = type.includes('WEEKLY') ? 'Weekly' : type.includes('MONTHLY') ? 'Monthly' : 'Usage';
549
+ const resetMs = isoToMs(period['end']) ?? isoToMs(config['billingPeriodEnd']) ?? isoToMs(config['billing_period_end']);
550
+ const creditPercent = numberOf(config['creditUsagePercent']);
551
+ const creditsShaped = config['creditUsagePercent'] !== undefined
552
+ || config['currentPeriod'] !== undefined
553
+ || config['isUnifiedBillingUser'] === true;
554
+ if (creditsShaped) {
555
+ // A credits-shaped payload with no readable percentage carries no figure:
556
+ // synthesizing zero would report a full allowance that was never measured.
557
+ return {
558
+ label,
559
+ ...creditPercent === undefined ? {} : { percent: percentOf(creditPercent) },
560
+ ...resetMs === undefined ? {} : { resetMs },
561
+ };
562
+ }
563
+ const used = numberOf(config['used']);
564
+ const limit = numberOf(config['monthlyLimit']) ?? numberOf(config['monthly_limit']);
565
+ const percent = used !== undefined && limit !== undefined && limit !== 0 ? percentOf(100 * used / limit) : undefined;
566
+ if (label === 'Usage')
567
+ label = 'Monthly';
568
+ return {
569
+ label,
570
+ ...percent === undefined ? {} : { percent },
571
+ ...used === undefined ? {} : { used },
572
+ ...limit === undefined ? {} : { limit },
573
+ ...resetMs === undefined ? {} : { resetMs },
574
+ };
575
+ }
576
+ /** Parse Grok's two billing payloads: weekly credits, then monthly spend. */
577
+ function parseGrokBilling(payloads) {
578
+ const seen = new Set();
579
+ const periods = [];
580
+ for (const payload of payloads) {
581
+ const period = grokPeriod(payload);
582
+ if (period === undefined)
583
+ continue;
584
+ // A meter with no figure at all is not a meter: it would only add a row
585
+ // that says nothing.
586
+ if (period.percent === undefined && period.used === undefined)
587
+ continue;
588
+ const key = `${period.label}/${String(period.resetMs ?? '')}`;
589
+ if (seen.has(key))
590
+ continue;
591
+ seen.add(key);
592
+ const money = period.used === undefined
593
+ ? undefined
594
+ : `${centsOf(period.used) ?? ''}${period.limit === undefined ? '' : ` of ${centsOf(period.limit) ?? ''}`}`;
595
+ // A spend meter reads better as money; a credit meter only has a percentage.
596
+ const line = money !== undefined
597
+ ? `${period.label} ${money}${resetSuffixFrom(period.resetMs)}`
598
+ : period.percent === undefined
599
+ ? `${period.label} usage${resetSuffixFrom(period.resetMs)}`
600
+ : windowLine(period.label, period.percent, period.resetMs);
601
+ periods.push({
602
+ label: period.label,
603
+ line,
604
+ ...period.percent === undefined ? {} : { percent: period.percent },
605
+ });
606
+ }
607
+ if (periods.length === 0)
608
+ return null;
609
+ const percentages = periods.filter(period => period.percent !== undefined);
610
+ const headline = fullest(percentages);
611
+ return {
612
+ text: headline === undefined ? 'Grok usage' : `Grok ${remainingOf(headline)}%`,
613
+ ...headline === undefined ? {} : { remaining: remainingOf(headline) },
614
+ lines: periods.map(period => period.line),
615
+ };
616
+ }
617
+ /** Cursor's plan name, from the membership string the API reports. */
618
+ function cursorPlanLabel(membership) {
619
+ const key = (membership ?? '').trim().toLowerCase().replaceAll('-', '_').replaceAll(' ', '_');
620
+ const names = {
621
+ free: 'Free',
622
+ hobby: 'Hobby',
623
+ pro: 'Pro',
624
+ pro_plus: 'Pro+',
625
+ proplus: 'Pro+',
626
+ ultra: 'Ultra',
627
+ business: 'Business',
628
+ team: 'Team',
629
+ teams: 'Team',
630
+ enterprise: 'Enterprise',
631
+ };
632
+ return names[key] ?? (key === '' ? 'Cursor' : key.replaceAll('_', ' ').replace(/\b\w/g, letter => letter.toUpperCase()));
633
+ }
634
+ /** One Cursor meter as a window, or `undefined` when it carries no figure. */
635
+ function cursorMeter(block, label, resetMs) {
636
+ const meter = record(block);
637
+ if (meter === undefined || meter['enabled'] === false)
638
+ return undefined;
639
+ const used = numberOf(meter['used']);
640
+ const limit = numberOf(meter['limit']);
641
+ let percent = percentOf(meter['totalPercentUsed']);
642
+ if (percent === undefined && used !== undefined && limit !== undefined && limit !== 0) {
643
+ percent = percentOf(100 * used / limit);
644
+ }
645
+ if (percent === undefined && used === undefined && limit === undefined)
646
+ return undefined;
647
+ const detail = used === undefined
648
+ ? undefined
649
+ : `${String(used)}${limit === undefined ? '' : ` of ${String(limit)}`}`;
650
+ return {
651
+ label,
652
+ ...percent === undefined ? {} : { percent },
653
+ ...resetMs === undefined ? {} : { resetMs },
654
+ line: percent === undefined
655
+ ? `${label} ${detail ?? 'usage'}${resetSuffixFrom(resetMs)}`
656
+ : windowLine(label, percent, resetMs),
657
+ };
658
+ }
659
+ /** Parse Cursor's `/api/usage-summary` payload: plan, included, on-demand. */
660
+ function parseCursorSummary(payload) {
661
+ const data = record(payload);
662
+ if (data === undefined)
663
+ return null;
664
+ const plan = cursorPlanLabel(stringOf(data['membershipType']));
665
+ const cycleEnd = isoToMs(data['billingCycleEnd']);
666
+ const meters = [];
667
+ const individual = record(data['individualUsage']) ?? {};
668
+ if (data['isUnlimited'] === true) {
669
+ return { text: 'Cursor unlimited', lines: [`Plan ${plan}`] };
670
+ }
671
+ const included = cursorMeter(individual['plan'], 'Included', cycleEnd)
672
+ ?? cursorMeter(individual['overall'], 'Included', cycleEnd);
673
+ if (included !== undefined)
674
+ meters.push(included);
675
+ const onDemand = cursorMeter(individual['onDemand'], 'On-demand', cycleEnd);
676
+ if (onDemand !== undefined)
677
+ meters.push(onDemand);
678
+ if (meters.length === 0)
679
+ return null;
680
+ const percentages = meters.filter(meter => meter.percent !== undefined);
681
+ const headline = fullest(percentages);
682
+ return {
683
+ text: headline === undefined ? 'Cursor usage' : `Cursor ${remainingOf(headline)}%`,
684
+ ...headline === undefined ? {} : { remaining: remainingOf(headline) },
685
+ lines: [`Plan ${plan}`, ...meters.map(meter => meter.line)],
686
+ };
687
+ }
688
+ /** DeepSeek balance probe, against the configured origin or the public API. */
689
+ function deepSeekProbe(origin) {
690
+ return {
691
+ kind: 'balance',
692
+ url: `${origin}/user/balance`,
693
+ envNames: ['DEEPSEEK_API_KEY'],
694
+ parse: payloads => parseDeepSeekBalance(payloads[0]),
695
+ };
696
+ }
697
+ /** OpenRouter credits probe. */
698
+ function openRouterProbe(origin) {
699
+ return {
700
+ kind: 'balance',
701
+ url: `${origin}/api/v1/credits`,
702
+ envNames: ['OPENROUTER_API_KEY'],
703
+ parse: payloads => parseOpenRouterCredits(payloads[0]),
704
+ };
705
+ }
706
+ /** z.ai / BigModel Coding Plan quota probe. */
707
+ function zaiProbe(origin) {
708
+ return {
709
+ kind: 'quota',
710
+ url: `${origin}/api/monitor/usage/quota/limit`,
711
+ envNames: ['ZAI_API_KEY', 'Z_AI_API_KEY', 'BIGMODEL_API_KEY', 'ZHIPU_API_KEY'],
712
+ parse: payloads => parseZaiQuota(payloads[0]),
713
+ };
714
+ }
715
+ /** LiteLLM key budget probe: the fallback shape for a proxy deployment. */
716
+ function liteLlmProbe(origin, tentative = false) {
717
+ return {
718
+ kind: 'balance',
719
+ tentative,
720
+ url: `${origin}/key/info`,
721
+ envNames: [],
722
+ parse: payloads => parseLiteLlmKeyInfo(payloads[0]),
723
+ };
724
+ }
725
+ /** Claude Code subscription probe: the plan's own usage meter. */
726
+ function claudeProbe() {
727
+ return { kind: 'quota', local: 'claude', parse: payloads => parseClaudeUsage(payloads[0]) };
728
+ }
729
+ /** Codex (ChatGPT) subscription probe. */
730
+ function codexProbe() {
731
+ return { kind: 'quota', local: 'codex', parse: payloads => parseCodexUsage(payloads[0]) };
732
+ }
733
+ /** Grok subscription probe: two billing meters in one reading. */
734
+ function grokProbe() {
735
+ return { kind: 'quota', local: 'grok', parse: parseGrokBilling };
736
+ }
737
+ /** Cursor subscription probe. */
738
+ function cursorProbe() {
739
+ return { kind: 'quota', local: 'cursor', parse: payloads => parseCursorSummary(payloads[0]) };
740
+ }
741
+ /** OpenCode Go subscription usage, authenticated with the route's API key. */
742
+ function opencodeGoProbe() {
743
+ return {
744
+ kind: 'quota',
745
+ url: 'https://opencode.ai/zen/go/v1/usage',
746
+ envNames: ['OPENCODE_GO_API_KEY', 'OPENCODE_API_KEY'],
747
+ parse: payloads => {
748
+ const usage = record(record(payloads[0])?.['usage']);
749
+ const windows = [];
750
+ for (const [key, label] of [['rolling', 'Session (5h)'], ['weekly', 'Weekly'], ['monthly', 'Monthly']]) {
751
+ const meter = record(usage?.[key]);
752
+ const percent = percentOf(meter?.['percent']);
753
+ if (percent === undefined)
754
+ continue;
755
+ windows.push({ percent, line: windowLine(label, percent, isoToMs(meter?.['resetsAt'])) });
756
+ }
757
+ const headline = fullest(windows);
758
+ if (headline === undefined)
759
+ return null;
760
+ return {
761
+ text: `Go ${remainingOf(headline)}%`,
762
+ remaining: remainingOf(headline),
763
+ lines: ['OpenCode Go', ...windows.map(window => window.line)],
764
+ };
765
+ },
766
+ };
767
+ }
768
+ /**
769
+ * Whether a route id names a vendor whose own endpoint this plugin knows, so a
770
+ * route that matches no host must not be handed to the aggregator fallback.
771
+ * @param id - lowercased provider route id.
772
+ * @returns true when the id names a known vendor.
773
+ */
774
+ function namesVendor(id) {
775
+ return id.startsWith('deepseek') || id.includes('openrouter') || id.includes('litellm')
776
+ || id.includes('zai') || id.includes('zhipu') || id.includes('bigmodel') || id.includes('glm')
777
+ || id.includes('claude') || id.includes('anthropic') || id.includes('codex')
778
+ || id.includes('chatgpt') || id.includes('grok') || id.includes('xai') || id.includes('cursor')
779
+ || id.includes('opencode');
780
+ }
781
+ /**
782
+ * Resolve the probe answering for one provider route.
783
+ *
784
+ * A configured base URL decides alone when it names a known host, because the
785
+ * host is the endpoint that actually answers. The provider id only decides
786
+ * when no base URL is configured (the route relies on its library's own
787
+ * default), so a route named after a provider but pointed somewhere else (a
788
+ * local vLLM serving DeepSeek weights, or Vertex-hosted Claude, say) is never
789
+ * read from that provider's own subscription credential.
790
+ *
791
+ * The subscription probes come first among the id-based rules because their
792
+ * host is the vendor itself, not a reseller: a route called `anthropic` with no
793
+ * base URL is Claude Code's plan meter, while `google-vertex-anthropic` carries
794
+ * a googleapis.com host and resolves nothing.
795
+ *
796
+ * A route that names no vendor and still has a base URL gets the LiteLLM
797
+ * key-budget probe, which is how a proxy deployment is recognized at all.
798
+ * @param providerId - route id as the model picker names it, e.g. `deepseek-official`.
799
+ * @param baseURL - the route's configured base URL, when it has one.
800
+ * @returns the probe, or `undefined` when no balance or quota route is known.
801
+ */
802
+ export function resolveProbe(providerId, baseURL) {
803
+ const id = providerId.toLowerCase();
804
+ const host = hostOf(baseURL);
805
+ const unset = host === '';
806
+ if (host === 'opencode.ai') {
807
+ return /^\/zen\/go(?:\/|$)/.test(new URL(baseURL).pathname) ? opencodeGoProbe() : undefined;
808
+ }
809
+ if (unset && id.includes('opencode-go'))
810
+ return opencodeGoProbe();
811
+ if (hostIs(host, 'api.anthropic.com') || (unset && (id.includes('claude') || id.includes('anthropic')))) {
812
+ return claudeProbe();
813
+ }
814
+ if (hostIs(host, 'chatgpt.com') || (unset && (id.includes('codex') || id.includes('chatgpt')))) {
815
+ return codexProbe();
816
+ }
817
+ if (hostIs(host, 'cli-chat-proxy.grok.com') || hostIs(host, 'api.x.ai')
818
+ || (unset && (id.includes('grok') || id.includes('xai')))) {
819
+ return grokProbe();
820
+ }
821
+ if (hostIs(host, 'cursor.com') || (unset && id.includes('cursor')))
822
+ return cursorProbe();
823
+ if (hostIs(host, 'api.deepseek.com') || (unset && id.startsWith('deepseek'))) {
824
+ return deepSeekProbe(originOf(baseURL, 'https://api.deepseek.com'));
825
+ }
826
+ if (hostIs(host, 'openrouter.ai') || (unset && id.includes('openrouter'))) {
827
+ return openRouterProbe(originOf(baseURL, 'https://openrouter.ai'));
828
+ }
829
+ const bigmodel = hostIs(host, 'open.bigmodel.cn');
830
+ if (bigmodel || hostIs(host, 'api.z.ai') || (unset && ZAI_IDS.has(id))) {
831
+ return zaiProbe(originOf(baseURL, bigmodel ? 'https://open.bigmodel.cn' : 'https://api.z.ai'));
832
+ }
833
+ if (id.includes('litellm'))
834
+ return liteLlmProbe(originOf(baseURL, 'http://localhost:4000'));
835
+ // OmniRoute fronts the accounts it holds, so its own API, not the vendor's,
836
+ // is where a figure lives, and it answers per upstream connection. The route
837
+ // is recognised by its id because the host is whatever machine runs it.
838
+ if (id.includes('omniroute'))
839
+ return omnirouteProbe(originOf(baseURL, 'http://localhost:20128'));
840
+ // An aggregator route: named after a model, a plan, or the proxy itself. The
841
+ // one per-route figure such a host may carry is LiteLLM's key budget, and a
842
+ // host without that route answers 404, which renders as no chip. The probe is
843
+ // tentative: a route that only guesses LiteLLM must stay silent when the host
844
+ // turns out to be something else.
845
+ if (baseURL === undefined || unset || namesVendor(id))
846
+ return undefined;
847
+ return liteLlmProbe(originOf(baseURL, baseURL), true);
848
+ }