maxpool 1.24.28 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.24.28",
3
+ "version": "1.24.29",
4
4
  "description": "Multi-account Claude Code proxy with adaptive, rate-aware load balancing across Claude accounts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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.log(`[reset-card] ${account.name}: redeem FAILED card ${act.card.recordId}: ${r?.error}`);
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
  }
@@ -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 fetch(LIST_URL, {
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
- const res = await fetch(USE_URL, {
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);