mslxdff 0.1.29 → 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 +50 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/routes.js CHANGED
@@ -89,22 +89,31 @@ async function relay(res, upRes, body, { onFirstChunk, streamTimeoutMs = STREAM_
89
89
  let first = true;
90
90
  let wroteAny = false;
91
91
  let timedOut = false;
92
- // whole-stream ceiling (TTFB + generation): cancels the upstream body so
93
- // the loop exits and we proactively end the stream instead of hanging.
94
- let genTooLong = false;
95
- const genTimer = GEN_TIMEOUT_MS
96
- ? setTimeout(() => {
97
- genTooLong = true;
98
- if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
99
- }, GEN_TIMEOUT_MS)
100
- : null;
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
+ };
101
104
  const firstTimer = setTimeout(() => {
102
105
  timedOut = true;
103
106
  if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
104
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;
105
114
  try {
106
115
  for await (const chunk of upRes.body) {
107
- if (timedOut || genTooLong) break;
116
+ if (timedOut || stalled || tooLong) break;
108
117
  if (first) {
109
118
  first = false;
110
119
  ttf = Math.round(performance.now() - t0);
@@ -112,24 +121,26 @@ async function relay(res, upRes, body, { onFirstChunk, streamTimeoutMs = STREAM_
112
121
  }
113
122
  wroteAny = true;
114
123
  res.write(chunk);
124
+ armStall(); // any fresh chunk resets the stall clock
115
125
  }
116
126
  } catch (err) {
117
- // body threw — if we already started, treat as a mid-stream interrupt
118
- if (!wroteAny) { timedOut = true; genTooLong = false; }
127
+ // body threw — if nothing was written, treat it as a first-block timeout
128
+ if (!wroteAny) timedOut = true;
129
+ else stalled = true;
119
130
  } finally {
120
131
  if (firstTimer) clearTimeout(firstTimer);
121
- if (genTimer) clearTimeout(genTimer);
132
+ if (maxTimer) clearTimeout(maxTimer);
133
+ if (stallTimer) clearTimeout(stallTimer);
122
134
  }
123
135
  if (timedOut && !wroteAny) {
124
136
  // nothing written to res yet — safe to drop this model and let the
125
137
  // caller fail over to the next one. Do NOT write/end res here.
126
138
  return { status: STREAM_TIMEOUT_MS, ttfMs: null, totalMs: Math.round(performance.now() - t0), aborted: true };
127
139
  }
128
- if (genTooLong || (timedOut && wroteAny)) {
129
- // we'd already started streaming when it exceeded the ceiling — can't
130
- // cleanly fail over a half-written response, just end it so the client
131
- // sees a clean EOF rather than hanging. interrupted signals the caller
132
- // to remember this model as slow.
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.
133
144
  interrupted = true;
134
145
  try { res.end(); } catch { /* ignore */ }
135
146
  return { status: 200, ttfMs: ttf, totalMs: Math.round(performance.now() - t0), aborted: false, interrupted };
@@ -268,13 +279,21 @@ export const STREAM_TIMEOUT_MS = (() => {
268
279
  return Number.isInteger(n) && n > 0 ? n : 25_000;
269
280
  })();
270
281
 
271
- // Ceiling on the total wall-clock a single streamed response may run (TTFB +
272
- // generation + relay). Once exceeded we proactively end the stream so the
273
- // client gets a clean EOF instead of hanging on a very slow model; the model
274
- // is then flagged slow and demoted for the next request. Set to 0 to disable.
275
- export const GEN_TIMEOUT_MS = (() => {
276
- const n = Number(process.env.MSLXDFF_GEN_TIMEOUT_MS);
277
- return Number.isInteger(n) && n > 0 ? n : 20_000;
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;
278
297
  })();
279
298
 
280
299
  async function racePeerCandidates(candidates, ctx) {
@@ -437,11 +456,12 @@ const ROUTES = [
437
456
  continue;
438
457
  }
439
458
  if (out.interrupted) {
440
- // the whole stream ran past GEN_TIMEOUT_MS we ended it proactively
441
- // so the client got a clean EOF instead of hanging. Remember this
442
- // model as slow so the next request prefers a faster one.
443
- if (auto) await auto.recordError(model, { status: 200, slow: true, note: `gen timeout ${GEN_TIMEOUT_MS}ms` });
444
- evt("slow-model", { model, elapsedMs: out.totalMs ?? (Date.now() - startedAt), threshold: GEN_TIMEOUT_MS, interrupted: true });
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 });
445
465
  logCall(model, 200);
446
466
  evt("result", { model, status: out.status, via: "local", timing: upRes._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, interrupted: true });
447
467
  return;