maxpool 1.5.35 → 1.5.36

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +26 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.5.35",
3
+ "version": "1.5.36",
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/server.js CHANGED
@@ -25,6 +25,11 @@ const UPSTREAM_TTFB_MS = Math.max(5_000, Number(process.env.MAXPOOL_TTFB_MS) ||
25
25
  const STREAM_IDLE_MS = Math.max(30_000, Number(process.env.MAXPOOL_STREAM_IDLE_MS) || 300_000); // max gap BETWEEN streamed chunks (reset per chunk)
26
26
  const UPSTREAM_BODY_MS = Math.max(30_000, Number(process.env.MAXPOOL_BODY_MS) || 300_000); // non-streaming body read
27
27
  const CLIENT_DRAIN_MS = Math.max(5_000, Number(process.env.MAXPOOL_DRAIN_MS) || 60_000); // max wait for a backpressured client to drain (half-open client → free the lease)
28
+ // A provider 403 is (unlike a 401) almost always transient QUOTA/PLAN exhaustion — cool
29
+ // the provider down RECOVERABLY for this window, then re-probe, instead of permanently
30
+ // disabling it. Short (not reset-length) so a provider-pinned session's hold window
31
+ // isn't blown, and a still-exhausted provider just re-benches on the next single retry.
32
+ const PROVIDER_FORBIDDEN_COOLDOWN_SEC = Math.max(60, Number(process.env.MAXPOOL_PROVIDER_403_COOLDOWN_SEC) || 1800);
28
33
  // Backstop reaper idle ceiling. Floored WELL above the longest a legit NON-streaming
29
34
  // request can go silent (no heartbeat there): TTFB 120s + nonStreamMaxWaitMs 300s +
30
35
  // UPSTREAM_BODY_MS 300s ≈ 720s — so a slow-but-alive non-streaming request is never
@@ -84,6 +89,11 @@ export function createProxyServer(accountManager, config, hooks = {}) {
84
89
 
85
90
  const server = http.createServer(async (req, res) => {
86
91
  try {
92
+ // Disable Nagle on the client socket: flush each SSE event immediately instead
93
+ // of coalescing small writes into bursts. Smooths streaming cadence toward the
94
+ // HTTP/2-direct shape, reducing how often a client terminal's re-render churns
95
+ // on long streams. Same underlying socket as res, so this covers the write path.
96
+ try { req.socket?.setNoDelay(true); } catch { /* socket may already be gone */ }
87
97
  try { res.setHeader('x-maxpool-worker', workerStamp); } catch { /* headers sent */ }
88
98
  if (draining) {
89
99
  try { res.setHeader('Connection', 'close'); } catch { /* headers may be sent */ }
@@ -654,16 +664,27 @@ async function forwardRequest(
654
664
 
655
665
  if (account.type === 'provider' && isProviderAuthStatus(upstreamRes.status)) {
656
666
  const errorBody = await readErrorBody(upstreamRes);
657
- const reason = upstreamRes.status === 401 ? 'auth_failed' : 'forbidden';
658
- accountManager.markAuthFailed(account.index, upstreamRes.status, reason);
659
- accountManager.releaseAccount(lease, { status: upstreamRes.status, error: reason });
667
+ const forbidden = upstreamRes.status === 403;
668
+ if (forbidden) {
669
+ // 403 = almost always QUOTA/PLAN exhaustion (e.g. Kimi Coding-Plan weekly maxed),
670
+ // NOT bad credentials — the usage probe still succeeds with the same key. Cool it
671
+ // down RECOVERABLY (rides the rate-limit recovery in _isAvailable → auto-un-benches
672
+ // at expiry) instead of the permanent auth-disable that stranded it until restart.
673
+ // `neutral` release so a transient quota-403 doesn't bump failure/scoring counters.
674
+ accountManager.markRateLimited(account.index, PROVIDER_FORBIDDEN_COOLDOWN_SEC, { status: 403, recordFailure: false });
675
+ accountManager.releaseAccount(lease, { neutral: true });
676
+ } else {
677
+ // 401 = genuinely bad/missing credentials → permanent disable (needs re-auth).
678
+ accountManager.markAuthFailed(account.index, upstreamRes.status, 'auth_failed');
679
+ accountManager.releaseAccount(lease, { status: upstreamRes.status, error: 'auth_failed' });
680
+ }
660
681
  excludedIndexes.add(account.index);
661
682
 
662
683
  if (logDir) {
663
- logSections.push(`=== RESPONSE ${upstreamRes.status} — "${account.name}" disabled (${reason}), failing over ===\n${formatHeaders(upstreamRes.headers)}`);
684
+ logSections.push(`=== RESPONSE ${upstreamRes.status} — "${account.name}" ${forbidden ? `cooled down ${PROVIDER_FORBIDDEN_COOLDOWN_SEC}s (quota/forbidden)` : 'disabled (auth)'}, failing over ===\n${formatHeaders(upstreamRes.headers)}`);
664
685
  if (errorBody) logSections.push(`=== ERROR BODY ===\n${errorBody}`);
665
686
  }
666
- console.log(`[Maxpool] ${upstreamRes.status} on provider "${account.name}" — disabled and failing over before first byte`);
687
+ console.log(`[Maxpool] ${upstreamRes.status} on provider "${account.name}" — ${forbidden ? `cooled down ${PROVIDER_FORBIDDEN_COOLDOWN_SEC}s (recoverable)` : 'disabled'} and failing over before first byte`);
667
688
 
668
689
  if (
669
690
  canRetryBufferedBody &&