maxpool 1.18.0 → 1.19.1
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 +35 -19
- package/src/server.js +74 -2
package/package.json
CHANGED
package/src/account-manager.js
CHANGED
|
@@ -170,16 +170,18 @@ const DEFAULT_SCHEDULER = {
|
|
|
170
170
|
// account sat at ~17% of fleet traffic (measured 2026-08-25) while four Claude
|
|
171
171
|
// accounts ran at weekly 1.0.
|
|
172
172
|
//
|
|
173
|
-
// The discount is a MULTIPLIER on
|
|
174
|
-
// bonus would drive an idle account's total NEGATIVE
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
// (
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
173
|
+
// The discount is a MULTIPLIER on the BALANCING terms — never a flat bonus. A flat
|
|
174
|
+
// bonus would drive an idle account's total NEGATIVE, below the entire band structure
|
|
175
|
+
// that reserveFloorCost:5 / criticalPressureCost:21 assume is non-negative. A
|
|
176
|
+
// multiplier in [0,1] cannot: it only ever REMOVES cost that is already there.
|
|
177
|
+
// Term set as of 2026-08-29 (v1.19.0): utilization + pace (original, 2026-08-25),
|
|
178
|
+
// spread (v1.18.0), and the in-flight terms — linear concurrency + capPenalty
|
|
179
|
+
// (v1.19.0). The in-flight extension exists because live requests run 18-27s at
|
|
180
|
+
// weight 10-50, making in-flight the marginal price of traffic: measured
|
|
181
|
+
// 2026-08-28, spread-only left the unlimited account at parity (0.24x share),
|
|
182
|
+
// defeating the owner's approved outcome. Reserve/critical/ramp/failure costs stay
|
|
183
|
+
// undiscounted, as do the hard gates (safetyMaxActivePerAccount, usage cap,
|
|
184
|
+
// cooldowns) — those are the anti-dogpile backstops, not price signals.
|
|
183
185
|
fastRefillDiscount: 0.6, // 0 = off (full cost), 0.6 = discount up to 60% of the two balancing terms
|
|
184
186
|
fastRefillFadeUtil: 0.65, // discount reaches 0 at this session utilization (weeklySoftThreshold)
|
|
185
187
|
recoveryRampWeight: 4, // decaying penalty applied to a just-recovered account
|
|
@@ -2542,11 +2544,20 @@ export class AccountManager {
|
|
|
2542
2544
|
const now = ctx?.now ?? Date.now();
|
|
2543
2545
|
const reqWeight = Math.max(1, requestInfo.weight || 1);
|
|
2544
2546
|
const inflight = account.activeWeight + reqWeight;
|
|
2547
|
+
const refillMult = this._fastRefillMultiplier(account);
|
|
2545
2548
|
|
|
2546
2549
|
// DOMINANT term: in-flight concurrency. Short-term throttling is driven by
|
|
2547
2550
|
// how many requests pile on one account, so least-loaded-first spread is
|
|
2548
2551
|
// the primary objective.
|
|
2549
|
-
|
|
2552
|
+
// FAST-REFILL (2026-08-29 extension): in the LIVE regime this term (plus the
|
|
2553
|
+
// capPenalty below) is what actually sets the equilibrium share — requests
|
|
2554
|
+
// run 18-27s at weight 10-50, so in-flight dwarfs every balancing term.
|
|
2555
|
+
// Measured 2026-08-28 post-v1.18.0: a spread-only discount left the
|
|
2556
|
+
// unlimited account at parity (12 of 56 GLM switches, share 0.24x). The
|
|
2557
|
+
// linear term is BALANCING (it shares load), so it carries the discount;
|
|
2558
|
+
// the steep past-D floor, the hard request gate, cooldowns and failure
|
|
2559
|
+
// backoff are the anti-dogpile machinery and are handled below.
|
|
2560
|
+
const concurrency = inflight * this.scheduler.concurrencyWeight * refillMult;
|
|
2550
2561
|
|
|
2551
2562
|
// Steep soft cap past depth D — the throttle safety floor. No single
|
|
2552
2563
|
// account absorbs a deep concurrent burst no matter how "cheap" it looks. A
|
|
@@ -2560,15 +2571,21 @@ export class AccountManager {
|
|
|
2560
2571
|
const concTarget = (weeklyState === 'reserve' || (weeklyState === 'critical' && criticalUnlock))
|
|
2561
2572
|
? this.scheduler.reserveConcurrencyTarget
|
|
2562
2573
|
: this.scheduler.perAccountConcurrencyTarget;
|
|
2574
|
+
// FAST-REFILL (2026-08-29): carries the discount for the same reason as the
|
|
2575
|
+
// linear term — at live weights every account sits past D, so the marginal
|
|
2576
|
+
// in-flight price IS capPenalty; discounting one without the other changes
|
|
2577
|
+
// nothing. The floor stays STEEP in absolute terms (≥ capPenaltyWeight*mult
|
|
2578
|
+
// per unit, still >2x the largest balancing term at the max discount) and
|
|
2579
|
+
// the hard per-account request gate (safetyMaxActivePerAccount), cooldowns
|
|
2580
|
+
// and failurePenalty remain undiscounted backstops.
|
|
2563
2581
|
const capPenalty = this.scheduler.capPenaltyWeight
|
|
2564
|
-
* Math.max(0, inflight - concTarget);
|
|
2582
|
+
* Math.max(0, inflight - concTarget) * refillMult;
|
|
2565
2583
|
|
|
2566
2584
|
// Burn-pace COST only (demoted from the old dominant scarcity×6 term): a
|
|
2567
2585
|
// soft de-preference of accounts burning ahead of an even pace. Never a bench.
|
|
2568
|
-
// FAST-REFILL DISCOUNT: applied to the pace and utilization terms
|
|
2569
|
-
//
|
|
2570
|
-
//
|
|
2571
|
-
const refillMult = this._fastRefillMultiplier(account);
|
|
2586
|
+
// FAST-REFILL DISCOUNT: applied to the pace and utilization terms for an
|
|
2587
|
+
// account whose only cap is a fast-refilling session window — see the
|
|
2588
|
+
// DEFAULT_SCHEDULER block for the full rationale (refillMult computed above).
|
|
2572
2589
|
const paceCost = this._accountScarcity(account, now) * this.scheduler.paceCostWeight * refillMult;
|
|
2573
2590
|
|
|
2574
2591
|
// RAW utilization cost — direct, not pace-adjusted. The pace cost above discounts
|
|
@@ -2601,9 +2618,8 @@ export class AccountManager {
|
|
|
2601
2618
|
// unused every 5h. Discounted, the equilibrium share of a weeklyAbsent account is
|
|
2602
2619
|
// ~mult/(1-disc*(1-share)) of a sibling's — at the default 0.6 discount roughly
|
|
2603
2620
|
// 2.3x early in its window, fading to parity at the same ses 0.65 the multiplier
|
|
2604
|
-
// already uses.
|
|
2605
|
-
//
|
|
2606
|
-
// never applied to concurrency/capPenalty/reserve/critical.
|
|
2621
|
+
// already uses. A multiplier of 1 makes every discounted term byte-identical to
|
|
2622
|
+
// pre-2026-08-25 behaviour; reserve/critical/ramp/failure are never discounted.
|
|
2607
2623
|
const spread = share * this.scheduler.spreadShareWeight * refillMult;
|
|
2608
2624
|
|
|
2609
2625
|
const ramp = this._recoveryRamp(account, now);
|
package/src/server.js
CHANGED
|
@@ -2856,6 +2856,21 @@ function startIdleRequestReaper(res, reqId, idleMs, { now = Date.now, setInterva
|
|
|
2856
2856
|
return timer;
|
|
2857
2857
|
}
|
|
2858
2858
|
|
|
2859
|
+
|
|
2860
|
+
function concatUint8(chunks) {
|
|
2861
|
+
let n = 0;
|
|
2862
|
+
for (const c of chunks) n += c.length;
|
|
2863
|
+
const out = new Uint8Array(n);
|
|
2864
|
+
let o = 0;
|
|
2865
|
+
for (const c of chunks) { out.set(c, o); o += c.length; }
|
|
2866
|
+
return out;
|
|
2867
|
+
}
|
|
2868
|
+
function totalLen(chunks) {
|
|
2869
|
+
let n = 0;
|
|
2870
|
+
for (const c of chunks) n += c.length;
|
|
2871
|
+
return n;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2859
2874
|
async function streamResponse(webStream, res, status, responseHeaders, accountIndex, accountManager, streamLog, requestInfo = {}, idleMs = STREAM_IDLE_MS) {
|
|
2860
2875
|
const reader = webStream.getReader();
|
|
2861
2876
|
const decoder = new TextDecoder();
|
|
@@ -2880,6 +2895,10 @@ async function streamResponse(webStream, res, status, responseHeaders, accountIn
|
|
|
2880
2895
|
const onClose = () => { reader.cancel().catch(() => {}); };
|
|
2881
2896
|
res.once('close', onClose);
|
|
2882
2897
|
|
|
2898
|
+
// MODEL ECHO NORMALIZATION state (rationale at the write site).
|
|
2899
|
+
let modelEchoPending = true;
|
|
2900
|
+
let modelEchoBuffer = null;
|
|
2901
|
+
|
|
2883
2902
|
try {
|
|
2884
2903
|
while (true) {
|
|
2885
2904
|
// Idle guard: a half-open upstream (headers, then silence, never closes) would
|
|
@@ -2915,10 +2934,44 @@ async function streamResponse(webStream, res, status, responseHeaders, accountIn
|
|
|
2915
2934
|
committed = true;
|
|
2916
2935
|
}
|
|
2917
2936
|
|
|
2937
|
+
// MODEL ECHO NORMALIZATION (2026-08-31): provider upstreams echo their own model
|
|
2938
|
+
// id ("glm-5.3", kimi-*) in message_start, and Claude Code persists that id into
|
|
2939
|
+
// the session transcript — on resume the id fails model-family resolution and the
|
|
2940
|
+
// session prints "Session model ... could not be restored (not a model this
|
|
2941
|
+
// version of Claude Code recognizes)" (278 sessions affected, measured).
|
|
2942
|
+
// Rewrite the model field back to the CLIENT'S requested model (requestInfo.model,
|
|
2943
|
+
// captured before the per-account rewrite) inside the first complete SSE event
|
|
2944
|
+
// carrying a "model" key; afterwards chunks pass through untouched. ReadableStream
|
|
2945
|
+
// chunks are Uint8Array — Buffer.concat/toString would yield comma-joined byte
|
|
2946
|
+
// numbers (caught by the normalization tests), so hold an array of chunks and
|
|
2947
|
+
// concat with decoder-safe helpers.
|
|
2948
|
+
let out = value;
|
|
2949
|
+
if (requestInfo?.model && modelEchoPending) {
|
|
2950
|
+
modelEchoBuffer = modelEchoBuffer ? [...modelEchoBuffer, value] : [value];
|
|
2951
|
+
const s = decoder.decode(concatUint8(modelEchoBuffer));
|
|
2952
|
+
if (s.includes('"model"') && s.includes('\n\n')) {
|
|
2953
|
+
const normalized = s.replace(
|
|
2954
|
+
/("model":")[^"]+(")/,
|
|
2955
|
+
`$1${requestInfo.model.replace(/["\\]/g, '\\$&')}$2`,
|
|
2956
|
+
);
|
|
2957
|
+
out = Buffer.from(normalized, 'utf8');
|
|
2958
|
+
modelEchoBuffer = null;
|
|
2959
|
+
modelEchoPending = false;
|
|
2960
|
+
} else if (totalLen(modelEchoBuffer) > 64 * 1024) {
|
|
2961
|
+
// Pathological upstream: 64KB with no complete model-bearing event. Flush
|
|
2962
|
+
// verbatim — the warning is the worst outcome of a missed rewrite.
|
|
2963
|
+
out = Buffer.from(concatUint8(modelEchoBuffer));
|
|
2964
|
+
modelEchoBuffer = null;
|
|
2965
|
+
modelEchoPending = false;
|
|
2966
|
+
} else {
|
|
2967
|
+
continue; // hold until the model-bearing event is complete
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2918
2971
|
// Forward chunk immediately
|
|
2919
|
-
const ok = res.write(
|
|
2972
|
+
const ok = res.write(out);
|
|
2920
2973
|
|
|
2921
|
-
const text = decoder.decode(
|
|
2974
|
+
const text = decoder.decode(out, { stream: true });
|
|
2922
2975
|
|
|
2923
2976
|
// Capture for logging
|
|
2924
2977
|
if (streamLog) streamLog.push(text);
|
|
@@ -2974,6 +3027,25 @@ async function streamResponse(webStream, res, status, responseHeaders, accountIn
|
|
|
2974
3027
|
readFailed = true;
|
|
2975
3028
|
throw err;
|
|
2976
3029
|
} finally {
|
|
3030
|
+
// MODEL ECHO NORMALIZATION last-resort flush: the read loop can exit via done,
|
|
3031
|
+
// error, or client disconnect while chunks are still HELD for the rewrite.
|
|
3032
|
+
// Whatever the exit path, deliver the held bytes and accrue their usage — a
|
|
3033
|
+
// mid-flight death must not swallow delivered tokens (H4) nor truncate the
|
|
3034
|
+
// body (empty '' responses, C1).
|
|
3035
|
+
if (modelEchoBuffer) {
|
|
3036
|
+
const held = Buffer.from(concatUint8(modelEchoBuffer));
|
|
3037
|
+
modelEchoBuffer = null;
|
|
3038
|
+
try {
|
|
3039
|
+
if (!readFailed && !res.destroyed) res.write(held);
|
|
3040
|
+
} catch { /* client already gone */ }
|
|
3041
|
+
const heldText = held.toString('utf8');
|
|
3042
|
+
if (streamLog) streamLog.push(heldText);
|
|
3043
|
+
sseBuffer += heldText;
|
|
3044
|
+
for (const ev of sseBuffer.split('\n\n')) {
|
|
3045
|
+
if (ev.trim()) parseSSEEvent(ev, accountIndex, accountManager, requestInfo);
|
|
3046
|
+
}
|
|
3047
|
+
sseBuffer = '';
|
|
3048
|
+
}
|
|
2977
3049
|
res.off('close', onClose);
|
|
2978
3050
|
// Cancel upstream reader to stop consuming data nobody needs
|
|
2979
3051
|
reader.cancel().catch(() => {});
|