mslxdff 0.1.113 → 0.1.114

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.113",
3
+ "version": "0.1.114",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -49,6 +49,8 @@ export async function runSerialTrial(ctx, deps = {}) {
49
49
  for (let idx = 0; idx < order.length; idx++) {
50
50
  const model = order[idx];
51
51
  handlerCtx.model = model;
52
+ handlerCtx.orderLen = order.length;
53
+ handlerCtx.idx = idx;
52
54
  evt("model-try", { reqId, model, idx, remaining: order.length - idx });
53
55
  if (plugins?.length) {
54
56
  const bt = await runHook(plugins, "model:beforeTry", { reqId, requested, model, idx, hops });
@@ -3,6 +3,13 @@ import { recordModelStats } from "../../state.js";
3
3
  import { normalizeFullId } from "../../providers/model-id.js";
4
4
  import { computeMetrics, extractUsageFromJson, extractUsageFromSseText } from "../../metrics.js";
5
5
 
6
+ // 唯一/最后候选没有 failover 去向:首块闸门退化为纯"防连接泄漏",放宽避免误杀慢模型
7
+ // (参考 opencode:zen 通道不设超时;openai responses 硬编码 300s headerTimeout)
8
+ const LAST_CANDIDATE_TIMEOUT_MS = (() => {
9
+ const n = Number(process.env.MSLXDFF_LAST_CANDIDATE_TIMEOUT_MS);
10
+ return Number.isInteger(n) && n >= 0 ? n : 120_000;
11
+ })();
12
+
6
13
  /**
7
14
  * RelayPipeline 深模块
8
15
  * 把 5 个 handler 各自的 fallback→relay→scoring→事件 6段流水收敛为单一真相。
@@ -72,9 +79,16 @@ export function createRelayPipeline({
72
79
  }
73
80
  _evt("relay-start", { reqId, model: actual, via, isStream: Boolean(body?.stream), fallback });
74
81
 
75
- // 3. relay
82
+ // 3. relay(唯一/最后候选:无 failover 去向 → 闸门放宽到防泄漏级别)
83
+ const orderLen = handlerCtx?.orderLen;
84
+ const curIdx = handlerCtx?.idx;
85
+ const isLastCandidate =
86
+ Number.isInteger(orderLen) && orderLen > 0 &&
87
+ (orderLen === 1 || (Number.isInteger(curIdx) && curIdx >= orderLen - 1));
88
+ const streamTimeoutMs = isLastCandidate ? LAST_CANDIDATE_TIMEOUT_MS : C.STREAM_TIMEOUT_MS;
76
89
  const out = await _relay(res, upRes, body, {
77
90
  fallback,
91
+ streamTimeoutMs,
78
92
  onFirstChunk: (delta) => {
79
93
  try { markFn(`ttf-${actual}`); } catch {}
80
94
  _evt("relay-first-chunk", { reqId, model: actual, ttfMs: delta, via });
@@ -99,12 +113,12 @@ export function createRelayPipeline({
99
113
  });
100
114
 
101
115
  // 5a. 首块超时未写字节 → 回退
102
- if (out.status === C.STREAM_TIMEOUT_MS) {
103
- if (auto) try { await auto.recordError(actual, { status: 502, slow: true, note: `stream timeout ${C.STREAM_TIMEOUT_MS}ms` }); } catch {}
104
- try { _logError(actual, 502, `stream timeout ${C.STREAM_TIMEOUT_MS}ms`); } catch {}
116
+ if (streamTimeoutMs > 0 && out.status === streamTimeoutMs) {
117
+ if (auto) try { await auto.recordError(actual, { status: 502, slow: true, note: `stream timeout ${streamTimeoutMs}ms` }); } catch {}
118
+ try { _logError(actual, 502, `stream timeout ${streamTimeoutMs}ms`); } catch {}
105
119
  _evt("upstream-error", { reqId, model: actual, status: 502, message: "stream timeout", timing: null });
106
120
  _evt("fallback", { reqId, from: actual, to: null, reason: "stream timeout" });
107
- return { handled: false, upRes: null, lastErr: { model: actual, upstream: null, status: 502, message: `stream timed out after ${C.STREAM_TIMEOUT_MS}ms` } };
121
+ return { handled: false, upRes: null, lastErr: { model: actual, upstream: null, status: 502, message: `stream timed out after ${streamTimeoutMs}ms` } };
108
122
  }
109
123
 
110
124
  // 5b. 中断(stall 超时 / max 流时长)
@@ -11,6 +11,14 @@ function chunkText(chunk) {
11
11
  return "";
12
12
  }
13
13
 
14
+ // body.cancel() 可能返回非 Promise(自定义/AI SDK 流)——同步异常与 rejection 双路径都要吞掉
15
+ function cancelBody(body) {
16
+ try {
17
+ const p = typeof body?.cancel === "function" ? body.cancel() : null;
18
+ if (p && typeof p.catch === "function") p.catch(() => {});
19
+ } catch { /* ignore */ }
20
+ }
21
+
14
22
  export const SLOW_TOTAL_MS = (() => {
15
23
  const n = Number(process.env.MSLXDFF_SLOW_TOTAL_MS);
16
24
  return Number.isInteger(n) && n > 0 ? n : 20_000;
@@ -18,7 +26,8 @@ export const SLOW_TOTAL_MS = (() => {
18
26
 
19
27
  export const STREAM_TIMEOUT_MS = (() => {
20
28
  const n = Number(process.env.MSLXDFF_STREAM_TIMEOUT_MS);
21
- return Number.isInteger(n) && n > 0 ? n : 25_000;
29
+ // 0 = 显式关闭首块超时(慢思考模型专用);未设/非法值 → 默认 25s
30
+ return Number.isInteger(n) && n >= 0 ? n : 25_000;
22
31
  })();
23
32
 
24
33
  export const STALL_TIMEOUT_MS = (() => {
@@ -75,6 +84,7 @@ export async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort,
75
84
  downstreamClosed: false,
76
85
  usage: null,
77
86
  chars: 0,
87
+ recoveries: 0,
78
88
  };
79
89
  let prevChunkAt = t0;
80
90
  const onClose = () => {
@@ -106,20 +116,22 @@ export async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort,
106
116
  ? setTimeout(() => {
107
117
  stalled = true;
108
118
  detail.exitReason = "stall";
109
- if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
119
+ cancelBody(upRes.body);
110
120
  }, STALL_TIMEOUT_MS)
111
121
  : null;
112
122
  };
113
- let firstTimer = setTimeout(() => {
114
- timedOut = true;
115
- detail.exitReason = "first-timeout";
116
- if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
117
- }, streamTimeoutMs);
123
+ let firstTimer = streamTimeoutMs > 0
124
+ ? setTimeout(() => {
125
+ timedOut = true;
126
+ detail.exitReason = "first-timeout";
127
+ cancelBody(upRes.body);
128
+ }, streamTimeoutMs)
129
+ : null;
118
130
  const maxTimer = MAX_STREAM_MS
119
131
  ? setTimeout(() => {
120
132
  tooLong = true;
121
133
  detail.exitReason = "max";
122
- if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
134
+ cancelBody(upRes.body);
123
135
  }, MAX_STREAM_MS)
124
136
  : null;
125
137
  try {
@@ -177,6 +189,15 @@ export async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort,
177
189
  } catch {}
178
190
  }
179
191
  } catch { /* ignore */ }
192
+ // 首块/空闲超时后上游仍吐出了数据 → 只是慢,不是死:撤销超时判定,照常转发
193
+ //(cancel 是协作式的,缓冲数据仍会到达;丢掉已到达的数据是纯损失)
194
+ if (timedOut || stalled) {
195
+ timedOut = false;
196
+ stalled = false;
197
+ detail.recoveries = (detail.recoveries || 0) + 1;
198
+ if (detail.exitReason === "first-timeout" || detail.exitReason === "stall") detail.exitReason = null;
199
+ if (firstTimer) { clearTimeout(firstTimer); firstTimer = null; }
200
+ }
180
201
  if (timedOut || stalled || tooLong) break;
181
202
  if (first) {
182
203
  first = false;
@@ -216,7 +237,7 @@ export async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort,
216
237
  }
217
238
  if (timedOut && !wroteAny) {
218
239
  res.removeListener("close", onClose);
219
- return { status: STREAM_TIMEOUT_MS, ttfMs: null, totalMs: Math.round(performance.now() - t0), aborted: true, interrupted: false, detail };
240
+ return { status: streamTimeoutMs, ttfMs: null, totalMs: Math.round(performance.now() - t0), aborted: true, interrupted: false, detail };
220
241
  }
221
242
  if ((stalled || tooLong) && wroteAny) {
222
243
  interrupted = true;