mslxdff 0.1.28 → 0.1.30
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/routes.js +62 -10
package/package.json
CHANGED
package/src/routes.js
CHANGED
|
@@ -84,19 +84,36 @@ async function relay(res, upRes, body, { onFirstChunk, streamTimeoutMs = STREAM_
|
|
|
84
84
|
res.setHeader("Cache-Control", "no-cache");
|
|
85
85
|
res.setHeader("Connection", "keep-alive");
|
|
86
86
|
let ttf = null;
|
|
87
|
+
let interrupted = false;
|
|
87
88
|
if (upRes.body) {
|
|
88
89
|
let first = true;
|
|
89
90
|
let wroteAny = false;
|
|
90
91
|
let timedOut = false;
|
|
91
|
-
|
|
92
|
+
let stalled = false;
|
|
93
|
+
let tooLong = false;
|
|
94
|
+
let stallTimer = null;
|
|
95
|
+
const armStall = () => {
|
|
96
|
+
if (stallTimer) clearTimeout(stallTimer);
|
|
97
|
+
stallTimer = STALL_TIMEOUT_MS
|
|
98
|
+
? setTimeout(() => {
|
|
99
|
+
stalled = true;
|
|
100
|
+
if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
|
|
101
|
+
}, STALL_TIMEOUT_MS)
|
|
102
|
+
: null;
|
|
103
|
+
};
|
|
104
|
+
const firstTimer = setTimeout(() => {
|
|
92
105
|
timedOut = true;
|
|
93
|
-
// nothing written yet — cancel the upstream body so the loop can exit
|
|
94
|
-
// and we can fail over to the next model cleanly.
|
|
95
106
|
if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
|
|
96
107
|
}, streamTimeoutMs);
|
|
108
|
+
const maxTimer = MAX_STREAM_MS
|
|
109
|
+
? setTimeout(() => {
|
|
110
|
+
tooLong = true;
|
|
111
|
+
if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
|
|
112
|
+
}, MAX_STREAM_MS)
|
|
113
|
+
: null;
|
|
97
114
|
try {
|
|
98
115
|
for await (const chunk of upRes.body) {
|
|
99
|
-
if (timedOut) break;
|
|
116
|
+
if (timedOut || stalled || tooLong) break;
|
|
100
117
|
if (first) {
|
|
101
118
|
first = false;
|
|
102
119
|
ttf = Math.round(performance.now() - t0);
|
|
@@ -104,22 +121,29 @@ async function relay(res, upRes, body, { onFirstChunk, streamTimeoutMs = STREAM_
|
|
|
104
121
|
}
|
|
105
122
|
wroteAny = true;
|
|
106
123
|
res.write(chunk);
|
|
124
|
+
armStall(); // any fresh chunk resets the stall clock
|
|
107
125
|
}
|
|
108
126
|
} catch (err) {
|
|
109
|
-
|
|
127
|
+
// body threw — if nothing was written, treat it as a first-block timeout
|
|
128
|
+
if (!wroteAny) timedOut = true;
|
|
129
|
+
else stalled = true;
|
|
110
130
|
} finally {
|
|
111
|
-
clearTimeout(
|
|
131
|
+
if (firstTimer) clearTimeout(firstTimer);
|
|
132
|
+
if (maxTimer) clearTimeout(maxTimer);
|
|
133
|
+
if (stallTimer) clearTimeout(stallTimer);
|
|
112
134
|
}
|
|
113
135
|
if (timedOut && !wroteAny) {
|
|
114
136
|
// nothing written to res yet — safe to drop this model and let the
|
|
115
137
|
// caller fail over to the next one. Do NOT write/end res here.
|
|
116
138
|
return { status: STREAM_TIMEOUT_MS, ttfMs: null, totalMs: Math.round(performance.now() - t0), aborted: true };
|
|
117
139
|
}
|
|
118
|
-
if (
|
|
119
|
-
//
|
|
120
|
-
//
|
|
140
|
+
if ((stalled || tooLong) && wroteAny) {
|
|
141
|
+
// a response that was flowing either went silent for the stall window
|
|
142
|
+
// or blew the total ceiling — we can't cleanly fail over a half-written
|
|
143
|
+
// body, so end it and let the caller remember this model as slow.
|
|
144
|
+
interrupted = true;
|
|
121
145
|
try { res.end(); } catch { /* ignore */ }
|
|
122
|
-
return { status: 200, ttfMs: ttf, totalMs: Math.round(performance.now() - t0), aborted: false };
|
|
146
|
+
return { status: 200, ttfMs: ttf, totalMs: Math.round(performance.now() - t0), aborted: false, interrupted };
|
|
123
147
|
}
|
|
124
148
|
}
|
|
125
149
|
const totalMs = Math.round(performance.now() - t0);
|
|
@@ -255,6 +279,23 @@ export const STREAM_TIMEOUT_MS = (() => {
|
|
|
255
279
|
return Number.isInteger(n) && n > 0 ? n : 25_000;
|
|
256
280
|
})();
|
|
257
281
|
|
|
282
|
+
// Ceiling on how long a streamed response may produce no new chunk before we
|
|
283
|
+
// treat the model as stalled and proactively end the stream (clean EOF instead
|
|
284
|
+
// of hanging on a dead upstream). A model that keeps emitting chunks is never
|
|
285
|
+
// cut off — only silence triggers it. Set to 0 to disable.
|
|
286
|
+
export const STALL_TIMEOUT_MS = (() => {
|
|
287
|
+
const n = Number(process.env.MSLXDFF_STALL_TIMEOUT_MS);
|
|
288
|
+
return Number.isInteger(n) && n > 0 ? n : 15_000;
|
|
289
|
+
})();
|
|
290
|
+
|
|
291
|
+
// Loose total ceiling (TTFB + generation) so an unbounded stream can never
|
|
292
|
+
// run forever. Kept much larger than the stall timeout so normally-flowing
|
|
293
|
+
// responses are not truncated. Set to 0 to disable.
|
|
294
|
+
export const MAX_STREAM_MS = (() => {
|
|
295
|
+
const n = Number(process.env.MSLXDFF_MAX_STREAM_MS);
|
|
296
|
+
return Number.isInteger(n) && n > 0 ? n : 120_000;
|
|
297
|
+
})();
|
|
298
|
+
|
|
258
299
|
async function racePeerCandidates(candidates, ctx) {
|
|
259
300
|
for (let i = 0; i < candidates.length; i += PEER_RACE_LIMIT) {
|
|
260
301
|
const batch = candidates.slice(i, i + PEER_RACE_LIMIT);
|
|
@@ -414,6 +455,17 @@ const ROUTES = [
|
|
|
414
455
|
upRes = null;
|
|
415
456
|
continue;
|
|
416
457
|
}
|
|
458
|
+
if (out.interrupted) {
|
|
459
|
+
// the model went silent past STALL_TIMEOUT_MS (or blew the total
|
|
460
|
+
// ceiling) — we ended it so the client got a clean EOF instead of
|
|
461
|
+
// hanging. Remember this model as slow so the next request prefers
|
|
462
|
+
// a faster one.
|
|
463
|
+
if (auto) await auto.recordError(model, { status: 200, slow: true, note: `stall ${STALL_TIMEOUT_MS}ms` });
|
|
464
|
+
evt("slow-model", { model, elapsedMs: out.totalMs ?? (Date.now() - startedAt), threshold: STALL_TIMEOUT_MS, interrupted: true });
|
|
465
|
+
logCall(model, 200);
|
|
466
|
+
evt("result", { model, status: out.status, via: "local", timing: upRes._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, interrupted: true });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
417
469
|
// A model that took a long wall-clock time (TTFB + generation + relay)
|
|
418
470
|
// gets remembered as slow so the next request prefers a faster one.
|
|
419
471
|
const elapsed = Date.now() - startedAt;
|