maxpool 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/prober.js +9 -0
- package/src/tui.js +151 -36
package/package.json
CHANGED
package/src/prober.js
CHANGED
|
@@ -68,6 +68,11 @@ export class Prober {
|
|
|
68
68
|
if (this._running) return this._inflight || Promise.resolve();
|
|
69
69
|
this._running = true;
|
|
70
70
|
this._stopping = false;
|
|
71
|
+
// Publish the sweep's liveness so the UI can answer "what happens next?"
|
|
72
|
+
// rather than printing a bare "stale". Reported 2026-08-27: a stale marker with
|
|
73
|
+
// no next step reads as a problem the user must fix, when the prober is already
|
|
74
|
+
// retrying on its own.
|
|
75
|
+
this.am.quotaProbeSweeping = true;
|
|
71
76
|
this._inflight = (async () => {
|
|
72
77
|
try {
|
|
73
78
|
// CAPACITY LEDGER: close any open cycle whose window's reset stamp has
|
|
@@ -110,6 +115,10 @@ export class Prober {
|
|
|
110
115
|
} finally {
|
|
111
116
|
this._running = false;
|
|
112
117
|
this._inflight = null;
|
|
118
|
+
this.am.quotaProbeSweeping = false;
|
|
119
|
+
// When the NEXT sweep starts. setInterval fires every intervalMs from the
|
|
120
|
+
// last tick, so "now + interval" is the honest estimate for the UI.
|
|
121
|
+
this.am.quotaProbeNextSweepAt = this.intervalMs > 0 ? Date.now() + this.intervalMs : null;
|
|
113
122
|
}
|
|
114
123
|
})();
|
|
115
124
|
return this._inflight;
|
package/src/tui.js
CHANGED
|
@@ -139,6 +139,18 @@ function formatMs(ms) {
|
|
|
139
139
|
return `${min}m${String(rem).padStart(2, '0')}s`;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/** Coarse human duration for AGE and NEXT-REFRESH text: "45s", "12m", "2h".
|
|
143
|
+
* Deliberately not formatMs — "quota 12m03s old" spends precision on a number
|
|
144
|
+
* nobody reads to the second, and the extra digits are what made the old cell
|
|
145
|
+
* look like machine output. */
|
|
146
|
+
function formatAge(ms) {
|
|
147
|
+
if (ms == null || isNaN(ms) || ms < 0) return '';
|
|
148
|
+
if (ms < 60_000) return `${Math.max(1, Math.round(ms / 1000))}s`;
|
|
149
|
+
if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`;
|
|
150
|
+
if (ms < 86_400_000) return `${Math.round(ms / 3_600_000)}h`;
|
|
151
|
+
return `${Math.round(ms / 86_400_000)}d`;
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
function statusColor(status) {
|
|
143
155
|
if (status == null) return '-';
|
|
144
156
|
if (status >= 200 && status < 300) return green(String(status));
|
|
@@ -162,22 +174,69 @@ function countdown(ts) {
|
|
|
162
174
|
return `${Math.ceil(ms / 86_400_000)}d`;
|
|
163
175
|
}
|
|
164
176
|
|
|
177
|
+
/** ACTIVITY cell — "is this account working, and is it healthy?" in plain words.
|
|
178
|
+
* Rewritten twice on owner feedback (2026-08-27): first to symbols ("▶2 · 17/h ·
|
|
179
|
+
* 8.5s"), which read as secret code; now to words. Average latency is GONE —
|
|
180
|
+
* "how long each request takes" is not something the owner acts on. An idle
|
|
181
|
+
* account renders NOTHING.
|
|
182
|
+
*
|
|
183
|
+
* 2 live · 17 req/hr · 1 failed
|
|
184
|
+
* 17 req/hr working, idle this second
|
|
185
|
+
* (blank) resting — nothing to say
|
|
186
|
+
*/
|
|
165
187
|
function loadText(load) {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
188
|
+
const inflight = load?.current?.inFlight || 0;
|
|
189
|
+
const hourReq = load?.last1h?.requests || 0;
|
|
190
|
+
const failed = load?.last15m?.failed || 0;
|
|
191
|
+
if (!inflight && !hourReq && !failed) return '';
|
|
192
|
+
const parts = [];
|
|
193
|
+
if (inflight) parts.push(`${inflight} live`);
|
|
194
|
+
if (hourReq) parts.push(`${hourReq} req/hr`);
|
|
195
|
+
if (failed) parts.push(red(`${failed} failed`));
|
|
196
|
+
return parts.join(' · ');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The usage CAP is an account PROPERTY, not a state — so it renders whenever one
|
|
200
|
+
* is set, on every account type, whatever the account is doing. It previously
|
|
201
|
+
* lived inline in the provider branch only, so the OAuth account the feature was
|
|
202
|
+
* built for (max@gomokka.com) showed no cap anywhere: reported 2026-08-27, "I need
|
|
203
|
+
* to be able to see whether an account has a cap or not." Yellow while the cap is
|
|
204
|
+
* actively holding traffic back, dim otherwise. */
|
|
205
|
+
function capText(a, benched) {
|
|
206
|
+
if (a?.capUtilization == null) return '';
|
|
207
|
+
const t = `cap ${Math.round(a.capUtilization * 100)}%`;
|
|
208
|
+
return benched ? yellow(t) : dim(t);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** PER-ACCOUNT SETTINGS the user set by hand — the last column's whole job
|
|
212
|
+
* (owner, 2026-08-27: "the last column should contain any and all settings that
|
|
213
|
+
* are custom per account"). Fleet-wide settings (routing mode, peak policy) stay
|
|
214
|
+
* in the top header where they already live; only what is scoped to THIS account
|
|
215
|
+
* belongs on THIS row.
|
|
216
|
+
* preferred this account is the manual routing pin (the 'p' key)
|
|
217
|
+
* cap NN% its reserved-capacity ceiling (the 'c' key)
|
|
218
|
+
* Deliberately NOT here: automatic routing policies (peak, fast-refill) — those
|
|
219
|
+
* are the system's behaviour, not the user's settings; they keep their own tags.
|
|
220
|
+
*/
|
|
221
|
+
function settingsTags(am, a) {
|
|
222
|
+
const tags = [];
|
|
223
|
+
if (am?.routingMode === 'preferred' && a?.name === am.preferredAccountName) {
|
|
224
|
+
tags.push(cyan('preferred'));
|
|
225
|
+
}
|
|
226
|
+
const capTag = capText(a, capBenched(am, a));
|
|
227
|
+
if (capTag) tags.push(capTag);
|
|
228
|
+
return tags;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** True when the reservation is what is currently keeping traffic off this account
|
|
232
|
+
* — either window at or past the cap. Reads the manager's own predicate so the
|
|
233
|
+
* label can never disagree with routing. */
|
|
234
|
+
function capBenched(am, a) {
|
|
235
|
+
if (a?.capUtilization == null) return false;
|
|
236
|
+
const q = a.quota || {};
|
|
237
|
+
const ses = a.type === 'provider' ? q.providerSes : q.unified5h;
|
|
238
|
+
const wk = a.type === 'provider' ? q.providerWk : q.unified7d;
|
|
239
|
+
return !!(am?._capped?.(a, ses) || am?._capped?.(a, wk));
|
|
181
240
|
}
|
|
182
241
|
|
|
183
242
|
export function weeklyPolicyText(am, account) {
|
|
@@ -190,6 +249,14 @@ export function weeklyPolicyText(am, account) {
|
|
|
190
249
|
return yellow(`Cap ${Math.round((account.capUtilization || 0) * 100)}%`);
|
|
191
250
|
}
|
|
192
251
|
if (!state || state === 'unknown' || state === 'normal') return '';
|
|
252
|
+
// SAY IT ONCE (2026-08-27). An account Anthropic is rejecting outright already
|
|
253
|
+
// says so twice on this row: the Wk bar reads 100%, and the Status column reads
|
|
254
|
+
// "exhausted". A third "Wk exhausted 100%" tag was pure repetition, and it pushed
|
|
255
|
+
// the genuinely-informative tags (Cap, a per-model sub-limit) off to the right.
|
|
256
|
+
// Dropped ONLY when the Status column carries the same fact — a soft/reserve/
|
|
257
|
+
// critical row, or an exhausted-by-threshold row the status column shows as
|
|
258
|
+
// "active", still needs the tag.
|
|
259
|
+
if (am._isAccountWideRejected?.(account)) return '';
|
|
193
260
|
const rawState = am._weeklyRawState?.(account) || state;
|
|
194
261
|
const used = Number(account.quota?.unified7d);
|
|
195
262
|
const pct = Number.isFinite(used)
|
|
@@ -1735,11 +1802,11 @@ export class TUI {
|
|
|
1735
1802
|
if (hidden > 0) {
|
|
1736
1803
|
lines.push(` ${dim(`… ${hidden} disabled account${hidden === 1 ? '' : 's'} hidden — press h to show`)}`);
|
|
1737
1804
|
}
|
|
1738
|
-
//
|
|
1739
|
-
//
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1805
|
+
// The glossary FOOTER is gone (owner, 2026-08-27). A legend is a symptom: it
|
|
1806
|
+
// exists to decode a row that could not be read on its own. Every cell now
|
|
1807
|
+
// says what it means in words ("2 live · 17 req/hr", "quota 12m old ·
|
|
1808
|
+
// refreshing in 45s"), so there is nothing left to decode — and one fewer
|
|
1809
|
+
// line of chrome between the operator and the data.
|
|
1743
1810
|
}
|
|
1744
1811
|
|
|
1745
1812
|
// ── Activity header
|
|
@@ -1859,13 +1926,16 @@ export class TUI {
|
|
|
1859
1926
|
if (a.enabled !== false && upstreamBlocking && a.status === 'active') {
|
|
1860
1927
|
effectiveStatus = a.inFlight > 0 ? 'probing' : 'waiting';
|
|
1861
1928
|
}
|
|
1862
|
-
//
|
|
1863
|
-
//
|
|
1864
|
-
//
|
|
1865
|
-
//
|
|
1866
|
-
//
|
|
1929
|
+
// Anthropic is rejecting the WHOLE account (a 'rejected' unified status
|
|
1930
|
+
// corroborated by an exhausted unified bucket). A per-model cap (Fable) is NOT
|
|
1931
|
+
// account-wide — it shows as the separate "… maxed" tag, leaving the status
|
|
1932
|
+
// column truthful. Keys on a.status (not effectiveStatus) so a genuine block
|
|
1933
|
+
// wins even inside an upstream-throttle window.
|
|
1934
|
+
// Named "exhausted", not "blocked" (2026-08-27): the row's own quota bar and
|
|
1935
|
+
// every other surface call this state exhausted, and two words for one state
|
|
1936
|
+
// read as two different problems.
|
|
1867
1937
|
if (a.enabled !== false && a.status === 'active' && this.am._isAccountWideRejected?.(a)) {
|
|
1868
|
-
effectiveStatus = '
|
|
1938
|
+
effectiveStatus = 'exhausted';
|
|
1869
1939
|
}
|
|
1870
1940
|
// A dead refresh token surfaces as "reauth" (re-login needed) rather than a
|
|
1871
1941
|
// generic "error", so the user knows the fix. Display-only — account.status
|
|
@@ -1879,7 +1949,7 @@ export class TUI {
|
|
|
1879
1949
|
switch (effectiveStatus) {
|
|
1880
1950
|
case 'active': status = isCur ? green('active') : 'active'; break;
|
|
1881
1951
|
case 'reauth': status = yellow('reauth'); break;
|
|
1882
|
-
case 'blocked': status = red('
|
|
1952
|
+
case 'blocked': status = red('exhausted'); break;
|
|
1883
1953
|
case 'probing': status = green('probing'); break;
|
|
1884
1954
|
case 'waiting': status = yellow('waiting'); break;
|
|
1885
1955
|
case 'paused': status = yellow('paused'); break;
|
|
@@ -1936,6 +2006,15 @@ export class TUI {
|
|
|
1936
2006
|
}
|
|
1937
2007
|
const weekly = weeklyPolicyText(this.am, a);
|
|
1938
2008
|
if (weekly) line += ` ${weekly}`;
|
|
2009
|
+
// Per-account SETTINGS (preferred pin, usage cap), whatever the account is
|
|
2010
|
+
// doing. weeklyPolicyText renders "Cap 50%" only while the cap is the ACTIVE
|
|
2011
|
+
// weekly state; these are the standing properties, so a capped account that is
|
|
2012
|
+
// exhausted, throttled or idle still says so. The cap is suppressed when the
|
|
2013
|
+
// weekly tag already IS the Cap label (no double tag).
|
|
2014
|
+
for (const tag of settingsTags(this.am, a)) {
|
|
2015
|
+
if (weekly.includes('Cap ') && tag.includes('cap ')) continue;
|
|
2016
|
+
line += ` ${tag}`;
|
|
2017
|
+
}
|
|
1939
2018
|
// Per-model weekly caps (e.g. Fable, while the unified weekly still has
|
|
1940
2019
|
// headroom). Show the ACTUAL utilization — "Fable 90%" (yellow) while high but
|
|
1941
2020
|
// still usable, "Fable maxed" (red) ONLY at genuine exhaustion. This is the
|
|
@@ -1958,7 +2037,8 @@ export class TUI {
|
|
|
1958
2037
|
// cause (e.g. rate-limited) when a probe failure is on record — rather than imply
|
|
1959
2038
|
// the last-known value is current.
|
|
1960
2039
|
line += this._probeHealthNote(a);
|
|
1961
|
-
|
|
2040
|
+
const act = loadText(this._accountLoad(a));
|
|
2041
|
+
if (act) line += ` ${dim(act)}`;
|
|
1962
2042
|
return line;
|
|
1963
2043
|
}
|
|
1964
2044
|
|
|
@@ -1990,10 +2070,35 @@ export class TUI {
|
|
|
1990
2070
|
const headerFresh = q.lastHeaderQuotaAt && (Date.now() - q.lastHeaderQuotaAt) <= Math.max(3 * interval, 180_000);
|
|
1991
2071
|
if (headerFresh) return '';
|
|
1992
2072
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
2073
|
+
// SAY WHAT HAPPENS NEXT (owner, 2026-08-27): "stale" / "stale·probe throttled"
|
|
2074
|
+
// named an internal mechanism and left the user with nothing to do. Nothing IS
|
|
2075
|
+
// the correct action — the prober retries on its own schedule and backs off
|
|
2076
|
+
// automatically when Anthropic rate-limits it — so the cell now states how old
|
|
2077
|
+
// the numbers are AND when they refresh, in that order.
|
|
2078
|
+
const age = q.lastProbeOkAt ? formatAge(Date.now() - q.lastProbeOkAt) : null;
|
|
2079
|
+
const ageText = age ? `quota ${age} old` : 'quota not read yet';
|
|
2080
|
+
const next = this._probeNextText();
|
|
2081
|
+
const throttled = q.lastProbeErrorStatus === 429;
|
|
2082
|
+
// Throttled is the ONE case worth colouring: it is why the refresh is late, and
|
|
2083
|
+
// it self-clears. Everything else is a plain dim statement of fact.
|
|
2084
|
+
const body = throttled
|
|
2085
|
+
? `${ageText} · rate-limited, retrying ${next}`
|
|
2086
|
+
: `${ageText} · refreshing ${next}`;
|
|
2087
|
+
return ` ${throttled ? yellow(body) : dim(body)}`;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/** "now" while a sweep is in flight, else "in 45s" from the prober's next-tick
|
|
2091
|
+
* stamp. Falls back to the configured interval when no sweep has completed yet
|
|
2092
|
+
* (fresh boot), and to a bare "shortly" when the probe is manual/off. */
|
|
2093
|
+
_probeNextText() {
|
|
2094
|
+
if (this.am.quotaProbeSweeping) return 'now';
|
|
2095
|
+
const at = this.am.quotaProbeNextSweepAt;
|
|
2096
|
+
if (at) {
|
|
2097
|
+
const ms = at - Date.now();
|
|
2098
|
+
return ms > 0 ? `in ${formatAge(ms)}` : 'now';
|
|
2099
|
+
}
|
|
2100
|
+
const interval = this.am.quotaProbeIntervalMs;
|
|
2101
|
+
return interval > 0 ? `every ${formatAge(interval)}` : 'shortly';
|
|
1997
2102
|
}
|
|
1998
2103
|
|
|
1999
2104
|
_renderProviderAcct(sel, cur, name, type, status, a, bw = 11, showBoth = true) {
|
|
@@ -2031,7 +2136,6 @@ export class TUI {
|
|
|
2031
2136
|
const m = this.am._fastRefillMultiplier(a);
|
|
2032
2137
|
if (m < 1) note += ` ${cyan(`fast·refill ×${m.toFixed(2)}`)}`;
|
|
2033
2138
|
}
|
|
2034
|
-
if (a.capUtilization != null) note += ` ${yellow(`cap ${Math.round(a.capUtilization * 100)}%`)}`;
|
|
2035
2139
|
} else if (q.providerQuotaSource === 'console-only') {
|
|
2036
2140
|
sesCell = emptyBar('n/a', bw);
|
|
2037
2141
|
wkCell = emptyBar('n/a', bw);
|
|
@@ -2040,15 +2144,26 @@ export class TUI {
|
|
|
2040
2144
|
sesCell = emptyBar('probing', bw);
|
|
2041
2145
|
wkCell = emptyBar('probing', bw);
|
|
2042
2146
|
}
|
|
2147
|
+
// Settings ride OUTSIDE the quota-readable branch: an account with an
|
|
2148
|
+
// unreadable or not-yet-probed quota still HAS its reservation, and hiding the
|
|
2149
|
+
// setting whenever the probe is quiet is how a shipped feature reads as absent.
|
|
2150
|
+
for (const tag of settingsTags(this.am, a)) note += ` ${tag}`;
|
|
2043
2151
|
|
|
2044
2152
|
let line = ` ${sel}${cur} ${name} ${type} ${status} Ses ${sesCell}`;
|
|
2045
2153
|
if (showBoth) line += ` Wk ${wkCell}`;
|
|
2046
2154
|
line += note;
|
|
2047
2155
|
// Same recent-load columns as OAuth (providers track inFlight + load events),
|
|
2048
2156
|
// plus a compact Last <status> <ms> — the one signal that matters for a
|
|
2049
|
-
// rarely-hit fallback
|
|
2050
|
-
|
|
2051
|
-
if (
|
|
2157
|
+
// rarely-hit fallback. loadText renders empty when fully idle.
|
|
2158
|
+
const act = loadText(this._accountLoad(a));
|
|
2159
|
+
if (act) line += ` ${dim(act)}`;
|
|
2160
|
+
// "Last 200 6.4s" next to a live "17/h · 6.4s" said the same thing twice. Keep it
|
|
2161
|
+
// for the two cases where it is the ONLY thing that speaks: a non-2xx last result
|
|
2162
|
+
// (always worth seeing), or an idle row whose activity cell is blank.
|
|
2163
|
+
const lastOk = a.lastStatus >= 200 && a.lastStatus < 300;
|
|
2164
|
+
if (a.lastStatus && (!lastOk || !act)) {
|
|
2165
|
+
line += ` ${dim('Last')} ${statusColor(a.lastStatus)} ${dim(formatMs(a.lastResponseMs))}`;
|
|
2166
|
+
}
|
|
2052
2167
|
// Header-derived generic rate-limit (distinct from the monitor-endpoint quota),
|
|
2053
2168
|
// if the provider upstream returns x-ratelimit-* headers — only when present.
|
|
2054
2169
|
if (q.genericLimit != null && q.genericRemaining != null) {
|