maxpool 1.24.32 → 1.24.33
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 +29 -1
- package/src/outside-usage.js +88 -0
- package/src/prober.js +42 -3
- package/src/reset-policy.js +1 -0
- package/src/tui.js +4 -0
- package/src/zai-reset-cards.js +24 -0
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { refreshAccessToken, isTokenExpiringSoon, modelFamily, tokenFingerprint } from './oauth.js';
|
|
2
|
+
import { hourFloor } from './outside-usage.js';
|
|
2
3
|
import { CapacityLedger, TANK_MIN_UTIL } from './capacity-ledger.js';
|
|
3
4
|
|
|
4
5
|
// Nominal window length per kind — mirrors capacity-ledger's WINDOW_MS (kept here as a
|
|
@@ -464,6 +465,7 @@ export class AccountManager {
|
|
|
464
465
|
else this.scheduler.routingMode = 'sticky';
|
|
465
466
|
}
|
|
466
467
|
this._refreshAccessToken = dependencies.refreshAccessToken || refreshAccessToken;
|
|
468
|
+
this.poolUpSince = Date.now();
|
|
467
469
|
this.accounts = accounts.map((acct, index) => ({
|
|
468
470
|
index,
|
|
469
471
|
name: acct.name,
|
|
@@ -785,6 +787,19 @@ export class AccountManager {
|
|
|
785
787
|
}));
|
|
786
788
|
}
|
|
787
789
|
|
|
790
|
+
/** OUTSIDE-USAGE (2026-10-27): count every request the pool sends on a provider
|
|
791
|
+
* account, per UTC hour — the pool half of the outside-usage comparison. The
|
|
792
|
+
* map is bounded (last 25 hours) and in-memory only. */
|
|
793
|
+
notePoolSend(account, at = Date.now()) {
|
|
794
|
+
if (!account || account.type !== 'provider') return;
|
|
795
|
+
account.poolHourly ||= new Map();
|
|
796
|
+
const h = hourFloor(at);
|
|
797
|
+
account.poolHourly.set(h, (account.poolHourly.get(h) || 0) + 1);
|
|
798
|
+
if (account.poolHourly.size > 25) {
|
|
799
|
+
for (const k of [...account.poolHourly.keys()].sort().slice(0, account.poolHourly.size - 25)) account.poolHourly.delete(k);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
788
803
|
acquireAccount(requestInfo = {}, excludedIndexes = new Set()) {
|
|
789
804
|
this._noteRequestPolicy(requestInfo);
|
|
790
805
|
// Capture the session's prior account BEFORE selection, so the caller can tell
|
|
@@ -806,6 +821,7 @@ export class AccountManager {
|
|
|
806
821
|
account.inFlight++;
|
|
807
822
|
account.activeWeight += weight;
|
|
808
823
|
account.lastUsedAt = Date.now();
|
|
824
|
+
this.notePoolSend(account);
|
|
809
825
|
// Non-null only when this acquire moved the session off its prior account.
|
|
810
826
|
const migratedFromName = (prevCurrentName && account.name !== prevCurrentName) ? prevCurrentName : null;
|
|
811
827
|
return { account, weight, startedAt: Date.now(), upstreamThrottleProbe, migratedFromName };
|
|
@@ -4499,7 +4515,12 @@ export class AccountManager {
|
|
|
4499
4515
|
return this.accounts.map(a => {
|
|
4500
4516
|
const quota = {};
|
|
4501
4517
|
for (const f of PERSISTED_QUOTA_FIELDS) quota[f] = a.quota[f];
|
|
4502
|
-
|
|
4518
|
+
const out = { accountUuid: a.accountUuid, name: a.name, quota };
|
|
4519
|
+
// OUTSIDE-USAGE flag survives restart (red-team R1.2): a restart is exactly
|
|
4520
|
+
// when a leak's cards are most at risk — remediation rotates keys and
|
|
4521
|
+
// restarts the pool, and the in-memory flag would vanish mid-remediation.
|
|
4522
|
+
if (a.outsideUsage?.outside) out.outsideUsage = { ...a.outsideUsage };
|
|
4523
|
+
return out;
|
|
4503
4524
|
});
|
|
4504
4525
|
}
|
|
4505
4526
|
|
|
@@ -4533,6 +4554,12 @@ export class AccountManager {
|
|
|
4533
4554
|
}
|
|
4534
4555
|
// We already know this account's weekly window, so it isn't "probing".
|
|
4535
4556
|
if (account.quota.unified7dReset != null) account.probing = false;
|
|
4557
|
+
// OUTSIDE-USAGE restored: keep holding R7 and force the next sweep to
|
|
4558
|
+
// re-verify immediately (fresh poolHourly can only re-prove the leak).
|
|
4559
|
+
if (match.outsideUsage?.outside) {
|
|
4560
|
+
account.outsideUsage = { ...match.outsideUsage, stale: true };
|
|
4561
|
+
account._outsideCheckedAt = 0;
|
|
4562
|
+
}
|
|
4536
4563
|
}
|
|
4537
4564
|
}
|
|
4538
4565
|
|
|
@@ -4592,6 +4619,7 @@ export class AccountManager {
|
|
|
4592
4619
|
? new Date(Math.max(a.cooldownUntil || 0, a.provisionalUpstreamUntil || 0)).toISOString()
|
|
4593
4620
|
: null,
|
|
4594
4621
|
quota: { ...a.quota },
|
|
4622
|
+
outsideUsage: a.outsideUsage ? { ...a.outsideUsage } : null,
|
|
4595
4623
|
weekly: {
|
|
4596
4624
|
state: this._weeklyState(a),
|
|
4597
4625
|
rawState: this._weeklyRawState(a),
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// OUTSIDE-USAGE DETECTOR (2026-10-27).
|
|
2
|
+
//
|
|
3
|
+
// The pool knows every request IT sends per account; z.ai counts EVERY request
|
|
4
|
+
// against the account, including ones sent by anyone else holding the same API
|
|
5
|
+
// key. From 2026-08-10 to 2026-10-26 an outside holder of max+1's key burned its
|
|
6
|
+
// weekly quota and all three reset cards (final week: 45,155 z.ai calls vs 1,682
|
|
7
|
+
// pool sends) and nothing in the pool could see it — the owner spotted it by
|
|
8
|
+
// accident. This module compares, per account, z.ai's hourly call counts (monitor
|
|
9
|
+
// API, zero-spend) with the pool's own hourly sends, over the complete hours the
|
|
10
|
+
// pool has been running, and flags when the outside share dominates.
|
|
11
|
+
//
|
|
12
|
+
// Why REQUESTS per HOUR, not tokens per day: the pool's token ledger excludes
|
|
13
|
+
// cached input that z.ai bills, so even a clean account reads ~200x apart on
|
|
14
|
+
// tokens (measured: glm1 3.43B z.ai vs 15.6M ledger). Request counts on a clean
|
|
15
|
+
// account agree to ~1.1x (glm1, 2026-10-23..27). Counting only hours the pool was
|
|
16
|
+
// up means a restart or an update cutover can never manufacture a false alarm.
|
|
17
|
+
//
|
|
18
|
+
// z.ai's monitor labels hours in UTC+8 ("2026-10-24 12:00" = 04:00Z).
|
|
19
|
+
//
|
|
20
|
+
// Operating limits (red-team 2026-10-27, accepted):
|
|
21
|
+
// - ONE pool per key. A second maxpool sending on the same key IS outside traffic
|
|
22
|
+
// to this one and would trip the flag on both — share keys by giving each
|
|
23
|
+
// person their own account (the registry rule), not by running two pools.
|
|
24
|
+
// - z.ai only. Kimi exposes no per-hour call monitor; its keys are single-holder.
|
|
25
|
+
// - Detection, not prevention: a leak under 3x the pool's own volume, or one that
|
|
26
|
+
// started before the pool booted and has not yet produced 50 calls in complete
|
|
27
|
+
// hours since, is not flagged. The key-literal guard test and per-person keys
|
|
28
|
+
// are the prevention.
|
|
29
|
+
// - Failure is fail-SAFE: an unreadable/malformed monitor reply never clears a
|
|
30
|
+
// flag (it marks the reading stale, R7 keeps holding, the next sweep retries);
|
|
31
|
+
// the flag persists across restarts. Only a well-formed reading clears it.
|
|
32
|
+
|
|
33
|
+
export const OUTSIDE_FACTOR = 3; // z.ai > 3x pool over the window = outside traffic
|
|
34
|
+
export const OUTSIDE_MIN_CALLS = 50; // below this the window is noise
|
|
35
|
+
export const OUTSIDE_WINDOW_HOURS = 24; // compare at most the last 24 complete hours
|
|
36
|
+
export const OUTSIDE_CHECK_MS = 60 * 60_000; // re-check each account hourly
|
|
37
|
+
const HOUR = 3600_000;
|
|
38
|
+
const ZAI_TZ_OFFSET_MS = 8 * HOUR;
|
|
39
|
+
|
|
40
|
+
export const hourFloor = (ms) => Math.floor(ms / HOUR) * HOUR;
|
|
41
|
+
|
|
42
|
+
/** A z.ai model-usage payload we can trust: parallel non-empty arrays. A
|
|
43
|
+
* malformed/partial payload is treated as a FAILED read (never as "zero calls"),
|
|
44
|
+
* while a well-formed all-zero payload is real data — that is what lets a flag
|
|
45
|
+
* clear after the key is rotated and the outside traffic stops. */
|
|
46
|
+
export function isUsableZaiUsage(data) {
|
|
47
|
+
const xs = data?.x_time, cs = data?.modelCallCount;
|
|
48
|
+
return Array.isArray(xs) && Array.isArray(cs) && xs.length > 0 && xs.length === cs.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** z.ai model-usage `data` ({x_time:["YYYY-MM-DD HH:mm"...], modelCallCount:[...]})
|
|
52
|
+
* → Map(utcHourStartMs → calls). Unparseable rows are skipped. */
|
|
53
|
+
export function zaiHourlyCalls(data) {
|
|
54
|
+
const out = new Map();
|
|
55
|
+
const xs = data?.x_time, cs = data?.modelCallCount;
|
|
56
|
+
if (!Array.isArray(xs) || !Array.isArray(cs)) return out;
|
|
57
|
+
for (let i = 0; i < xs.length; i++) {
|
|
58
|
+
const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})$/.exec(String(xs[i]));
|
|
59
|
+
if (!m) continue;
|
|
60
|
+
const labelAsUtc = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]);
|
|
61
|
+
// Floor to the hour: z.ai labels are hour-aligned today, but folding a finer
|
|
62
|
+
// bucket (":30", a summary row) into its hour is correct either way, while
|
|
63
|
+
// keying on the raw label would silently DROP it (hourFloor lookups miss) and
|
|
64
|
+
// read a leaking account as 0.
|
|
65
|
+
const utc = Math.floor((labelAsUtc - ZAI_TZ_OFFSET_MS) / HOUR) * HOUR;
|
|
66
|
+
const c = Number(cs[i]) || 0;
|
|
67
|
+
out.set(utc, (out.get(utc) || 0) + c);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Compare z.ai's and the pool's hourly counts over the complete hours in
|
|
73
|
+
* [max(poolUpSince ceiled to the next hour, now - window), floor(now)).
|
|
74
|
+
* Returns {hours, zaiCalls, poolCalls, outside, from, to}; hours=0 when no
|
|
75
|
+
* complete hour is comparable yet (then outside is always false). */
|
|
76
|
+
export function compareOutsideUsage(zaiHours, poolHours, { now, poolUpSince, factor = OUTSIDE_FACTOR, minCalls = OUTSIDE_MIN_CALLS, windowHours = OUTSIDE_WINDOW_HOURS } = {}) {
|
|
77
|
+
const to = hourFloor(now);
|
|
78
|
+
const upFrom = Math.ceil(poolUpSince / HOUR) * HOUR; // first hour the pool saw in full
|
|
79
|
+
const from = Math.max(upFrom, to - windowHours * HOUR);
|
|
80
|
+
let zaiCalls = 0, poolCalls = 0, hours = 0;
|
|
81
|
+
for (let h = from; h < to; h += HOUR) {
|
|
82
|
+
hours++;
|
|
83
|
+
zaiCalls += zaiHours?.get(h) || 0;
|
|
84
|
+
poolCalls += poolHours?.get(h) || 0;
|
|
85
|
+
}
|
|
86
|
+
const outside = hours > 0 && zaiCalls >= minCalls && zaiCalls > poolCalls * factor;
|
|
87
|
+
return { hours, zaiCalls, poolCalls, outside, from, to };
|
|
88
|
+
}
|
package/src/prober.js
CHANGED
|
@@ -8,16 +8,17 @@
|
|
|
8
8
|
// This is the one sanctioned active-upstream feature; the proxy is otherwise passive.
|
|
9
9
|
|
|
10
10
|
import { fetchUsage, fetchProviderUsage } from './oauth.js';
|
|
11
|
-
import { listResetCards, redeemResetCard } from './zai-reset-cards.js';
|
|
11
|
+
import { listResetCards, redeemResetCard, fetchZaiModelUsage } from './zai-reset-cards.js';
|
|
12
12
|
import { listResetGrants, claimResetGrant } from './claude-reset-grants.js';
|
|
13
13
|
import { decideZai, decideClaude } from './reset-policy.js';
|
|
14
|
+
import { zaiHourlyCalls, compareOutsideUsage, isUsableZaiUsage, OUTSIDE_CHECK_MS } from './outside-usage.js';
|
|
14
15
|
|
|
15
16
|
// How often a subscription-latched account is rechecked (one probe + at most one
|
|
16
17
|
// token refresh per window — cheap, and bounded so a truly canceled plan isn't hammered).
|
|
17
18
|
export const SUB_RECHECK_MS = 30 * 60_000;
|
|
18
19
|
|
|
19
20
|
export class Prober {
|
|
20
|
-
constructor(accountManager, { intervalMs = 0, probeFn = fetchUsage, providerProbeFn = fetchProviderUsage, grantsFn = listResetGrants, timeoutMs = 10_000, log = console.log, usageGapMs = null } = {}) {
|
|
21
|
+
constructor(accountManager, { intervalMs = 0, probeFn = fetchUsage, providerProbeFn = fetchProviderUsage, grantsFn = listResetGrants, timeoutMs = 10_000, log = console.log, usageGapMs = null, zaiUsageFn = fetchZaiModelUsage, cardsFn = listResetCards, now = Date.now } = {}) {
|
|
21
22
|
this.am = accountManager;
|
|
22
23
|
// Injectable like probeFn: the default hits api.anthropic.com, so a harness that
|
|
23
24
|
// doesn't stub it makes a REAL network call per oauth sweep (a test then passes
|
|
@@ -38,6 +39,9 @@ export class Prober {
|
|
|
38
39
|
this._stopping = false;
|
|
39
40
|
this._pendingSleep = null; // canceller for an in-flight pacing sleep
|
|
40
41
|
this._sweepStart = 0; // rotates the per-sweep start account (fairness)
|
|
42
|
+
this.zaiUsageFn = zaiUsageFn; // z.ai hourly model-usage fetch (outside-usage detector)
|
|
43
|
+
this.cardsFn = cardsFn; // reset-card listing, injectable like the others
|
|
44
|
+
this._now = now; // clock (tests inject; defaults to the real one)
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
start() {
|
|
@@ -190,6 +194,7 @@ export class Prober {
|
|
|
190
194
|
const usage = await this._withTimeout(this.providerProbeFn(account));
|
|
191
195
|
if (!usage) return; // timed out — try again next cycle
|
|
192
196
|
this.am.applyProviderUsage(account.index, usage);
|
|
197
|
+
await this._checkOutsideUsage(account);
|
|
193
198
|
// Reset cards piggyback on the same poll cycle (z.ai only; zero-spend read).
|
|
194
199
|
// Failures are swallowed on purpose: cards are an enhancement, never a probe
|
|
195
200
|
// health signal — a card-list 401 must not turn the quota bars stale.
|
|
@@ -199,7 +204,7 @@ export class Prober {
|
|
|
199
204
|
if (account.enabled !== false && account.credential) {
|
|
200
205
|
if (account.provider === 'zai' && this.am.applyResetCards) {
|
|
201
206
|
try {
|
|
202
|
-
const cards = await this._withTimeout(
|
|
207
|
+
const cards = await this._withTimeout(this.cardsFn(account.credential));
|
|
203
208
|
if (cards) {
|
|
204
209
|
this.am.applyResetCards(account.index, cards);
|
|
205
210
|
const act = decideZai(account, cards);
|
|
@@ -211,6 +216,40 @@ export class Prober {
|
|
|
211
216
|
} catch { /* best-effort; never let a probe throw */ }
|
|
212
217
|
}
|
|
213
218
|
|
|
219
|
+
/** OUTSIDE-USAGE DETECTOR (2026-10-27), hourly per account: fetch z.ai's own
|
|
220
|
+
* hourly model-call counts for the account and compare with what the pool sent
|
|
221
|
+
* in the same complete hours. A divergent account (key used by someone else —
|
|
222
|
+
* measured 2026-08-10→10-26: 45,155 outside calls on max+1) is recorded on the
|
|
223
|
+
* account (reset-card policy R7 refuses to feed it) and logged ONCE per
|
|
224
|
+
* transition, never per sweep. Disabled accounts are never checked: someone
|
|
225
|
+
* else legitimately using the key is WHY the account is benched. */
|
|
226
|
+
async _checkOutsideUsage(account, now = this._now()) {
|
|
227
|
+
try {
|
|
228
|
+
if (account.provider !== 'zai' || !account.credential || account.enabled === false) return;
|
|
229
|
+
const last = account._outsideCheckedAt || 0;
|
|
230
|
+
if (now - last < OUTSIDE_CHECK_MS) return;
|
|
231
|
+
const data = await this._withTimeout(this.zaiUsageFn(account.credential, { now }));
|
|
232
|
+
if (!isUsableZaiUsage(data)) {
|
|
233
|
+
// FAILED or malformed read. Never downgrade a true flag on missing data (red-team R1.1:
|
|
234
|
+
// one 5xx un-protected a known leak and the SAME sweep redeemed a card into
|
|
235
|
+
// it). Keep the reading, mark it stale (R7 still holds), and RETRY next
|
|
236
|
+
// sweep — the hourly slot is only consumed by a successful read.
|
|
237
|
+
const prev = account.outsideUsage;
|
|
238
|
+
if (prev) account.outsideUsage = { ...prev, stale: true, checkedAt: now };
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
account._outsideCheckedAt = now; // stamp AFTER success (R1.5)
|
|
242
|
+
const zai = zaiHourlyCalls(data);
|
|
243
|
+
const res = compareOutsideUsage(zai, account.poolHourly, { now, poolUpSince: this.am.poolUpSince });
|
|
244
|
+
const prev = account.outsideUsage;
|
|
245
|
+
const wasOutside = Boolean(prev?.outside);
|
|
246
|
+
account.outsideUsage = { ...res, checkedAt: now };
|
|
247
|
+
if (res.outside && !wasOutside) {
|
|
248
|
+
this.log(`[outside-usage] ${account.name}: z.ai counted ${res.zaiCalls} calls in the last ${res.hours}h but the pool sent ${res.poolCalls} — someone else holds this key; reset cards held`);
|
|
249
|
+
}
|
|
250
|
+
} catch { /* detector is best-effort; never block the probe */ }
|
|
251
|
+
}
|
|
252
|
+
|
|
214
253
|
/** Execute a z.ai card redeem decided by the policy. Logs before/after.
|
|
215
254
|
* A FAILED redeem backs off exponentially per card (15m, 30m, 1h, cap 6h):
|
|
216
255
|
* 2026-10-22/23, a z.ai API change made every redeem fail "unavailable" and the
|
package/src/reset-policy.js
CHANGED
|
@@ -58,6 +58,7 @@ function msLeft(t, now) { return t == null ? Infinity : t - now; }
|
|
|
58
58
|
export function decideZai(account, cards, now = Date.now()) {
|
|
59
59
|
if (account?.enabled === false) return null; // R0
|
|
60
60
|
if (!cards || cards.error) return null;
|
|
61
|
+
if (account?.outsideUsage?.outside) return null; // R7: key used by someone else — a card would feed the leak
|
|
61
62
|
const q = account.quota || {};
|
|
62
63
|
const ses = q.providerSes ?? null;
|
|
63
64
|
const wk = q.weeklyAbsent ? null : (q.providerWk ?? null);
|
package/src/tui.js
CHANGED
|
@@ -2388,6 +2388,10 @@ export class TUI {
|
|
|
2388
2388
|
wkCell = q.providerWk != null ? bar(q.providerWk, bw, q.providerWkReset)
|
|
2389
2389
|
: emptyBar(q.weeklyAbsent ? 'none' : '—', bw);
|
|
2390
2390
|
note = this._probeHealthNote(a);
|
|
2391
|
+
// OUTSIDE-USAGE tag (2026-10-27): someone else is spending this account's quota
|
|
2392
|
+
// with the same key. Red and first, because it explains every other symptom on
|
|
2393
|
+
// the row (wall hit too fast, cards vanishing) and the fix is a key rotation.
|
|
2394
|
+
if (a.outsideUsage?.outside) note += ` ${red(`key used outside pool ×${Math.max(1, Math.round(a.outsideUsage.zaiCalls / Math.max(1, a.outsideUsage.poolCalls)))}`)}`;
|
|
2391
2395
|
// PEAK row tag (SC9): mirrors the router's own predicates — hard-barred = off,
|
|
2392
2396
|
// tier 2 = over cap, tier 1 = peak (or "cap n/a" when the weekly is unreadable,
|
|
2393
2397
|
// so the soft cap is inert). The label can never disagree with routing because
|
package/src/zai-reset-cards.js
CHANGED
|
@@ -131,3 +131,27 @@ export async function redeemResetCard(token, recordId, resetType, { grantType, a
|
|
|
131
131
|
return { ok: false, error: err.message || String(err) };
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
// z.ai's own hourly per-account model-call counts (zero-spend monitor read). The
|
|
136
|
+
// labels are UTC+8 ("YYYY-MM-DD HH:mm"); outside-usage.js converts. Consumed by
|
|
137
|
+
// the outside-usage detector (2026-10-27) — never by routing.
|
|
138
|
+
const MODEL_USAGE_URL = 'https://api.z.ai/api/monitor/usage/model-usage';
|
|
139
|
+
const zaiStamp = (ms) => new Date(ms + 8 * 3600_000).toISOString().slice(0, 19).replace('T', ' ');
|
|
140
|
+
|
|
141
|
+
/** Fetch the last `hours` (default 26) of z.ai hourly model-call counts for a key.
|
|
142
|
+
* Returns the raw `data` ({x_time, modelCallCount, ...}) or null on any failure. */
|
|
143
|
+
export async function fetchZaiModelUsage(token, { hours = 26, now = Date.now(), signal, fetchImpl = fetch } = {}) {
|
|
144
|
+
if (!token) return null;
|
|
145
|
+
try {
|
|
146
|
+
const q = new URLSearchParams({ startTime: zaiStamp(now - hours * 3600_000), endTime: zaiStamp(now) });
|
|
147
|
+
const res = await fetchImpl(`${MODEL_USAGE_URL}?${q}`, {
|
|
148
|
+
headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json', 'Accept-Language': 'en-US' },
|
|
149
|
+
signal,
|
|
150
|
+
});
|
|
151
|
+
if (!res.ok) return null;
|
|
152
|
+
const data = await res.json();
|
|
153
|
+
return data?.code === 200 && data?.data ? data.data : null;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|