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
|
@@ -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 ===
|
|
103
|
-
if (auto) try { await auto.recordError(actual, { status: 502, slow: true, note: `stream timeout ${
|
|
104
|
-
try { _logError(actual, 502, `stream timeout ${
|
|
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 ${
|
|
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 流时长)
|
package/src/routes/stream.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
119
|
+
cancelBody(upRes.body);
|
|
110
120
|
}, STALL_TIMEOUT_MS)
|
|
111
121
|
: null;
|
|
112
122
|
};
|
|
113
|
-
let firstTimer =
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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:
|
|
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;
|