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.
- package/package.json +1 -1
- package/src/server.js +74 -2
package/package.json
CHANGED
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(() => {});
|