maxpool 1.24.27 → 1.24.29
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/prober.js +20 -3
- package/src/zai-reset-cards.js +16 -10
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/prober.js
CHANGED
|
@@ -211,20 +211,37 @@ export class Prober {
|
|
|
211
211
|
} catch { /* best-effort; never let a probe throw */ }
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
/** Execute a z.ai card redeem decided by the policy. Logs before/after.
|
|
214
|
+
/** Execute a z.ai card redeem decided by the policy. Logs before/after.
|
|
215
|
+
* A FAILED redeem backs off exponentially per card (15m, 30m, 1h, cap 6h):
|
|
216
|
+
* 2026-10-22/23, a z.ai API change made every redeem fail "unavailable" and the
|
|
217
|
+
* 2-minute poll retried the same card 519 times over 16h — the exact
|
|
218
|
+
* rapid-retry shape z.ai's usage-risk notice flagged before. A success resets
|
|
219
|
+
* the backoff; the idempotent requestId keeps any overlapping attempt single-spend. */
|
|
215
220
|
async _redeemZaiCard(account, act) {
|
|
216
221
|
try {
|
|
222
|
+
const key = `${account.index}:${act.card.recordId}`;
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
const until = this._cardRetryUntil?.get(key);
|
|
225
|
+
if (until && until > now) return; // still backing off
|
|
217
226
|
const q = account.quota || {};
|
|
218
227
|
this.log(`[reset-card] ${account.name}: redeeming ${act.resetType} card ${act.card.recordId} (${act.reason}; ses=${q.providerSes ?? '?'} wk=${q.providerWk ?? '?'})`);
|
|
219
|
-
const r = await this._withTimeout(redeemResetCard(account.credential, act.card.recordId, act.resetType));
|
|
228
|
+
const r = await this._withTimeout(redeemResetCard(account.credential, act.card.recordId, act.resetType, { grantType: act.card.grantType }));
|
|
220
229
|
if (r?.ok) {
|
|
230
|
+
if (!this._cardRetryUntil) this._cardRetryUntil = new Map();
|
|
231
|
+
this._cardRetryUntil.set(key, 0); // success clears any backoff
|
|
221
232
|
this.log(`[reset-card] ${account.name}: card ${act.card.recordId} redeemed`);
|
|
222
233
|
if (this.am.applyResetCards) {
|
|
223
234
|
const fresh = await this._withTimeout(listResetCards(account.credential));
|
|
224
235
|
if (fresh) this.am.applyResetCards(account.index, fresh);
|
|
225
236
|
}
|
|
226
237
|
} else {
|
|
227
|
-
this.
|
|
238
|
+
const tries = (this._cardTries?.get(key) || 0) + 1;
|
|
239
|
+
if (!this._cardTries) this._cardTries = new Map();
|
|
240
|
+
this._cardTries.set(key, tries);
|
|
241
|
+
const waitMs = Math.min(6 * 3600_000, 900_000 * 2 ** (tries - 1));
|
|
242
|
+
if (!this._cardRetryUntil) this._cardRetryUntil = new Map();
|
|
243
|
+
this._cardRetryUntil.set(key, now + waitMs);
|
|
244
|
+
this.log(`[reset-card] ${account.name}: redeem FAILED card ${act.card.recordId} (try ${tries}, next attempt in ${Math.round(waitMs / 60000)}min): ${r?.error}`);
|
|
228
245
|
}
|
|
229
246
|
} catch (e) { this.log(`[reset-card] ${account.name}: redeem error ${e.message}`); }
|
|
230
247
|
}
|
package/src/zai-reset-cards.js
CHANGED
|
@@ -44,7 +44,7 @@ export function parseCardExpiry(s) {
|
|
|
44
44
|
return Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]) - 8 * 3600_000;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** Normalize a raw card array -> [{recordId, expiresAt, expired}], available only. */
|
|
47
|
+
/** Normalize a raw card array -> [{recordId, grantType, expiresAt, expired}], available only. */
|
|
48
48
|
function normalizeCards(arr, now = Date.now()) {
|
|
49
49
|
const out = [];
|
|
50
50
|
for (const c of (Array.isArray(arr) ? arr : [])) {
|
|
@@ -52,7 +52,7 @@ function normalizeCards(arr, now = Date.now()) {
|
|
|
52
52
|
const recordId = Number(c.recordId);
|
|
53
53
|
if (!Number.isFinite(recordId)) continue;
|
|
54
54
|
const expiresAt = parseCardExpiry(c.expireTime);
|
|
55
|
-
out.push({ recordId, expiresAt, expired: expiresAt != null && expiresAt <= now });
|
|
55
|
+
out.push({ recordId, grantType: typeof c.grantType === 'string' ? c.grantType : undefined, expiresAt, expired: expiresAt != null && expiresAt <= now });
|
|
56
56
|
}
|
|
57
57
|
// Soonest-expiring first — that's the one to spend first.
|
|
58
58
|
out.sort((a, b) => (a.expiresAt ?? Infinity) - (b.expiresAt ?? Infinity));
|
|
@@ -61,10 +61,10 @@ function normalizeCards(arr, now = Date.now()) {
|
|
|
61
61
|
|
|
62
62
|
/** List this account's available reset cards. Zero-spend read.
|
|
63
63
|
* Returns { fiveHour: [], weekly: [] } | { error }. */
|
|
64
|
-
export async function listResetCards(token, { signal } = {}) {
|
|
64
|
+
export async function listResetCards(token, { signal, fetchImpl = fetch } = {}) {
|
|
65
65
|
if (!token) return { error: 'no token' };
|
|
66
66
|
try {
|
|
67
|
-
const res = await
|
|
67
|
+
const res = await fetchImpl(LIST_URL, {
|
|
68
68
|
headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' },
|
|
69
69
|
signal,
|
|
70
70
|
});
|
|
@@ -93,19 +93,25 @@ function requestIdFor(recordId) {
|
|
|
93
93
|
|
|
94
94
|
/** Redeem one card. NEVER retries on the wire here (the idempotency key makes a
|
|
95
95
|
* caller-level retry safe). Returns { ok:true, recordId } | { ok:false, error, code }. */
|
|
96
|
-
export async function redeemResetCard(token, recordId, resetType, { signal } = {}) {
|
|
96
|
+
export async function redeemResetCard(token, recordId, resetType, { grantType, signal, fetchImpl = fetch } = {}) {
|
|
97
97
|
if (resetType !== 'WEEK' && resetType !== 'FIVE_HOUR') {
|
|
98
98
|
return { ok: false, error: `invalid resetType ${resetType}` };
|
|
99
99
|
}
|
|
100
100
|
if (!Number.isFinite(recordId)) return { ok: false, error: 'invalid recordId' };
|
|
101
101
|
try {
|
|
102
|
-
|
|
102
|
+
// grantType is REQUIRED (2026-10-23): z.ai started rejecting card redeems without
|
|
103
|
+
// it ("The specified reset attempt is unavailable") — their web UI sends
|
|
104
|
+
// {targetType, resetType, recordId, requestId, grantType:'GLOBAL'}. A bogus
|
|
105
|
+
// grantType answers "Unsupported coupon grant type" without spending anything.
|
|
106
|
+
const body = {
|
|
107
|
+
recordId, targetType: 'PERSONAL', resetType,
|
|
108
|
+
requestId: requestIdFor(recordId),
|
|
109
|
+
};
|
|
110
|
+
if (grantType) body.grantType = grantType;
|
|
111
|
+
const res = await fetchImpl(USE_URL, {
|
|
103
112
|
method: 'POST',
|
|
104
113
|
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
105
|
-
body: JSON.stringify(
|
|
106
|
-
recordId, targetType: 'PERSONAL', resetType,
|
|
107
|
-
requestId: requestIdFor(recordId),
|
|
108
|
-
}),
|
|
114
|
+
body: JSON.stringify(body),
|
|
109
115
|
signal,
|
|
110
116
|
});
|
|
111
117
|
const data = await res.json().catch(() => null);
|