maxpool 1.19.0 → 1.19.2
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 +105 -5
package/package.json
CHANGED
package/src/server.js
CHANGED
|
@@ -1355,8 +1355,16 @@ async function forwardRequest(
|
|
|
1355
1355
|
res.end();
|
|
1356
1356
|
}
|
|
1357
1357
|
} else {
|
|
1358
|
-
|
|
1359
|
-
|
|
1358
|
+
// MODEL ECHO: rewrite the provider's model id back to the client's before the
|
|
1359
|
+
// body reaches Claude Code. Computed BEFORE writeHead so a length change lands
|
|
1360
|
+
// in the SAME header block — setting content-length after writeHead is a no-op
|
|
1361
|
+
// and would ship a body/length mismatch (the client then hangs or truncates).
|
|
1362
|
+
const outBuf = normalizeModelEcho(buf, requestInfo.model);
|
|
1363
|
+
const outHeaders = outBuf === buf
|
|
1364
|
+
? responseHeaders
|
|
1365
|
+
: { ...responseHeaders, 'content-length': String(outBuf.length) };
|
|
1366
|
+
if (!res.headersSent) res.writeHead(upstreamRes.status, outHeaders);
|
|
1367
|
+
res.end(outBuf);
|
|
1360
1368
|
}
|
|
1361
1369
|
}
|
|
1362
1370
|
} catch (err) {
|
|
@@ -1657,7 +1665,7 @@ function isContextLengthError(errorBody) {
|
|
|
1657
1665
|
return /exceeded model token limit|maximum context length|context length exceeded|context window (?:size )?(?:exceeded|too)|prompt is too long|input is too long|reduce the length of|too many (?:input )?tokens|request too large/i.test(errorBody);
|
|
1658
1666
|
}
|
|
1659
1667
|
|
|
1660
|
-
export const __serverTest = { reanchorOrphanedSystemMessages, unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, classifyEffortRejection, repairEffort, isCapacitySignalStatus, isStrippableThinkingBlock, stripForeignThinkingBlocks, parseRejectedBlockPath, stripRejectedBlockClass, peekRejectedBlockType, describeRejectedBlock, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, commitStreamGraceHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody, isContextLengthError, streamResponse, startIdleRequestReaper };
|
|
1668
|
+
export const __serverTest = { reanchorOrphanedSystemMessages, unavailableMessage, computeQueueWindowMs, isRetriableUpstreamStatus, classifyEffortRejection, repairEffort, isCapacitySignalStatus, isStrippableThinkingBlock, stripForeignThinkingBlocks, parseRejectedBlockPath, stripRejectedBlockClass, peekRejectedBlockType, describeRejectedBlock, headerValue, getMaxpoolProfile, ensureQueueHeartbeat, clearQueueHeartbeat, commitStreamGraceHeartbeat, describeRequest, classifyRateLimit, detectTranscriptOrigin, isAnthropicIncompatBody, isContextLengthError, streamResponse, startIdleRequestReaper, normalizeModelEcho };
|
|
1661
1669
|
|
|
1662
1670
|
async function readErrorBody(upstreamRes, limitBytes = 64 * 1024) {
|
|
1663
1671
|
if (!upstreamRes.body) return '';
|
|
@@ -2856,6 +2864,41 @@ function startIdleRequestReaper(res, reqId, idleMs, { now = Date.now, setInterva
|
|
|
2856
2864
|
return timer;
|
|
2857
2865
|
}
|
|
2858
2866
|
|
|
2867
|
+
|
|
2868
|
+
/** Rewrite a non-streaming JSON body's `model` back to the CLIENT'S requested model.
|
|
2869
|
+
* Twin of the SSE model-echo normalization in streamResponse: provider upstreams
|
|
2870
|
+
* (z.ai/GLM, Kimi) echo their own id, Claude Code persists it, and a resumed session
|
|
2871
|
+
* then prints "Session model glm-5.3 could not be restored". The streaming half
|
|
2872
|
+
* shipped in v1.19.1; this covers the buffered path, which is where the remaining
|
|
2873
|
+
* 5,719 contaminated turns came from (measured 2026-09-02). No-op when the ids
|
|
2874
|
+
* already match, when the body is not JSON, or when no client model is known. */
|
|
2875
|
+
function normalizeModelEcho(buf, clientModel) {
|
|
2876
|
+
if (!clientModel || !buf?.length) return buf;
|
|
2877
|
+
try {
|
|
2878
|
+
const json = JSON.parse(buf.toString('utf8'));
|
|
2879
|
+
if (!json || typeof json !== 'object' || typeof json.model !== 'string') return buf;
|
|
2880
|
+
if (json.model === clientModel) return buf;
|
|
2881
|
+
json.model = clientModel;
|
|
2882
|
+
return Buffer.from(JSON.stringify(json), 'utf8');
|
|
2883
|
+
} catch {
|
|
2884
|
+
return buf; // non-JSON (or truncated) body — forward untouched
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
function concatUint8(chunks) {
|
|
2889
|
+
let n = 0;
|
|
2890
|
+
for (const c of chunks) n += c.length;
|
|
2891
|
+
const out = new Uint8Array(n);
|
|
2892
|
+
let o = 0;
|
|
2893
|
+
for (const c of chunks) { out.set(c, o); o += c.length; }
|
|
2894
|
+
return out;
|
|
2895
|
+
}
|
|
2896
|
+
function totalLen(chunks) {
|
|
2897
|
+
let n = 0;
|
|
2898
|
+
for (const c of chunks) n += c.length;
|
|
2899
|
+
return n;
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2859
2902
|
async function streamResponse(webStream, res, status, responseHeaders, accountIndex, accountManager, streamLog, requestInfo = {}, idleMs = STREAM_IDLE_MS) {
|
|
2860
2903
|
const reader = webStream.getReader();
|
|
2861
2904
|
const decoder = new TextDecoder();
|
|
@@ -2880,6 +2923,10 @@ async function streamResponse(webStream, res, status, responseHeaders, accountIn
|
|
|
2880
2923
|
const onClose = () => { reader.cancel().catch(() => {}); };
|
|
2881
2924
|
res.once('close', onClose);
|
|
2882
2925
|
|
|
2926
|
+
// MODEL ECHO NORMALIZATION state (rationale at the write site).
|
|
2927
|
+
let modelEchoPending = true;
|
|
2928
|
+
let modelEchoBuffer = null;
|
|
2929
|
+
|
|
2883
2930
|
try {
|
|
2884
2931
|
while (true) {
|
|
2885
2932
|
// Idle guard: a half-open upstream (headers, then silence, never closes) would
|
|
@@ -2915,10 +2962,44 @@ async function streamResponse(webStream, res, status, responseHeaders, accountIn
|
|
|
2915
2962
|
committed = true;
|
|
2916
2963
|
}
|
|
2917
2964
|
|
|
2965
|
+
// MODEL ECHO NORMALIZATION (2026-08-31): provider upstreams echo their own model
|
|
2966
|
+
// id ("glm-5.3", kimi-*) in message_start, and Claude Code persists that id into
|
|
2967
|
+
// the session transcript — on resume the id fails model-family resolution and the
|
|
2968
|
+
// session prints "Session model ... could not be restored (not a model this
|
|
2969
|
+
// version of Claude Code recognizes)" (278 sessions affected, measured).
|
|
2970
|
+
// Rewrite the model field back to the CLIENT'S requested model (requestInfo.model,
|
|
2971
|
+
// captured before the per-account rewrite) inside the first complete SSE event
|
|
2972
|
+
// carrying a "model" key; afterwards chunks pass through untouched. ReadableStream
|
|
2973
|
+
// chunks are Uint8Array — Buffer.concat/toString would yield comma-joined byte
|
|
2974
|
+
// numbers (caught by the normalization tests), so hold an array of chunks and
|
|
2975
|
+
// concat with decoder-safe helpers.
|
|
2976
|
+
let out = value;
|
|
2977
|
+
if (requestInfo?.model && modelEchoPending) {
|
|
2978
|
+
modelEchoBuffer = modelEchoBuffer ? [...modelEchoBuffer, value] : [value];
|
|
2979
|
+
const s = decoder.decode(concatUint8(modelEchoBuffer));
|
|
2980
|
+
if (s.includes('"model"') && s.includes('\n\n')) {
|
|
2981
|
+
const normalized = s.replace(
|
|
2982
|
+
/("model":")[^"]+(")/,
|
|
2983
|
+
`$1${requestInfo.model.replace(/["\\]/g, '\\$&')}$2`,
|
|
2984
|
+
);
|
|
2985
|
+
out = Buffer.from(normalized, 'utf8');
|
|
2986
|
+
modelEchoBuffer = null;
|
|
2987
|
+
modelEchoPending = false;
|
|
2988
|
+
} else if (totalLen(modelEchoBuffer) > 64 * 1024) {
|
|
2989
|
+
// Pathological upstream: 64KB with no complete model-bearing event. Flush
|
|
2990
|
+
// verbatim — the warning is the worst outcome of a missed rewrite.
|
|
2991
|
+
out = Buffer.from(concatUint8(modelEchoBuffer));
|
|
2992
|
+
modelEchoBuffer = null;
|
|
2993
|
+
modelEchoPending = false;
|
|
2994
|
+
} else {
|
|
2995
|
+
continue; // hold until the model-bearing event is complete
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2918
2999
|
// Forward chunk immediately
|
|
2919
|
-
const ok = res.write(
|
|
3000
|
+
const ok = res.write(out);
|
|
2920
3001
|
|
|
2921
|
-
const text = decoder.decode(
|
|
3002
|
+
const text = decoder.decode(out, { stream: true });
|
|
2922
3003
|
|
|
2923
3004
|
// Capture for logging
|
|
2924
3005
|
if (streamLog) streamLog.push(text);
|
|
@@ -2974,6 +3055,25 @@ async function streamResponse(webStream, res, status, responseHeaders, accountIn
|
|
|
2974
3055
|
readFailed = true;
|
|
2975
3056
|
throw err;
|
|
2976
3057
|
} finally {
|
|
3058
|
+
// MODEL ECHO NORMALIZATION last-resort flush: the read loop can exit via done,
|
|
3059
|
+
// error, or client disconnect while chunks are still HELD for the rewrite.
|
|
3060
|
+
// Whatever the exit path, deliver the held bytes and accrue their usage — a
|
|
3061
|
+
// mid-flight death must not swallow delivered tokens (H4) nor truncate the
|
|
3062
|
+
// body (empty '' responses, C1).
|
|
3063
|
+
if (modelEchoBuffer) {
|
|
3064
|
+
const held = Buffer.from(concatUint8(modelEchoBuffer));
|
|
3065
|
+
modelEchoBuffer = null;
|
|
3066
|
+
try {
|
|
3067
|
+
if (!readFailed && !res.destroyed) res.write(held);
|
|
3068
|
+
} catch { /* client already gone */ }
|
|
3069
|
+
const heldText = held.toString('utf8');
|
|
3070
|
+
if (streamLog) streamLog.push(heldText);
|
|
3071
|
+
sseBuffer += heldText;
|
|
3072
|
+
for (const ev of sseBuffer.split('\n\n')) {
|
|
3073
|
+
if (ev.trim()) parseSSEEvent(ev, accountIndex, accountManager, requestInfo);
|
|
3074
|
+
}
|
|
3075
|
+
sseBuffer = '';
|
|
3076
|
+
}
|
|
2977
3077
|
res.off('close', onClose);
|
|
2978
3078
|
// Cancel upstream reader to stop consuming data nobody needs
|
|
2979
3079
|
reader.cancel().catch(() => {});
|