maxpool 1.14.0 → 1.16.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/account-manager.js +79 -11
- package/src/index.js +8 -0
- package/src/tui.js +159 -32
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -325,6 +325,20 @@ function parseResetHeader(value) {
|
|
|
325
325
|
return Number.isNaN(asDate) ? null : asDate;
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
/**
|
|
329
|
+
* A capUtilization is valid only as a real number strictly inside (0,1). Everything
|
|
330
|
+
* else — strings, NaN, negative, 0, >=1 — means "no cap"; an invalid EXPLICIT value
|
|
331
|
+
* also logs once so a hand-edited config doesn't silently fail open (a NaN cap makes
|
|
332
|
+
* every `util >= cap` comparison false = uncapped, with no error anywhere).
|
|
333
|
+
*/
|
|
334
|
+
function _sanitizeCap(value, name) {
|
|
335
|
+
if (value == null) return null;
|
|
336
|
+
const n = Number(value);
|
|
337
|
+
if (Number.isFinite(n) && n > 0 && n < 1) return n;
|
|
338
|
+
console.log(`[Maxpool] Ignoring invalid capUtilization ${JSON.stringify(value)} for "${name}" — expected 0-1`);
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
328
342
|
export class AccountManager {
|
|
329
343
|
constructor(accounts, switchThreshold = 0.90, schedulerOptions = {}, dependencies = {}) {
|
|
330
344
|
this.scheduler = { ...DEFAULT_SCHEDULER, ...schedulerOptions };
|
|
@@ -354,6 +368,13 @@ export class AccountManager {
|
|
|
354
368
|
authHeader: acct.authHeader || null,
|
|
355
369
|
profiles: acct.profiles || (acct.type === 'provider' ? ['all'] : ['claude', 'all']),
|
|
356
370
|
priority: Number.isFinite(acct.priority) ? acct.priority : 0,
|
|
371
|
+
// USAGE CAP (owner-directed 2026-08-26): reserve capacity on this account —
|
|
372
|
+
// the proxy benches it at capUtilization of BOTH the 5h and 7d windows, keeping
|
|
373
|
+
// the rest for out-of-band use. null/undefined = fully utilized (the default;
|
|
374
|
+
// no behavior change). SANITIZED at parse: non-finite/out-of-range values drop
|
|
375
|
+
// to null HERE, visibly (below), so a hand-edited "50" or "abc" in the config
|
|
376
|
+
// can never fail the >= comparisons open as NaN.
|
|
377
|
+
capUtilization: _sanitizeCap(acct.capUtilization, acct.name),
|
|
357
378
|
model: acct.model || null,
|
|
358
379
|
modelMap: acct.modelMap || null,
|
|
359
380
|
stripBetaHeaders: Boolean(acct.stripBetaHeaders),
|
|
@@ -837,7 +858,7 @@ export class AccountManager {
|
|
|
837
858
|
// headroom (e.g. 69% used, resets in days) must stay in the healthy-spread
|
|
838
859
|
// pool even if it's burning fast. Pace is a soft SCORE cost, never a bench.
|
|
839
860
|
const weeklyState = this._weeklyRawState(account);
|
|
840
|
-
if (weeklyState === 'exhausted') return false;
|
|
861
|
+
if (weeklyState === 'exhausted' || weeklyState === 'capped') return false;
|
|
841
862
|
if (weeklyState === 'critical' && !options.allowWeeklyCritical) return false;
|
|
842
863
|
if (weeklyState === 'reserve' && !options.allowWeeklyReserve) return false;
|
|
843
864
|
|
|
@@ -1266,28 +1287,47 @@ export class AccountManager {
|
|
|
1266
1287
|
}
|
|
1267
1288
|
}
|
|
1268
1289
|
|
|
1290
|
+
/**
|
|
1291
|
+
* The session-bench threshold for THIS account: the global switchThreshold, lowered
|
|
1292
|
+
* to its usage cap when one is set. Shared by _isSessionQuotaUnavailable (the bench)
|
|
1293
|
+
* and _shortTermRetry (the retry oracle) — one helper, both call sites, so the
|
|
1294
|
+
* oracle can never desync from the bench (a capped-benched account MUST report a
|
|
1295
|
+
* finite retry time or a live session holding on it gets error-fasted).
|
|
1296
|
+
*/
|
|
1297
|
+
_sessionBenchThreshold(account) {
|
|
1298
|
+
const cap = account?.capUtilization;
|
|
1299
|
+
return (cap != null && cap < this.switchThreshold) ? cap : this.switchThreshold;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/** True when the account's usage cap has it benched on the given window reading. */
|
|
1303
|
+
_capped(account, utilization) {
|
|
1304
|
+
const cap = account?.capUtilization;
|
|
1305
|
+
return cap != null && utilization != null && utilization >= cap;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1269
1308
|
_isSessionQuotaUnavailable(account) {
|
|
1270
1309
|
const q = account.quota;
|
|
1271
1310
|
this._clearExpiredQuotas(account);
|
|
1311
|
+
const bench = this._sessionBenchThreshold(account);
|
|
1272
1312
|
|
|
1273
1313
|
// Unified 5h quota is immediate availability. Weekly quota is handled
|
|
1274
1314
|
// separately as long-horizon admission control.
|
|
1275
|
-
if (q.unified5h != null && q.unified5h >=
|
|
1315
|
+
if (q.unified5h != null && q.unified5h >= bench) return true;
|
|
1276
1316
|
|
|
1277
1317
|
// Standard quotas (API key accounts)
|
|
1278
1318
|
if (q.tokensLimit != null && q.tokensRemaining != null) {
|
|
1279
1319
|
const used = 1 - (q.tokensRemaining / q.tokensLimit);
|
|
1280
|
-
if (used >=
|
|
1320
|
+
if (used >= bench) return true;
|
|
1281
1321
|
}
|
|
1282
1322
|
|
|
1283
1323
|
if (q.requestsLimit != null && q.requestsRemaining != null) {
|
|
1284
1324
|
const used = 1 - (q.requestsRemaining / q.requestsLimit);
|
|
1285
|
-
if (used >=
|
|
1325
|
+
if (used >= bench) return true;
|
|
1286
1326
|
}
|
|
1287
1327
|
|
|
1288
1328
|
// Provider (z.ai/Kimi) session quota — the 5h token window. Without this a
|
|
1289
1329
|
// provider at 95% of its 5h cap reads as fully available.
|
|
1290
|
-
if (q.providerSes != null && q.providerSes >=
|
|
1330
|
+
if (q.providerSes != null && q.providerSes >= bench) return true;
|
|
1291
1331
|
|
|
1292
1332
|
return false;
|
|
1293
1333
|
}
|
|
@@ -1578,9 +1618,10 @@ export class AccountManager {
|
|
|
1578
1618
|
queueable: true,
|
|
1579
1619
|
};
|
|
1580
1620
|
}
|
|
1581
|
-
if (weeklyState === 'exhausted') {
|
|
1621
|
+
if (weeklyState === 'exhausted' || weeklyState === 'capped') {
|
|
1582
1622
|
// Hard block: only a weekly reset unblocks it — a sooner short-term clear
|
|
1583
|
-
// does not help — so key the hold on the weekly reset.
|
|
1623
|
+
// does not help — so key the hold on the weekly reset. 'capped' (usage cap)
|
|
1624
|
+
// shares this arm: the cap's recovery time IS the window reset.
|
|
1584
1625
|
//
|
|
1585
1626
|
// Read the PROVIDER reset too. `unified7dReset` is an Anthropic-only field;
|
|
1586
1627
|
// a GLM/Kimi account stores its weekly reset in `providerWkReset`
|
|
@@ -1672,6 +1713,11 @@ export class AccountManager {
|
|
|
1672
1713
|
// queueable} the retry oracle can hold on. Kept separate from the weekly state
|
|
1673
1714
|
// so weekly-critical accounts surface their real near-term recovery time.
|
|
1674
1715
|
_shortTermRetry(account, now, q) {
|
|
1716
|
+
// USAGE CAP: the oracle reads the SAME per-account bench threshold as
|
|
1717
|
+
// _isSessionQuotaUnavailable (_sessionBenchThreshold) — a capped account benched
|
|
1718
|
+
// at 50% MUST report a finite retryAt here or a live session holding on it gets
|
|
1719
|
+
// error-fasted instead of waiting out the window (red-team blocker 2).
|
|
1720
|
+
const bench = this._sessionBenchThreshold(account);
|
|
1675
1721
|
if (account.status === 'throttled' && account.rateLimitedUntil && now < account.rateLimitedUntil) {
|
|
1676
1722
|
return { cause: 'rate_limited', retryAt: account.rateLimitedUntil, queueable: true };
|
|
1677
1723
|
}
|
|
@@ -1684,13 +1730,13 @@ export class AccountManager {
|
|
|
1684
1730
|
return { cause: 'upstream_failure', retryAt: account.provisionalUpstreamUntil, queueable: true };
|
|
1685
1731
|
}
|
|
1686
1732
|
|
|
1687
|
-
if (q.unified5h != null && q.unified5h >=
|
|
1733
|
+
if (q.unified5h != null && q.unified5h >= bench) {
|
|
1688
1734
|
return { cause: 'session_limit', retryAt: q.unified5hReset || null, queueable: Boolean(q.unified5hReset) };
|
|
1689
1735
|
}
|
|
1690
1736
|
|
|
1691
1737
|
if (q.tokensLimit != null && q.tokensRemaining != null && q.tokensLimit > 0) {
|
|
1692
1738
|
const used = 1 - q.tokensRemaining / q.tokensLimit;
|
|
1693
|
-
if (used >=
|
|
1739
|
+
if (used >= bench) {
|
|
1694
1740
|
const retryAt = q.resetsAt ? new Date(q.resetsAt).getTime() : null;
|
|
1695
1741
|
return { cause: 'token_limit', retryAt, queueable: Boolean(retryAt) };
|
|
1696
1742
|
}
|
|
@@ -1698,7 +1744,7 @@ export class AccountManager {
|
|
|
1698
1744
|
|
|
1699
1745
|
if (q.requestsLimit != null && q.requestsRemaining != null && q.requestsLimit > 0) {
|
|
1700
1746
|
const used = 1 - q.requestsRemaining / q.requestsLimit;
|
|
1701
|
-
if (used >=
|
|
1747
|
+
if (used >= bench) {
|
|
1702
1748
|
const retryAt = q.resetsAt ? new Date(q.resetsAt).getTime() : null;
|
|
1703
1749
|
return { cause: 'request_limit', retryAt, queueable: Boolean(retryAt) };
|
|
1704
1750
|
}
|
|
@@ -2819,7 +2865,7 @@ export class AccountManager {
|
|
|
2819
2865
|
|
|
2820
2866
|
_weeklyState(account) {
|
|
2821
2867
|
const rawState = this._weeklyRawState(account);
|
|
2822
|
-
if (rawState === 'unknown' || rawState === 'exhausted') return rawState;
|
|
2868
|
+
if (rawState === 'unknown' || rawState === 'exhausted' || rawState === 'capped') return rawState;
|
|
2823
2869
|
|
|
2824
2870
|
const pressure = Math.max(clamp01(account.quota.unified7d ?? 0), this._effectiveWeeklyUsage(account));
|
|
2825
2871
|
if (pressure >= this.scheduler.weeklyCriticalThreshold) return 'critical';
|
|
@@ -2858,6 +2904,11 @@ export class AccountManager {
|
|
|
2858
2904
|
const sesUsed = q.providerSes != null ? clamp01(q.providerSes) : null;
|
|
2859
2905
|
const wkUsed = q.providerWk != null ? clamp01(q.providerWk) : null;
|
|
2860
2906
|
const used = Math.max(sesUsed ?? 0, wkUsed ?? 0);
|
|
2907
|
+
// USAGE CAP — checked FIRST, before every tier: a reservation is owner intent
|
|
2908
|
+
// and outranks both the tier ladder and the upstream verdict. There is no
|
|
2909
|
+
// upstreamAllows carve-out for providers anyway, but the ordering documents
|
|
2910
|
+
// that a cap can never be talked out of by the vendor's "allowed".
|
|
2911
|
+
if (this._capped(account, used)) return 'capped';
|
|
2861
2912
|
if (used >= this.scheduler.weeklyExhaustedThreshold) return 'exhausted';
|
|
2862
2913
|
if (used >= this.scheduler.weeklyCriticalThreshold) return 'critical';
|
|
2863
2914
|
if (used >= this.scheduler.weeklyReserveThreshold) return 'reserve';
|
|
@@ -2878,6 +2929,12 @@ export class AccountManager {
|
|
|
2878
2929
|
// was waiting on, withheld because a threshold outranked the upstream's own answer.
|
|
2879
2930
|
// 'exhausted' is the only state that removes an account from routing, so the override
|
|
2880
2931
|
// is scoped to it — critical/reserve still apply their soft costs unchanged.
|
|
2932
|
+
// USAGE CAP — before the upstreamAllows carve-out BY DESIGN (red-team blocker 1):
|
|
2933
|
+
// a capped account is below its REAL limit, so upstream keeps saying "allowed"
|
|
2934
|
+
// right through the cap — the override exists for genuine over-limit-but-allowed
|
|
2935
|
+
// states and would otherwise make the cap a no-op on exactly the account it is
|
|
2936
|
+
// for (measured: this exact shape sat at unified7d=1.00 'allowed_warning').
|
|
2937
|
+
if (this._capped(account, used)) return 'capped';
|
|
2881
2938
|
const upstreamAllows = typeof q.unifiedStatus === 'string' && q.unifiedStatus.startsWith('allowed');
|
|
2882
2939
|
if (used >= this.scheduler.weeklyExhaustedThreshold && !upstreamAllows) return 'exhausted';
|
|
2883
2940
|
if (used >= this.scheduler.weeklyCriticalThreshold) return 'critical';
|
|
@@ -3748,6 +3805,7 @@ export class AccountManager {
|
|
|
3748
3805
|
runtime: Boolean(acctData.runtime),
|
|
3749
3806
|
configSourced: Boolean(acctData.configSourced),
|
|
3750
3807
|
secretName: acctData.secretName || null,
|
|
3808
|
+
capUtilization: _sanitizeCap(acctData.capUtilization, acctData.name),
|
|
3751
3809
|
enabled: acctData.enabled !== false,
|
|
3752
3810
|
refreshToken: acctData.refreshToken || null,
|
|
3753
3811
|
expiresAt: acctData.expiresAt || null,
|
|
@@ -3808,6 +3866,12 @@ export class AccountManager {
|
|
|
3808
3866
|
// all` header path (prepareRuntimeProviders) omits enabled, so a re-sent token
|
|
3809
3867
|
// NEVER silently re-enables a provider the user benched in the TUI.
|
|
3810
3868
|
if (acctData.enabled !== undefined) account.enabled = acctData.enabled !== false;
|
|
3869
|
+
// Same guard for the usage cap: the restore path carries an explicit persisted
|
|
3870
|
+
// value; the `cc all` header path omits it, so a re-sent token never clears a
|
|
3871
|
+
// cap the user set in the TUI.
|
|
3872
|
+
if (acctData.capUtilization !== undefined) {
|
|
3873
|
+
account.capUtilization = _sanitizeCap(acctData.capUtilization, account.name);
|
|
3874
|
+
}
|
|
3811
3875
|
if (account.status === 'error' && changed) {
|
|
3812
3876
|
account.status = 'active';
|
|
3813
3877
|
account.lastError = null;
|
|
@@ -3845,6 +3909,9 @@ export class AccountManager {
|
|
|
3845
3909
|
// benched across a restart — without this an intentionally-disabled GLM/Kimi
|
|
3846
3910
|
// silently comes back enabled on the next boot (restore defaults enabled:true).
|
|
3847
3911
|
enabled: a.enabled,
|
|
3912
|
+
// And the usage cap, same reasoning: a TUI-set reservation must survive both
|
|
3913
|
+
// the restart AND the next `cc all` header re-send (the upsert guard).
|
|
3914
|
+
capUtilization: a.capUtilization ?? null,
|
|
3848
3915
|
}));
|
|
3849
3916
|
}
|
|
3850
3917
|
|
|
@@ -4057,6 +4124,7 @@ export class AccountManager {
|
|
|
4057
4124
|
upstream: a.upstream,
|
|
4058
4125
|
profiles: a.profiles,
|
|
4059
4126
|
priority: a.priority,
|
|
4127
|
+
capUtilization: a.capUtilization ?? null,
|
|
4060
4128
|
runtime: a.runtime,
|
|
4061
4129
|
status: a.status,
|
|
4062
4130
|
refreshDead: Boolean(a.refreshDead),
|
package/src/index.js
CHANGED
|
@@ -2099,6 +2099,14 @@ async function syncAccountsFromDisk(diskConfig, memConfig, accountManager) {
|
|
|
2099
2099
|
accountManager.setAccountEnabled(mgr.index, enabled);
|
|
2100
2100
|
console.log(`[Maxpool] ${enabled ? 'Enabled' : 'Disabled'} account "${mgr.name}" from config`);
|
|
2101
2101
|
}
|
|
2102
|
+
// Propagate a hand-edited usage cap the same way as enabled (red-team: without
|
|
2103
|
+
// this a config-edit cap is stale until the next full reload).
|
|
2104
|
+
const diskCap = Number.isFinite(diskAcct.capUtilization) && diskAcct.capUtilization > 0 && diskAcct.capUtilization < 1
|
|
2105
|
+
? diskAcct.capUtilization : null;
|
|
2106
|
+
if (mgr.capUtilization !== diskCap) {
|
|
2107
|
+
mgr.capUtilization = diskCap;
|
|
2108
|
+
console.log(`[Maxpool] Usage cap for "${mgr.name}" ${diskCap ? `set to ${Math.round(diskCap * 100)}%` : 'removed'} from config`);
|
|
2109
|
+
}
|
|
2102
2110
|
memConfig.accounts[memIdx] = { ...memConfig.accounts[memIdx], ...diskAcct };
|
|
2103
2111
|
|
|
2104
2112
|
if (freshCred.accessToken) {
|
package/src/tui.js
CHANGED
|
@@ -162,28 +162,74 @@ function countdown(ts) {
|
|
|
162
162
|
return `${Math.ceil(ms / 86_400_000)}d`;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/** ACTIVITY cell — "is this account working, and is it healthy?" in as few glyphs
|
|
166
|
+
* as that question needs. Rewritten 2026-08-27 on owner feedback: the old form
|
|
167
|
+
* ("Now 0 15m 0r 1h 0r") printed three numbers on EVERY row, and on a resting
|
|
168
|
+
* account all three were zero — a column of noise whose vocabulary ("now", then
|
|
169
|
+
* some minutes, then hours) nobody could read. An idle account now renders
|
|
170
|
+
* NOTHING, and a working one renders live · rate · latency.
|
|
171
|
+
*
|
|
172
|
+
* ▶2 · 17/h · 8.5s 2 in flight, 17 requests in the last hour, 8.5s average
|
|
173
|
+
* 17/h · 8.5s idle this second, but working
|
|
174
|
+
* 17/h · 8.5s 2f …with 2 failures in the last 15m
|
|
175
|
+
* (blank) resting — nothing to say
|
|
176
|
+
*/
|
|
165
177
|
function loadText(load) {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
const inflight = load?.current?.inFlight || 0;
|
|
179
|
+
const hourReq = load?.last1h?.requests || 0;
|
|
180
|
+
const failed = load?.last15m?.failed || 0;
|
|
181
|
+
if (!inflight && !hourReq && !failed) return '';
|
|
182
|
+
const parts = [];
|
|
183
|
+
if (inflight) parts.push(`▶${inflight}`);
|
|
184
|
+
if (hourReq) parts.push(`${hourReq}/h`);
|
|
185
|
+
const avg = load?.last15m?.avgMs ?? load?.last1h?.avgMs;
|
|
186
|
+
if (avg != null && hourReq) parts.push(formatMs(avg));
|
|
187
|
+
let s = parts.join(' · ');
|
|
188
|
+
if (failed) s += ` ${red(`${failed}f`)}`;
|
|
189
|
+
return s;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The usage CAP is an account PROPERTY, not a state — so it renders whenever one
|
|
193
|
+
* is set, on every account type, whatever the account is doing. It previously
|
|
194
|
+
* lived inline in the provider branch only, so the OAuth account the feature was
|
|
195
|
+
* built for (max@gomokka.com) showed no cap anywhere: reported 2026-08-27, "I need
|
|
196
|
+
* to be able to see whether an account has a cap or not." Yellow while the cap is
|
|
197
|
+
* actively holding traffic back, dim otherwise. */
|
|
198
|
+
function capText(a, benched) {
|
|
199
|
+
if (a?.capUtilization == null) return '';
|
|
200
|
+
const t = `cap ${Math.round(a.capUtilization * 100)}%`;
|
|
201
|
+
return benched ? yellow(t) : dim(t);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** True when the reservation is what is currently keeping traffic off this account
|
|
205
|
+
* — either window at or past the cap. Reads the manager's own predicate so the
|
|
206
|
+
* label can never disagree with routing. */
|
|
207
|
+
function capBenched(am, a) {
|
|
208
|
+
if (a?.capUtilization == null) return false;
|
|
209
|
+
const q = a.quota || {};
|
|
210
|
+
const ses = a.type === 'provider' ? q.providerSes : q.unified5h;
|
|
211
|
+
const wk = a.type === 'provider' ? q.providerWk : q.unified7d;
|
|
212
|
+
return !!(am?._capped?.(a, ses) || am?._capped?.(a, wk));
|
|
181
213
|
}
|
|
182
214
|
|
|
183
215
|
export function weeklyPolicyText(am, account) {
|
|
184
216
|
if (!am?._weeklyState || !account || account.type === 'provider') return '';
|
|
185
217
|
const state = am._weeklyState(account);
|
|
218
|
+
// USAGE CAP: the reservation outranks every tier label — a capped account is NOT
|
|
219
|
+
// exhausted, it's deliberately held, and labelling it "Wk exhausted 50%" (red team)
|
|
220
|
+
// while the bar shows half-full misstates the owner's own setting.
|
|
221
|
+
if (state === 'capped') {
|
|
222
|
+
return yellow(`Cap ${Math.round((account.capUtilization || 0) * 100)}%`);
|
|
223
|
+
}
|
|
186
224
|
if (!state || state === 'unknown' || state === 'normal') return '';
|
|
225
|
+
// SAY IT ONCE (2026-08-27). An account Anthropic is rejecting outright already
|
|
226
|
+
// says so twice on this row: the Wk bar reads 100%, and the Status column reads
|
|
227
|
+
// "exhausted". A third "Wk exhausted 100%" tag was pure repetition, and it pushed
|
|
228
|
+
// the genuinely-informative tags (Cap, a per-model sub-limit) off to the right.
|
|
229
|
+
// Dropped ONLY when the Status column carries the same fact — a soft/reserve/
|
|
230
|
+
// critical row, or an exhausted-by-threshold row the status column shows as
|
|
231
|
+
// "active", still needs the tag.
|
|
232
|
+
if (am._isAccountWideRejected?.(account)) return '';
|
|
187
233
|
const rawState = am._weeklyRawState?.(account) || state;
|
|
188
234
|
const used = Number(account.quota?.unified7d);
|
|
189
235
|
const pct = Number.isFinite(used)
|
|
@@ -647,6 +693,11 @@ export class TUI {
|
|
|
647
693
|
this._startSelection('rename');
|
|
648
694
|
} else if (k === 't' && this.am.accounts.length > 0) {
|
|
649
695
|
this._startSelection('toggle');
|
|
696
|
+
} else if (k === 'u' && this.am.accounts.length > 0) {
|
|
697
|
+
// USAGE CAP (owner-directed 2026-08-26): reserve capacity — the proxy benches
|
|
698
|
+
// this account at the chosen % of both the 5h and weekly windows, keeping the
|
|
699
|
+
// rest for personal use. Enter 1-99 to set; enter 0 to remove (fully utilized).
|
|
700
|
+
this._startSelection('cap');
|
|
650
701
|
} else if (k === 'd' && this.am.accounts.length > 0) {
|
|
651
702
|
this._startSelection('delete');
|
|
652
703
|
} else if (k === 'esc' || k === 'q') {
|
|
@@ -1025,6 +1076,15 @@ export class TUI {
|
|
|
1025
1076
|
this.inputBuf = '';
|
|
1026
1077
|
this.inputSensitive = false;
|
|
1027
1078
|
this.inputCb = value => this._doRename(targetIdx, String(value || '').trim());
|
|
1079
|
+
} else if (this.selAction === 'cap') {
|
|
1080
|
+
const targetIdx = this.selIdx;
|
|
1081
|
+
const current = account.name;
|
|
1082
|
+
const existing = this.am.accounts[targetIdx]?.capUtilization;
|
|
1083
|
+
this.mode = 'input';
|
|
1084
|
+
this.inputPrompt = `Usage cap % for "${current}" (1-99, 0 = off, now ${existing ? Math.round(existing * 100) + '%' : 'off'})`;
|
|
1085
|
+
this.inputBuf = '';
|
|
1086
|
+
this.inputSensitive = false;
|
|
1087
|
+
this.inputCb = value => this._doSetCap(targetIdx, String(value || '').trim());
|
|
1028
1088
|
}
|
|
1029
1089
|
}
|
|
1030
1090
|
else if (k === 'esc' || k === 'q') { this.mode = 'normal'; }
|
|
@@ -1196,6 +1256,49 @@ export class TUI {
|
|
|
1196
1256
|
}
|
|
1197
1257
|
|
|
1198
1258
|
// Rename an account in config and in the running manager.
|
|
1259
|
+
/**
|
|
1260
|
+
* USAGE CAP setter — persists to config (rollback on write failure) and applies
|
|
1261
|
+
* live. `0` / `off` / `100` REMOVE the cap (fully utilized); 1-99 set it. Runtime
|
|
1262
|
+
* providers (not in config) keep the cap in memory like their `enabled` flag —
|
|
1263
|
+
* it rides state.json across restarts and must also survive `cc all` header
|
|
1264
|
+
* re-upserts (the upsert guard in account-manager).
|
|
1265
|
+
*/
|
|
1266
|
+
async _doSetCap(idx, raw) {
|
|
1267
|
+
const account = this.am.accounts[idx];
|
|
1268
|
+
if (!account) { this._addLog('Account no longer exists'); return; }
|
|
1269
|
+
const v = String(raw || '').trim().toLowerCase();
|
|
1270
|
+
const off = v === '' || v === '0' || v === 'off' || v === '100';
|
|
1271
|
+
let pct = null;
|
|
1272
|
+
if (!off) {
|
|
1273
|
+
pct = parseInt(v, 10);
|
|
1274
|
+
if (!Number.isInteger(pct) || pct < 1 || pct > 99) {
|
|
1275
|
+
this._addLog(`Usage cap must be 1-99 (or 0 to remove) — got "${raw}"`);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const cap = off ? null : pct / 100;
|
|
1280
|
+
|
|
1281
|
+
const loc = this._configLocation(account);
|
|
1282
|
+
if (loc) {
|
|
1283
|
+
const prev = this.config[loc.array][loc.index].capUtilization ?? null;
|
|
1284
|
+
if (cap == null) delete this.config[loc.array][loc.index].capUtilization;
|
|
1285
|
+
else this.config[loc.array][loc.index].capUtilization = cap;
|
|
1286
|
+
try {
|
|
1287
|
+
await this.saveConfig(this.config);
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
// rollback both config and (below) skip the live apply
|
|
1290
|
+
if (prev == null) delete this.config[loc.array][loc.index].capUtilization;
|
|
1291
|
+
else this.config[loc.array][loc.index].capUtilization = prev;
|
|
1292
|
+
throw error;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// No loc: a runtime provider — in-memory + state.json persistence (same as enabled).
|
|
1296
|
+
account.capUtilization = cap;
|
|
1297
|
+
this._addLog(cap == null
|
|
1298
|
+
? `Usage cap removed for "${account.name}" — fully utilized`
|
|
1299
|
+
: `Usage cap ${pct}% set for "${account.name}" — the proxy stops routing to it at ${pct}% of the 5h and weekly windows`);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1199
1302
|
async _doRename(idx, newName) {
|
|
1200
1303
|
const account = this.am.accounts[idx];
|
|
1201
1304
|
if (!account) { this._addLog('Account no longer exists'); return; }
|
|
@@ -1675,7 +1778,7 @@ export class TUI {
|
|
|
1675
1778
|
// Glossary FOOTER (expands the abbreviations the header + inline labels can't
|
|
1676
1779
|
// spell out). Below the rows so it never breaks the header↔column alignment.
|
|
1677
1780
|
if (W >= 88) {
|
|
1678
|
-
lines.push(' ' + dim('Legend Ses = 5h · Wk = 7d ·
|
|
1781
|
+
lines.push(' ' + dim('Legend Ses = 5h · Wk = 7d · ▶ = in-flight · /h = requests last hour · avg latency · f = fails'));
|
|
1679
1782
|
}
|
|
1680
1783
|
}
|
|
1681
1784
|
|
|
@@ -1796,13 +1899,16 @@ export class TUI {
|
|
|
1796
1899
|
if (a.enabled !== false && upstreamBlocking && a.status === 'active') {
|
|
1797
1900
|
effectiveStatus = a.inFlight > 0 ? 'probing' : 'waiting';
|
|
1798
1901
|
}
|
|
1799
|
-
//
|
|
1800
|
-
//
|
|
1801
|
-
//
|
|
1802
|
-
//
|
|
1803
|
-
//
|
|
1902
|
+
// Anthropic is rejecting the WHOLE account (a 'rejected' unified status
|
|
1903
|
+
// corroborated by an exhausted unified bucket). A per-model cap (Fable) is NOT
|
|
1904
|
+
// account-wide — it shows as the separate "… maxed" tag, leaving the status
|
|
1905
|
+
// column truthful. Keys on a.status (not effectiveStatus) so a genuine block
|
|
1906
|
+
// wins even inside an upstream-throttle window.
|
|
1907
|
+
// Named "exhausted", not "blocked" (2026-08-27): the row's own quota bar and
|
|
1908
|
+
// every other surface call this state exhausted, and two words for one state
|
|
1909
|
+
// read as two different problems.
|
|
1804
1910
|
if (a.enabled !== false && a.status === 'active' && this.am._isAccountWideRejected?.(a)) {
|
|
1805
|
-
effectiveStatus = '
|
|
1911
|
+
effectiveStatus = 'exhausted';
|
|
1806
1912
|
}
|
|
1807
1913
|
// A dead refresh token surfaces as "reauth" (re-login needed) rather than a
|
|
1808
1914
|
// generic "error", so the user knows the fix. Display-only — account.status
|
|
@@ -1816,7 +1922,7 @@ export class TUI {
|
|
|
1816
1922
|
switch (effectiveStatus) {
|
|
1817
1923
|
case 'active': status = isCur ? green('active') : 'active'; break;
|
|
1818
1924
|
case 'reauth': status = yellow('reauth'); break;
|
|
1819
|
-
case 'blocked': status = red('
|
|
1925
|
+
case 'blocked': status = red('exhausted'); break;
|
|
1820
1926
|
case 'probing': status = green('probing'); break;
|
|
1821
1927
|
case 'waiting': status = yellow('waiting'); break;
|
|
1822
1928
|
case 'paused': status = yellow('paused'); break;
|
|
@@ -1873,6 +1979,14 @@ export class TUI {
|
|
|
1873
1979
|
}
|
|
1874
1980
|
const weekly = weeklyPolicyText(this.am, a);
|
|
1875
1981
|
if (weekly) line += ` ${weekly}`;
|
|
1982
|
+
// The reservation, whatever the account is doing. weeklyPolicyText renders
|
|
1983
|
+
// "Cap 50%" only while the cap is the ACTIVE weekly state; this is the standing
|
|
1984
|
+
// property, so a capped account that is exhausted, throttled or idle still says
|
|
1985
|
+
// so. Suppressed when the weekly tag is already the Cap label (no double tag).
|
|
1986
|
+
if (weekly.includes('Cap ') === false) {
|
|
1987
|
+
const capTag = capText(a, capBenched(this.am, a));
|
|
1988
|
+
if (capTag) line += ` ${capTag}`;
|
|
1989
|
+
}
|
|
1876
1990
|
// Per-model weekly caps (e.g. Fable, while the unified weekly still has
|
|
1877
1991
|
// headroom). Show the ACTUAL utilization — "Fable 90%" (yellow) while high but
|
|
1878
1992
|
// still usable, "Fable maxed" (red) ONLY at genuine exhaustion. This is the
|
|
@@ -1895,7 +2009,8 @@ export class TUI {
|
|
|
1895
2009
|
// cause (e.g. rate-limited) when a probe failure is on record — rather than imply
|
|
1896
2010
|
// the last-known value is current.
|
|
1897
2011
|
line += this._probeHealthNote(a);
|
|
1898
|
-
|
|
2012
|
+
const act = loadText(this._accountLoad(a));
|
|
2013
|
+
if (act) line += ` ${dim(act)}`;
|
|
1899
2014
|
return line;
|
|
1900
2015
|
}
|
|
1901
2016
|
|
|
@@ -1976,15 +2091,27 @@ export class TUI {
|
|
|
1976
2091
|
sesCell = emptyBar('probing', bw);
|
|
1977
2092
|
wkCell = emptyBar('probing', bw);
|
|
1978
2093
|
}
|
|
2094
|
+
// The cap rides OUTSIDE the quota-readable branch: an account with an
|
|
2095
|
+
// unreadable or not-yet-probed quota still HAS its reservation, and hiding the
|
|
2096
|
+
// setting whenever the probe is quiet is how a shipped feature reads as absent.
|
|
2097
|
+
const capTag = capText(a, capBenched(this.am, a));
|
|
2098
|
+
if (capTag) note += ` ${capTag}`;
|
|
1979
2099
|
|
|
1980
2100
|
let line = ` ${sel}${cur} ${name} ${type} ${status} Ses ${sesCell}`;
|
|
1981
2101
|
if (showBoth) line += ` Wk ${wkCell}`;
|
|
1982
2102
|
line += note;
|
|
1983
2103
|
// Same recent-load columns as OAuth (providers track inFlight + load events),
|
|
1984
2104
|
// plus a compact Last <status> <ms> — the one signal that matters for a
|
|
1985
|
-
// rarely-hit fallback
|
|
1986
|
-
|
|
1987
|
-
if (
|
|
2105
|
+
// rarely-hit fallback. loadText renders empty when fully idle.
|
|
2106
|
+
const act = loadText(this._accountLoad(a));
|
|
2107
|
+
if (act) line += ` ${dim(act)}`;
|
|
2108
|
+
// "Last 200 6.4s" next to a live "17/h · 6.4s" said the same thing twice. Keep it
|
|
2109
|
+
// for the two cases where it is the ONLY thing that speaks: a non-2xx last result
|
|
2110
|
+
// (always worth seeing), or an idle row whose activity cell is blank.
|
|
2111
|
+
const lastOk = a.lastStatus >= 200 && a.lastStatus < 300;
|
|
2112
|
+
if (a.lastStatus && (!lastOk || !act)) {
|
|
2113
|
+
line += ` ${dim('Last')} ${statusColor(a.lastStatus)} ${dim(formatMs(a.lastResponseMs))}`;
|
|
2114
|
+
}
|
|
1988
2115
|
// Header-derived generic rate-limit (distinct from the monitor-endpoint quota),
|
|
1989
2116
|
// if the provider upstream returns x-ratelimit-* headers — only when present.
|
|
1990
2117
|
if (q.genericLimit != null && q.genericRemaining != null) {
|
|
@@ -2066,7 +2193,7 @@ export class TUI {
|
|
|
2066
2193
|
if (win === 'wk' && a.type === 'provider' && a.quota?.weeklyAbsent) {
|
|
2067
2194
|
const sesTank = this.am.capacityTank?.(i, 'ses');
|
|
2068
2195
|
const approx = sesTank && sesTank.n > 0
|
|
2069
|
-
?
|
|
2196
|
+
? formatTokens(Math.round(sesTank.avg * (7 * 24) / 5)) : '--';
|
|
2070
2197
|
const cellsByName = { Current: cyan(approx), Prev: '--', Avg: '--', N: sesTank?.n ? String(sesTank.n) : '--' };
|
|
2071
2198
|
out.push(' ' + name + ' ' + prov + ' '
|
|
2072
2199
|
+ COLS.map(c => cellsByName[c].padStart(CW)).join('')
|
|
@@ -2085,11 +2212,11 @@ export class TUI {
|
|
|
2085
2212
|
// vendor says it is. `~` estimate-from-open-window, `≥` lower bound (joined late).
|
|
2086
2213
|
let cur = '--';
|
|
2087
2214
|
if (tank && !(tank.source === 'live' && unprovenLive)) {
|
|
2088
|
-
cur =
|
|
2215
|
+
cur = formatTokens(tank.avg);
|
|
2089
2216
|
} else if (nowOpen?.tokensSoFar > 0 && util > 0) {
|
|
2090
|
-
cur =
|
|
2217
|
+
cur = formatTokens(Math.round(nowOpen.tokensSoFar / util));
|
|
2091
2218
|
}
|
|
2092
|
-
const curPct = util != null && cur !== '--' ? dim(
|
|
2219
|
+
const curPct = util != null && cur !== '--' ? dim(`@${Math.round(util * 100)}%`) : '';
|
|
2093
2220
|
const st = ledger.windowStats(a.name, win);
|
|
2094
2221
|
const cellsByName = {
|
|
2095
2222
|
Current: cur.padStart(CW - (curPct ? curPct.length + 1 : 0)),
|
|
@@ -2104,7 +2231,7 @@ export class TUI {
|
|
|
2104
2231
|
|
|
2105
2232
|
out.push('');
|
|
2106
2233
|
out.push(' ' + dim('Capacity = tokens ÷ how full the provider said the window was, at close.'));
|
|
2107
|
-
out.push(' ' + dim('
|
|
2234
|
+
out.push(' ' + dim('Current @% = live window, % full now · no-weekly weekly = avg 5h × 33.6.'));
|
|
2108
2235
|
return out;
|
|
2109
2236
|
}
|
|
2110
2237
|
|