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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/routes.js +62 -10
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
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
- const timer = setTimeout(() => {
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
- timedOut = true;
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(timer);
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 (timedOut && wroteAny) {
119
- // we'd already started streaming when it died can't fail over, just
120
- // end the response so the client sees a clean EOF.
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;