maxpool 1.24.26 → 1.24.28
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 +30 -2
- package/src/capacity-ledger.js +21 -7
- package/src/tui.js +26 -9
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -327,6 +327,10 @@ const PERSISTED_QUOTA_FIELDS = [
|
|
|
327
327
|
'providerQuotaSource', 'lastProbeOkAt',
|
|
328
328
|
];
|
|
329
329
|
|
|
330
|
+
const PROVIDER_429_MIN_BENCH_SEC = 60;
|
|
331
|
+
const PROVIDER_429_MAX_BENCH_SEC = 15 * 60;
|
|
332
|
+
const PROVIDER_429_STREAK_FORGET_MS = 60 * 60 * 1000;
|
|
333
|
+
|
|
330
334
|
function clampRetryAfterSeconds(value) {
|
|
331
335
|
const n = Number(value);
|
|
332
336
|
if (!Number.isFinite(n)) return 60;
|
|
@@ -845,6 +849,7 @@ export class AccountManager {
|
|
|
845
849
|
if (outcome.success) {
|
|
846
850
|
account.completedRequests++;
|
|
847
851
|
account.consecutiveFailures = 0;
|
|
852
|
+
account.providerRateLimitStreak = 0;
|
|
848
853
|
account.lastStatus = outcome.status || account.lastStatus;
|
|
849
854
|
account.lastResponseMs = Date.now() - lease.startedAt;
|
|
850
855
|
if (account.provisionalUpstreamFingerprint) {
|
|
@@ -3927,8 +3932,31 @@ export class AccountManager {
|
|
|
3927
3932
|
return;
|
|
3928
3933
|
}
|
|
3929
3934
|
|
|
3935
|
+
// Provider "slow down" backoff (2026-10-21): z.ai answers a GLM 429 with a 3-12s
|
|
3936
|
+
// hint, and honouring it re-selected the account seconds later — ~200 refusals on
|
|
3937
|
+
// max+1 over 2026-10-16/17, then a z.ai usage-risk notice. Bench a provider 429
|
|
3938
|
+
// for at least 60s, doubling per back-to-back 429 up to 15 min; a success resets
|
|
3939
|
+
// the streak (releaseAccount). Claude accounts keep the exact retry-after.
|
|
3940
|
+
// A parallel 429 landing while the account is already benched is the SAME refusal
|
|
3941
|
+
// burst — it keeps the bench and does not escalate (red-team 2026-10-21: up to 50
|
|
3942
|
+
// in-flight requests per account turned one burst into the 15-min cap). The streak
|
|
3943
|
+
// also forgets after an hour with no refusals.
|
|
3944
|
+
let benchSeconds = retryAfter;
|
|
3945
|
+
if (account.type === 'provider' && (options.status || 429) === 429) {
|
|
3946
|
+
const now = Date.now();
|
|
3947
|
+
if (account.status === 'throttled' && account.rateLimitedUntil > now) {
|
|
3948
|
+
benchSeconds = Math.max(retryAfter, Math.ceil((account.rateLimitedUntil - now) / 1000));
|
|
3949
|
+
} else {
|
|
3950
|
+
if (now - (account.providerRateLimitAt || 0) > PROVIDER_429_STREAK_FORGET_MS) account.providerRateLimitStreak = 0;
|
|
3951
|
+
const streak = account.providerRateLimitStreak || 0;
|
|
3952
|
+
benchSeconds = Math.max(retryAfter, Math.min(PROVIDER_429_MAX_BENCH_SEC, PROVIDER_429_MIN_BENCH_SEC * 2 ** streak));
|
|
3953
|
+
account.providerRateLimitStreak = streak + 1;
|
|
3954
|
+
}
|
|
3955
|
+
account.providerRateLimitAt = now;
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3930
3958
|
account.status = 'throttled';
|
|
3931
|
-
account.rateLimitedUntil = Date.now() + (
|
|
3959
|
+
account.rateLimitedUntil = Date.now() + (benchSeconds * 1000);
|
|
3932
3960
|
account.lastStatus = options.status || 429;
|
|
3933
3961
|
account.lastError = 'rate_limited';
|
|
3934
3962
|
account.lastErrorAt = Date.now();
|
|
@@ -3937,7 +3965,7 @@ export class AccountManager {
|
|
|
3937
3965
|
account.failedRequests++;
|
|
3938
3966
|
account.consecutiveFailures++;
|
|
3939
3967
|
}
|
|
3940
|
-
console.log(`[Maxpool] Account "${account.name}" rate limited for ${retryAfter}s`);
|
|
3968
|
+
console.log(`[Maxpool] Account "${account.name}" rate limited for ${benchSeconds}s${benchSeconds > retryAfter ? ` (provider asked ${retryAfter}s; backing off)` : ''}`);
|
|
3941
3969
|
}
|
|
3942
3970
|
|
|
3943
3971
|
markAuthFailed(accountIndex, status = 403, reason = 'auth_failed') {
|
package/src/capacity-ledger.js
CHANGED
|
@@ -280,6 +280,7 @@ export class CapacityLedger {
|
|
|
280
280
|
// (we only counted the tokens that flowed through maxpool, while the vendor's
|
|
281
281
|
// percentage counts everything). Dropping it here made every tank read "bounded".
|
|
282
282
|
...(open.windowStartedAt != null ? { windowStartedAt: open.windowStartedAt } : {}),
|
|
283
|
+
...(open.peakReset ? { spansRefill: true } : {}),
|
|
283
284
|
resetAt,
|
|
284
285
|
});
|
|
285
286
|
if (rec[window].closed.length > MAX_CYCLES_PER_WINDOW) rec[window].closed.shift();
|
|
@@ -361,10 +362,19 @@ export class CapacityLedger {
|
|
|
361
362
|
if (!open) continue;
|
|
362
363
|
// PEAK: fullness only rises inside one window, so the highest reading a cycle
|
|
363
364
|
// saw is its closing fullness (closeCycle prefers it over a later, lower one).
|
|
364
|
-
if (!Number.isFinite(open.peakUtilization) || m.utilization > open.peakUtilization) {
|
|
365
|
-
open.peakUtilization = Math.min(1, m.utilization);
|
|
366
|
-
}
|
|
367
365
|
if (!(m.utilization < 1)) continue;
|
|
366
|
+
// A reading of 1.0 is the wall (throttled), not a fullness — the same reason
|
|
367
|
+
// estimateFromUtilization refuses u>=1 — so it never becomes the peak.
|
|
368
|
+
// A reading BELOW the peak by >=10pp means the tank was refilled under this
|
|
369
|
+
// open cycle (a redeemed reset, or a rollover whose stamp arrived late): the
|
|
370
|
+
// old peak no longer describes this window, so it is dropped rather than
|
|
371
|
+
// left to divide the new window's tokens (red-team 2026-10-13, finding 2).
|
|
372
|
+
if (Number.isFinite(open.peakUtilization) && open.peakUtilization - m.utilization >= 0.10) {
|
|
373
|
+
open.peakUtilization = m.utilization;
|
|
374
|
+
open.peakReset = true;
|
|
375
|
+
} else if (!Number.isFinite(open.peakUtilization) || m.utilization > open.peakUtilization) {
|
|
376
|
+
open.peakUtilization = m.utilization;
|
|
377
|
+
}
|
|
368
378
|
const key = `${m.name}:${m.window}`;
|
|
369
379
|
const prev = this._utilMarks.get(key);
|
|
370
380
|
// Keep the OLDEST usable mark within this cycle: a wider span means a larger
|
|
@@ -452,6 +462,8 @@ export class CapacityLedger {
|
|
|
452
462
|
const closed = (rec?.[window]?.closed || []).filter(c =>
|
|
453
463
|
!c.disabledDuring
|
|
454
464
|
&& c.partialReason !== 'pre-v3-unverified'
|
|
465
|
+
&& c.partialReason !== 'no-weekly-plan'
|
|
466
|
+
&& !c.spansRefill
|
|
455
467
|
&& (c.endedAt - c.startedAt) >= floor - 1_000
|
|
456
468
|
// CEILING: a cycle far longer than its window swallowed a rollover it never
|
|
457
469
|
// saw — two tanks' tokens over one tank's percentage (see WINDOW_CEIL_FACTOR).
|
|
@@ -471,10 +483,12 @@ export class CapacityLedger {
|
|
|
471
483
|
// floor rather than being thrown away — discarding left an account whose
|
|
472
484
|
// every reading was contaminated with NOTHING to show. After a plan
|
|
473
485
|
// downgrade the floor over-states until old cycles age out — conservative.
|
|
474
|
-
// The floor
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
486
|
+
// The floor is taken over EVERY admitted cycle, partial ones included: a
|
|
487
|
+
// partial cycle's token count is still tokens the vendor really delivered,
|
|
488
|
+
// so it is still a hard lower bound on the tank. Restricting it to complete
|
|
489
|
+
// cycles left it inert on weekly history, where nearly every cycle is partial
|
|
490
|
+
// (red-team 2026-10-13, finding 4).
|
|
491
|
+
const maxDelivered = closed.reduce((m, c) => Math.max(m, c.tokens), 0);
|
|
478
492
|
const usable = closed.filter(c =>
|
|
479
493
|
Number.isFinite(c.finalUtilization) && c.finalUtilization >= TANK_MIN_UTIL);
|
|
480
494
|
if (!usable.length) return null;
|
package/src/tui.js
CHANGED
|
@@ -1254,11 +1254,12 @@ export class TUI {
|
|
|
1254
1254
|
this.mode = 'input';
|
|
1255
1255
|
// Plain-English prompt (owner 2026-10-13: "I'm not sure how does this work.
|
|
1256
1256
|
// I want to keep the dynamic floor, but I also want to raise the basic cap").
|
|
1257
|
-
// Type just a number = the dynamic cap's STARTING level
|
|
1258
|
-
// to NN%
|
|
1259
|
-
//
|
|
1260
|
-
//
|
|
1261
|
-
|
|
1257
|
+
// Type just a number = the dynamic cap's STARTING level: routing USES the
|
|
1258
|
+
// account up to NN% of each window, and the allowance rises toward the
|
|
1259
|
+
// switch threshold as the window nears its reset (by elapsed time —
|
|
1260
|
+
// _effectiveCap), so unused capacity isn't stranded. fNN = fixed stop at NN%.
|
|
1261
|
+
// 0 / off = no cap. Kept short: the typed value must stay on an 80-col line.
|
|
1262
|
+
this.inputPrompt = `Cap ${current}, now ${nowLabel}. 75=use to 75%+rises near reset f75=hard 0=off`;
|
|
1262
1263
|
this.inputBuf = '';
|
|
1263
1264
|
this.inputSensitive = false;
|
|
1264
1265
|
this.inputCb = value => this._doSetCap(targetIdx, String(value || '').trim());
|
|
@@ -2509,8 +2510,9 @@ export class TUI {
|
|
|
2509
2510
|
const prov = gray(providerLabel(a).padEnd(PROVIDER_W));
|
|
2510
2511
|
|
|
2511
2512
|
// The no-weekly plan has NO weekly window to complete — its weekly "capacity" is
|
|
2512
|
-
// derived: avg session tank × 33.6 (the number of 5h windows in 7 days). Shown
|
|
2513
|
-
//
|
|
2513
|
+
// derived: avg session tank × 33.6 (the number of 5h windows in 7 days). Shown
|
|
2514
|
+
// without a marker (owner 2026-08-26: the row's text states the derivation);
|
|
2515
|
+
// N is the count of session cycles it rests on.
|
|
2514
2516
|
if (win === 'wk' && a.type === 'provider' && a.quota?.weeklyAbsent) {
|
|
2515
2517
|
const sesTank = this.am.capacityTank?.(i, 'ses');
|
|
2516
2518
|
const approx = sesTank && sesTank.n > 0
|
|
@@ -2645,8 +2647,23 @@ export class TUI {
|
|
|
2645
2647
|
: 'delete';
|
|
2646
2648
|
return ` ${dim('↑↓')} select ${bold('Enter')} ${act} ${bold('Esc')} cancel`;
|
|
2647
2649
|
}
|
|
2648
|
-
case 'input':
|
|
2649
|
-
|
|
2650
|
+
case 'input': {
|
|
2651
|
+
// The typed value and the cursor are the whole point of this line — they are
|
|
2652
|
+
// NEVER truncated. On a narrow terminal the PROMPT gives way instead, keeping
|
|
2653
|
+
// its last words (nearest the value). Red-team 2026-10-13: the long cap
|
|
2654
|
+
// prompt pushed the input off a 200-column terminal entirely.
|
|
2655
|
+
const shown = this.inputSensitive ? '•'.repeat(this.inputBuf.length) : this.inputBuf;
|
|
2656
|
+
const full = ` ${this.inputPrompt}: ${shown}█`;
|
|
2657
|
+
const visible = full.replace(ANSI_RE, '');
|
|
2658
|
+
const budget = (process.stdout.columns || 80) - 2;
|
|
2659
|
+
if (visible.length <= budget) return full;
|
|
2660
|
+
const keep = Math.max(0, budget - (shown.length + 2)); // ' …' + value + cursor
|
|
2661
|
+
const cut = visible.length - budget;
|
|
2662
|
+
const trimmed = this.inputPrompt.length > cut + 1
|
|
2663
|
+
? this.inputPrompt.slice(0, Math.max(1, keep - 1)) + '…'
|
|
2664
|
+
: this.inputPrompt;
|
|
2665
|
+
return ` ${trimmed}: ${shown}█`;
|
|
2666
|
+
}
|
|
2650
2667
|
case 'confirm':
|
|
2651
2668
|
return ` ${bold(this.confirmTitle)} ${bold('y')} Yes ${bold('n')} No`;
|
|
2652
2669
|
default:
|