maxpool 1.19.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.js +74 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maxpool",
3
- "version": "1.19.0",
3
+ "version": "1.19.1",
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
@@ -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(value);
2972
+ const ok = res.write(out);
2920
2973
 
2921
- const text = decoder.decode(value, { stream: true });
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(() => {});