maxpool 1.0.1 → 1.0.3

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.0.1",
3
+ "version": "1.0.3",
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/config.js CHANGED
@@ -36,6 +36,12 @@ export function createDefaultConfig() {
36
36
  apiKey: 'mp-' + randomBytes(24).toString('base64url'),
37
37
  },
38
38
  upstream: 'https://api.anthropic.com',
39
+ // Per-account "stop using this account" gate, applied to BOTH the 5h
40
+ // session window and the 7d weekly window (whichever utilization is
41
+ // higher). 0.90 = stop routing to an account once it crosses 90% of a
42
+ // window, leaving a 10% safety margin so it is never hard-limited (429).
43
+ // Raise toward 0.97 to squeeze more out of accounts before rotating
44
+ // (less margin, slightly higher 429 risk); lower to rotate more eagerly.
39
45
  switchThreshold: 0.90,
40
46
  quotaProbeSeconds: 0, // background quota probe; 0 = off (opt-in)
41
47
  routing: {
@@ -48,11 +54,28 @@ export function createDefaultConfig() {
48
54
  safetyMaxGlobalActive: 150,
49
55
  cooldownMs: 30_000,
50
56
  maxCooldownMs: 15 * 60_000,
57
+ // Weekly (7d) quota tiers — how aggressively to de-prioritise an account
58
+ // as its weekly usage climbs. Each is a fraction (0..1) of the weekly
59
+ // limit. Below soft = full speed; soft..reserve = mild penalty;
60
+ // reserve..critical = heavy penalty; above exhausted = effectively
61
+ // parked until the weekly window resets. Tune to trade burst capacity
62
+ // against weekly-limit safety.
63
+ weeklySoftThreshold: 0.65,
64
+ weeklyReserveThreshold: 0.85,
65
+ weeklyCriticalThreshold: 0.95,
66
+ weeklyExhaustedThreshold: 0.985,
51
67
  },
52
68
  retry: {
53
69
  maxAttemptsPerRequest: 0,
54
70
  maxRetryBufferBytes: 10 * 1024 * 1024,
55
71
  },
72
+ // On quit (q / Ctrl-C / SIGTERM): stop accepting new requests, give
73
+ // in-flight requests up to drainTimeoutMs to finish, then force-exit.
74
+ // A second signal forces an immediate exit. Kept short so quit works
75
+ // under a continuous request flood instead of hanging.
76
+ shutdown: {
77
+ drainTimeoutMs: 15_000,
78
+ },
56
79
  queue: {
57
80
  enabled: true,
58
81
  maxWaitMs: 24 * 60 * 60 * 1000,
package/src/index.js CHANGED
@@ -193,7 +193,11 @@ async function serverWorkerCommand() {
193
193
  let syncTimer = null;
194
194
  let draining = false;
195
195
  let restartController = null;
196
- const drainTimeoutMs = Math.max(1000, Number(config.shutdown?.drainTimeoutMs) || 10 * 60_000);
196
+ // Quit drains in-flight requests, then force-exits. Kept short so a single
197
+ // 'q' / Ctrl-C / SIGTERM actually quits under a continuous request flood
198
+ // (where there are always active requests) instead of waiting indefinitely.
199
+ // A second signal forces an immediate exit. Override via config.shutdown.drainTimeoutMs.
200
+ const drainTimeoutMs = Math.max(1000, Number(config.shutdown?.drainTimeoutMs) || 15_000);
197
201
  const hooks = {
198
202
  onRequestStart: (id, info) => {
199
203
  const accepted = restartController.requestStarted(id);
@@ -242,7 +246,7 @@ async function serverWorkerCommand() {
242
246
  if (tui?.running) tui.stop();
243
247
 
244
248
  console.log(`\n[Maxpool] Draining shutdown (${reason}).`);
245
- console.log(`[Maxpool] Stopped accepting new requests; waiting for ${restartController.activeRequests.size} active request(s). Press Ctrl-C again to force.`);
249
+ console.log(`[Maxpool] Stopped accepting new requests; waiting up to ${Math.ceil(drainTimeoutMs / 1000)}s for ${restartController.activeRequests.size} active request(s), then forcing exit. Press Ctrl-C again to force now.`);
246
250
 
247
251
  let done = false;
248
252
  let reportTimer = null;
package/src/server.js CHANGED
@@ -846,7 +846,7 @@ function unavailableMessage(accountManager, requestInfo = {}, retryAfter, willRe
846
846
  return `All ${n} accounts exhausted. Retry in ${retryAfter}s.`;
847
847
  }
848
848
 
849
- export const __serverTest = { unavailableMessage, isRetriableUpstreamStatus };
849
+ export const __serverTest = { unavailableMessage, isRetriableUpstreamStatus, headerValue, getMaxpoolProfile };
850
850
 
851
851
  async function readErrorBody(upstreamRes, limitBytes = 64 * 1024) {
852
852
  if (!upstreamRes.body) return '';
@@ -1227,7 +1227,8 @@ function containsThinkingBlock(value) {
1227
1227
  }
1228
1228
 
1229
1229
  function getMaxpoolProfile(headers) {
1230
- const profile = String(headers['x-maxpool-profile'] || 'claude').trim().toLowerCase();
1230
+ // headerValue() handles the x-teamclaude-* legacy fallback.
1231
+ const profile = String(headerValue(headers, 'x-maxpool-profile') || 'claude').trim().toLowerCase();
1231
1232
  return profile || 'claude';
1232
1233
  }
1233
1234
 
@@ -1272,7 +1273,15 @@ function prepareRuntimeProviders(accountManager, headers) {
1272
1273
  }
1273
1274
 
1274
1275
  function headerValue(headers, name) {
1275
- const value = headers[name.toLowerCase()];
1276
+ const lname = name.toLowerCase();
1277
+ let value = headers[lname];
1278
+ // Backward compatibility: sessions launched before the teamclaude→maxpool
1279
+ // rename send x-teamclaude-* headers (a process's ANTHROPIC_CUSTOM_HEADERS is
1280
+ // fixed at launch). Fall back to the legacy name so already-running sessions
1281
+ // keep full routing/fallback without needing a restart.
1282
+ if ((value == null || value === '') && lname.startsWith('x-maxpool-')) {
1283
+ value = headers['x-teamclaude-' + lname.slice('x-maxpool-'.length)];
1284
+ }
1276
1285
  if (Array.isArray(value)) return value[0];
1277
1286
  return value ? String(value).trim() : '';
1278
1287
  }