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.
- package/package.json +1 -1
- package/src/server.js +26 -5
package/package.json
CHANGED
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
|
|
658
|
-
|
|
659
|
-
|
|
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}"
|
|
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 &&
|