maxpool 1.5.60 → 1.5.62
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/account-manager.js +22 -0
- package/src/tui.js +34 -4
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -176,6 +176,10 @@ const LOAD_EVENT_MAX_AGE_MS = 60 * 60 * 1000;
|
|
|
176
176
|
// fall back to per-account handling. Bounds any future "poisoned probe" from becoming
|
|
177
177
|
// an indefinite fleet-wide outage (2026-07-25 incident).
|
|
178
178
|
const MAX_FAILED_PROBES = 4;
|
|
179
|
+
// Consecutive quota-probe failures before we say so. High enough that a network blip or a
|
|
180
|
+
// single 429 never trips it; low enough that a permanently dead endpoint surfaces the same
|
|
181
|
+
// day rather than never.
|
|
182
|
+
const PROBE_FAILURE_ALERT_AT = 20;
|
|
179
183
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
180
184
|
const FIVE_HOUR_MS = 5 * 60 * 60 * 1000;
|
|
181
185
|
|
|
@@ -2183,6 +2187,8 @@ export class AccountManager {
|
|
|
2183
2187
|
* Same effect as learning quota from a live response, but for idle accounts.
|
|
2184
2188
|
*/
|
|
2185
2189
|
applyUsageData(accountIndex, usage) {
|
|
2190
|
+
// A successful probe ends any escalation streak.
|
|
2191
|
+
if (this.accounts[accountIndex]?.quota) this.accounts[accountIndex].quota.consecutiveProbeFailures = 0;
|
|
2186
2192
|
const account = this.accounts[accountIndex];
|
|
2187
2193
|
if (!account || !usage) return;
|
|
2188
2194
|
const q = account.quota;
|
|
@@ -2257,6 +2263,22 @@ export class AccountManager {
|
|
|
2257
2263
|
q.lastProbeError = message ? String(message).slice(0, 160) : 'probe failed';
|
|
2258
2264
|
q.lastProbeErrorAt = Date.now();
|
|
2259
2265
|
q.lastProbeErrorStatus = Number.isFinite(status) ? status : null;
|
|
2266
|
+
// ESCALATE A SYSTEMIC PROBE FAILURE. These fields were written and never read by
|
|
2267
|
+
// anything, so a probe that fails on EVERY call did so in total silence — measured
|
|
2268
|
+
// 2026-08-06: both quota endpoints (/v1/usages and /v1/usage) return 404, so weekly
|
|
2269
|
+
// quota was only ever learned from upstream 429 headers, and an account that had not
|
|
2270
|
+
// yet hit a 429 showed a blank weekly forever with no indication why.
|
|
2271
|
+
// A fail-open probe that never says it is failing is a log line, not a detector.
|
|
2272
|
+
q.consecutiveProbeFailures = (q.consecutiveProbeFailures || 0) + 1;
|
|
2273
|
+
const n = q.consecutiveProbeFailures;
|
|
2274
|
+
// Once, at a threshold that cannot be a blip, then every 100th so it stays visible
|
|
2275
|
+
// without walling the log.
|
|
2276
|
+
if (n === PROBE_FAILURE_ALERT_AT || (n > PROBE_FAILURE_ALERT_AT && n % 100 === 0)) {
|
|
2277
|
+
console.error(`[Maxpool] Quota probe has failed ${n}x in a row for "${account.name}"`
|
|
2278
|
+
+ `${status ? ` (HTTP ${status})` : ''}: ${q.lastProbeError}. `
|
|
2279
|
+
+ 'Weekly quota can only be learned from upstream 429 headers until this recovers, '
|
|
2280
|
+
+ 'so an account that has not hit a 429 will show a blank weekly.');
|
|
2281
|
+
}
|
|
2260
2282
|
}
|
|
2261
2283
|
|
|
2262
2284
|
/**
|
package/src/tui.js
CHANGED
|
@@ -480,6 +480,38 @@ export class TUI {
|
|
|
480
480
|
|
|
481
481
|
/** The visible Updates panel. Answers, at a glance: what am I running, is anything
|
|
482
482
|
* newer, is it automatic, and what is happening right now. */
|
|
483
|
+
/** The Routing header line. Split out so a test can assert the RENDERED string —
|
|
484
|
+
* matching source text is how a header that contradicted routing survived review. */
|
|
485
|
+
/** The "Cross-provider: …" fragment. Its own method so a test can assert what the user
|
|
486
|
+
* actually sees without needing a TTY — the previous version read a superseded global
|
|
487
|
+
* and contradicted routing, and only a source-text test would have "passed" on it. */
|
|
488
|
+
_crossProviderText() {
|
|
489
|
+
// Show the EFFECTIVE policy, i.e. what actually governs routing. Since per-provider
|
|
490
|
+
// settings were added, `_claudeFallbackFor(provider)` decides — and the legacy global
|
|
491
|
+
// knob this used to read is only its fallback when a provider has no entry of its own.
|
|
492
|
+
// Reading the global made the header contradict both the footer and reality: it said
|
|
493
|
+
// "when-exhausted" while GLM and Kimi were each set to "always" and routing agreed
|
|
494
|
+
// with the footer, not the header.
|
|
495
|
+
const provs = this.am.accounts.filter(a => a.type === 'provider');
|
|
496
|
+
if (!provs.length) return ''; // no GLM/Kimi in this pool — the whole line is meaningless
|
|
497
|
+
const eff = [...new Set(provs.map(a => this.am._claudeFallbackFor?.(a.provider)
|
|
498
|
+
|| this.am._crossProviderFallbackPolicy?.() || 'when-exhausted'))];
|
|
499
|
+
const colour = v => (v === 'never' ? yellow(v) : v === 'always' ? green(v) : cyan(v));
|
|
500
|
+
if (eff.length === 1) {
|
|
501
|
+
return ` ${dim('·')} ${dim('Cross-provider:')} ${colour(eff[0])}`;
|
|
502
|
+
} else {
|
|
503
|
+
// Providers disagree — naming each is the only honest summary; a single word here
|
|
504
|
+
// would misdescribe at least one of them.
|
|
505
|
+
const parts = provs.map(a => `${a.provider === 'zai' ? 'GLM' : a.provider === 'kimi' ? 'Kimi' : a.provider}: `
|
|
506
|
+
+ colour(this.am._claudeFallbackFor?.(a.provider) || 'never'));
|
|
507
|
+
return ` ${dim('·')} ${dim('Cross-provider:')} ${parts.join(dim(' / '))}`;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
_routingLine(routing, xpText) {
|
|
512
|
+
return ` Routing ${cyan(routing)}${xpText}`;
|
|
513
|
+
}
|
|
514
|
+
|
|
483
515
|
_renderUpdatesDetail() {
|
|
484
516
|
const v = this.am.versionInfo;
|
|
485
517
|
const running = v?.current ? `v${v.current}` : 'unknown';
|
|
@@ -1137,9 +1169,7 @@ export class TUI {
|
|
|
1137
1169
|
const hasProviders = this.am.accounts.some(a => a.type === 'provider');
|
|
1138
1170
|
let xpText = '';
|
|
1139
1171
|
if (hasProviders) {
|
|
1140
|
-
|
|
1141
|
-
const pc = p === 'never' ? yellow(p) : p === 'always' ? green(p) : cyan(p);
|
|
1142
|
-
xpText = ` ${dim('·')} ${dim('Cross-provider:')} ${pc}`;
|
|
1172
|
+
xpText = this._crossProviderText();
|
|
1143
1173
|
// Overflow visibility: when GLM/Kimi actually served requests recently, surface the
|
|
1144
1174
|
// volume so provider traffic isn't a mystery — it's overflow that spilled here while
|
|
1145
1175
|
// OAuth was saturated. Reuses the SAME 15m load window as the per-row "15m Nr" column.
|
|
@@ -1150,7 +1180,7 @@ export class TUI {
|
|
|
1150
1180
|
}
|
|
1151
1181
|
if (provReq > 0) xpText += ` ${dim('·')} ${yellow(`providers: ${provReq} req/15m`)}`;
|
|
1152
1182
|
}
|
|
1153
|
-
lines.push(
|
|
1183
|
+
lines.push(this._routingLine(routing, xpText));
|
|
1154
1184
|
const queuedCount = this.am.queueState?.waiting?.length || 0;
|
|
1155
1185
|
// Throttle and WAITING are different things and used to share one line labelled
|
|
1156
1186
|
// "Anthropic upstream throttled" — so a request parked purely because every account
|