mslxdff 0.1.160 → 0.1.161
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/bin/mslxdff.js +4 -4
- package/docs/cli_help_mini.md +135 -0
- package/package.json +1 -1
- package/src/auto.js +254 -254
- package/src/autostart.js +3 -0
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/empty-turn.js +119 -0
- package/src/chat-pipeline/index.js +126 -123
- package/src/chat-pipeline/policy.js +76 -76
- package/src/chat-pipeline/serial-trial.js +53 -19
- package/src/cli/commands/daemon.js +4 -4
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/list-providers.js +1 -1
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/globalqwenwork-login.js +126 -0
- package/src/cli/commands/provider/index.js +126 -124
- package/src/cli/commands/provider/models.js +142 -139
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/sync.js +232 -232
- package/src/cli/commands/system.js +2 -2
- package/src/cli/format.js +6 -0
- package/src/cli/policy.js +2 -2
- package/src/cli/provider-row.js +2 -2
- package/src/cli/status.js +279 -279
- package/src/daemon.js +101 -96
- package/src/logs.js +14 -1
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/model-trace.js +5 -2
- package/src/models.js +225 -225
- package/src/providers/AGENTS.md +46 -0
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/globalqwenwork/account-store.js +141 -0
- package/src/providers/globalqwenwork/constants.js +73 -0
- package/src/providers/globalqwenwork/cosy.js +123 -0
- package/src/providers/globalqwenwork/crypto.js +220 -0
- package/src/providers/globalqwenwork/http.js +22 -0
- package/src/providers/globalqwenwork/index.js +331 -0
- package/src/providers/globalqwenwork/payload.js +145 -0
- package/src/providers/globalqwenwork/rsa.js +56 -0
- package/src/providers/globalqwenwork/sse.js +270 -0
- package/src/providers/globalqwenwork/stream.js +132 -0
- package/src/providers/globalqwenwork/upstream.js +122 -0
- package/src/providers/globalqwenwork.js +1 -0
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +183 -183
- package/src/providers/qoder/index.js +230 -230
- package/src/providers/qoder/sse.js +103 -103
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +327 -327
- package/src/providers/qwenwork/payload.js +142 -142
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/registry.js +74 -66
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/sse.js +19 -2
- package/src/reasoning.js +32 -32
- package/src/routes/AGENTS.md +37 -0
- package/src/routes/chat/exhausted-handler.js +23 -8
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/local-handler.js +3 -1
- package/src/routes/chat/relay-pipeline.js +276 -264
- package/src/routes/chat/via-route-handler.js +146 -146
- package/src/routes/hedge.js +255 -255
- package/src/routes/helpers.js +20 -4
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream-hold.js +138 -0
- package/src/routes/stream-scan.js +192 -0
- package/src/routes/stream.js +386 -393
- package/src/runtime/bootstrap.js +45 -45
- package/src/runtime/lifecycle-forensics.js +116 -0
- package/src/runtime/lifecycle-log.js +23 -0
- package/src/runtime/provider-gate.js +34 -33
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +64 -64
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/talk-log.js +226 -0
- package/src/timeline.js +5 -2
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +289 -289
|
@@ -56,6 +56,8 @@ export async function handleLocalRelay({
|
|
|
56
56
|
stages,
|
|
57
57
|
startedAt,
|
|
58
58
|
});
|
|
59
|
+
// wrotePayload 是「正文真到了下游」的送达证据:serial-trial 记 empty-turn-recovered 只认它,
|
|
60
|
+
// 不认 handled 本身(handled 也覆盖下游已断开等零输出路径,见 ADR-0043)。
|
|
59
61
|
if (!r.handled) return { handled: false, upRes: null, lastErr: r.lastErr };
|
|
60
|
-
return { handled: true };
|
|
62
|
+
return { handled: true, wrotePayload: r.wrotePayload === true };
|
|
61
63
|
}
|
|
@@ -1,264 +1,276 @@
|
|
|
1
|
-
import { runHook } from "../../plugins.js";
|
|
2
|
-
import { recordModelStats } from "../../state.js";
|
|
3
|
-
import { normalizeFullId } from "../../providers/model-id.js";
|
|
4
|
-
import { computeMetrics } from "../../metrics.js";
|
|
5
|
-
import { recordChatUsage } from "../../usage/record.js"; // 窗口报表唯一写入点(canonical 名单记防双计)— 见 .agents/notes/implemented/feature/2026-09-19-usage-report-jsonl.md
|
|
6
|
-
import { recordOutput, computeOutputRow } from "../../providers/cline/usage.js"; // cline 专属旁路统计(账号×模型,流式也覆盖)
|
|
7
|
-
import { upstreamEcho } from "../../model-trace.js"; // 回显头→日志字段(谁上的/哪个号/为什么/是否冷却)单一来源
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
} catch {}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
_evt("
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
try { await auto.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
1
|
+
import { runHook } from "../../plugins.js";
|
|
2
|
+
import { recordModelStats } from "../../state.js";
|
|
3
|
+
import { normalizeFullId } from "../../providers/model-id.js";
|
|
4
|
+
import { computeMetrics } from "../../metrics.js";
|
|
5
|
+
import { recordChatUsage } from "../../usage/record.js"; // 窗口报表唯一写入点(canonical 名单记防双计)— 见 .agents/notes/implemented/feature/2026-09-19-usage-report-jsonl.md
|
|
6
|
+
import { recordOutput, computeOutputRow } from "../../providers/cline/usage.js"; // cline 专属旁路统计(账号×模型,流式也覆盖)
|
|
7
|
+
import { upstreamEcho } from "../../model-trace.js"; // 回显头→日志字段(谁上的/哪个号/为什么/是否冷却)单一来源
|
|
8
|
+
import { recordRelayTalk } from "../../talk-log.js"; // 环形对话日志(问答全文,最近 1 小时):五段流水在此汇合,落盘点只这一个
|
|
9
|
+
import { isEmptyTurnDetail } from "../stream-scan.js"; // 空轮判据单一真相(stream.js 用它决定封不封口,这里用它决定交不交回重试)
|
|
10
|
+
|
|
11
|
+
// 唯一/最后候选没有 failover 去向:首块闸门退化为纯"防连接泄漏",放宽避免误杀慢模型
|
|
12
|
+
// (参考 opencode:zen 通道不设超时;openai responses 硬编码 300s headerTimeout)
|
|
13
|
+
export const LAST_CANDIDATE_TIMEOUT_MS = (() => {
|
|
14
|
+
const n = Number(process.env.MSLXDFF_LAST_CANDIDATE_TIMEOUT_MS);
|
|
15
|
+
return Number.isInteger(n) && n >= 0 ? n : 120_000;
|
|
16
|
+
})();
|
|
17
|
+
|
|
18
|
+
/** 空转 200 判定(serial-trial 同模型重试用):与 execute 内 _emptyTurn 产生的 lastErr 同源 */
|
|
19
|
+
export function isEmptyTurnError(err) {
|
|
20
|
+
return Number(err?.status) === 502 && String(err?.message || "").startsWith("EMPTY_MODEL_RESPONSE");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* RelayPipeline 深模块
|
|
25
|
+
* 把 5 个 handler 各自的 fallback→relay→scoring→事件 6段流水收敛为单一真相。
|
|
26
|
+
* 对外 1 接口:createRelayPipeline(deps) => { execute(ctx) }
|
|
27
|
+
* 设计要点:全部外部可注入,便于在 pipeline seam 上做行为测试;constants 注入便于单测加速。
|
|
28
|
+
*/
|
|
29
|
+
export function createRelayPipeline({
|
|
30
|
+
relay,
|
|
31
|
+
buildFallbackInfo,
|
|
32
|
+
auto,
|
|
33
|
+
plugins,
|
|
34
|
+
evt,
|
|
35
|
+
mark,
|
|
36
|
+
logCall,
|
|
37
|
+
logError,
|
|
38
|
+
constants,
|
|
39
|
+
startedAt: defaultStartedAt,
|
|
40
|
+
stages: defaultStages,
|
|
41
|
+
perfNow,
|
|
42
|
+
} = {}) {
|
|
43
|
+
const C = {
|
|
44
|
+
STREAM_TIMEOUT_MS: 25_000,
|
|
45
|
+
SLOW_TOTAL_MS: 20_000,
|
|
46
|
+
STALL_TIMEOUT_MS: 0,
|
|
47
|
+
SCORE_STALL_MS: 15_000,
|
|
48
|
+
...(constants || {}),
|
|
49
|
+
};
|
|
50
|
+
const _relay = relay;
|
|
51
|
+
const _build = buildFallbackInfo;
|
|
52
|
+
const _evt = evt || (() => {});
|
|
53
|
+
const _mark = mark || (() => {});
|
|
54
|
+
const _logCall = logCall || (() => {});
|
|
55
|
+
const _logError = logError || (() => {});
|
|
56
|
+
const _perfNow = perfNow || (() => Date.now());
|
|
57
|
+
|
|
58
|
+
async function execute({
|
|
59
|
+
res,
|
|
60
|
+
upRes,
|
|
61
|
+
body,
|
|
62
|
+
requested,
|
|
63
|
+
actual,
|
|
64
|
+
lastErr,
|
|
65
|
+
via,
|
|
66
|
+
lockModel,
|
|
67
|
+
useAuto,
|
|
68
|
+
handlerCtx,
|
|
69
|
+
mark: m2,
|
|
70
|
+
perf0,
|
|
71
|
+
attemptStartMs,
|
|
72
|
+
stages: s2,
|
|
73
|
+
startedAt: sa2,
|
|
74
|
+
streamTimeoutMs: ctxStreamTimeoutMs,
|
|
75
|
+
} = {}) {
|
|
76
|
+
const markFn = m2 || _mark;
|
|
77
|
+
const curStartedAt = sa2 ?? defaultStartedAt ?? Date.now();
|
|
78
|
+
const curStages = s2 ?? defaultStages ?? [];
|
|
79
|
+
const reqId = handlerCtx?.reqId;
|
|
80
|
+
const hops = handlerCtx?.hops;
|
|
81
|
+
|
|
82
|
+
// 1. logCall(pre) — 保持原 handler 的 logCall→fallback→relay-start 时序
|
|
83
|
+
try { _logCall(actual, upRes?.status); } catch {}
|
|
84
|
+
// 2. fallback + relay-start
|
|
85
|
+
let fallback = null;
|
|
86
|
+
try {
|
|
87
|
+
if (_build) fallback = _build({ requested, actual, lastErr, via, useAuto, lockModel });
|
|
88
|
+
} catch {}
|
|
89
|
+
if (fallback?.fallback) {
|
|
90
|
+
_evt("fallback-notice", { reqId, requested, actual, reason: fallback.reason, notice: fallback.notice, via, fallback: true });
|
|
91
|
+
}
|
|
92
|
+
_evt("relay-start", { reqId, model: actual, via, isStream: Boolean(body?.stream), fallback });
|
|
93
|
+
|
|
94
|
+
// 3. relay(唯一/最后候选:无 failover 去向 → 闸门放宽到防泄漏级别;显式 streamTimeoutMs 优先)
|
|
95
|
+
const orderLen = handlerCtx?.orderLen;
|
|
96
|
+
const curIdx = handlerCtx?.idx;
|
|
97
|
+
const isLastCandidate =
|
|
98
|
+
Number.isInteger(orderLen) && orderLen > 0 &&
|
|
99
|
+
(orderLen === 1 || (Number.isInteger(curIdx) && curIdx >= orderLen - 1));
|
|
100
|
+
const streamTimeoutMs = Number.isInteger(ctxStreamTimeoutMs) && ctxStreamTimeoutMs >= 0
|
|
101
|
+
? ctxStreamTimeoutMs
|
|
102
|
+
: (isLastCandidate ? LAST_CANDIDATE_TIMEOUT_MS : C.STREAM_TIMEOUT_MS);
|
|
103
|
+
const out = await _relay(res, upRes, body, {
|
|
104
|
+
fallback,
|
|
105
|
+
streamTimeoutMs,
|
|
106
|
+
attemptStartMs, // 本次上游尝试起点(performance.now 同源),relay 用它算 preflightMs
|
|
107
|
+
onFirstChunk: (delta) => {
|
|
108
|
+
try { markFn(`ttf-${actual}`); } catch {}
|
|
109
|
+
_evt("relay-first-chunk", { reqId, model: actual, ttfMs: delta, via });
|
|
110
|
+
if (plugins?.length) runHook(plugins, "relay:first-chunk", { reqId, requested, model: actual, via, ttfMs: delta }).catch(() => {});
|
|
111
|
+
},
|
|
112
|
+
onDownstreamAbort: () => {
|
|
113
|
+
_evt("client-abort", { reqId, model: actual, totalMs: Math.round(_perfNow() - (perf0 ?? 0)), stages: [...curStages] });
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 4. relay-done
|
|
118
|
+
_evt("relay-done", {
|
|
119
|
+
reqId,
|
|
120
|
+
model: actual,
|
|
121
|
+
via,
|
|
122
|
+
status: out.status,
|
|
123
|
+
ttfMs: out.ttfMs,
|
|
124
|
+
totalMs: out.totalMs,
|
|
125
|
+
aborted: out.aborted,
|
|
126
|
+
interrupted: out.interrupted ?? false,
|
|
127
|
+
timedOut: out.timedOut ?? false,
|
|
128
|
+
...upstreamEcho(upRes),
|
|
129
|
+
detail: out.detail ?? null,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 环形对话日志:只记有真实输出的轮次(空轮交给下面的空转判定另行报错)。
|
|
133
|
+
// 纯旁路:不改闸门、不改计时、不改返回体;任何异常都吞掉,绝不影响转发。
|
|
134
|
+
try { recordRelayTalk({ reqId, model: actual, via, hops, body, out, echo: upstreamEcho(upRes) }); } catch {}
|
|
135
|
+
|
|
136
|
+
// 5a0. 空转 200:流正常结束但零正文零工具调用 → 客户端会报 EMPTY_MODEL_RESPONSE
|
|
137
|
+
// ("The model ended its turn without producing any output");转 failover 而不是
|
|
138
|
+
// 把空轮递给客户端。chatShaped 是前置证据:只有看得出是 chat 轮才判空,
|
|
139
|
+
// 非 chat SSE/无 choices JSON 透传是正式契约(chat-route 单测锁死),一律放行。
|
|
140
|
+
// 工具轮豁免:tool_calls 无正文是合法 agent 形态;
|
|
141
|
+
// finish=tool_calls/function_call 兜底豁免(防计数漏检误杀);下游已断开不重试(写给谁看)。
|
|
142
|
+
// 对标 dsh-cline-pass 的 EMPTY_RESPONSE 语义;不记 auto 冷却(空转≠模型坏,重试多半能好)。
|
|
143
|
+
// 判据单一真相在 stream-scan.js:isEmptyTurnDetail —— relay 侧「要不要封口」与这里「要不要重拉」必须同源,否则会出现留了口却不再救、或已终结却仍重拉的错拍
|
|
144
|
+
// heldOpen 也计入:非流式留口返回的是上游原状态码(2xx 变体或中继的 ≥400),只认 200 会漏判 → 没人收场,连接挂死
|
|
145
|
+
const _d = out.detail || {};
|
|
146
|
+
const _emptyTurn = (out.status === 200 || out.heldOpen === true) && !out.timedOut && !out.interrupted && !_d.downstreamClosed && isEmptyTurnDetail(_d);
|
|
147
|
+
if (_emptyTurn) {
|
|
148
|
+
const _why = _d.sawFinishReason ? ` (finish_reason=${_d.sawFinishReason})` : " (no content, no tool calls)";
|
|
149
|
+
// 错误包络暂扣后下游不再直观看到上游原文:把摘要带进最终报错(截断 200 字),排障不断线。
|
|
150
|
+
const _err = _d.upstreamErrorText ? ` upstream=${String(_d.upstreamErrorText).slice(0, 200)}` : "";
|
|
151
|
+
// 零正文的成因直接写进报错(不只给现象):finish_reason=length + 大量思考 = 额度被 reasoning 吃光
|
|
152
|
+
// (实测 cline-free deepseek reasoningChars=28151、completion_tokens=8192=max_tokens、chars=0)。
|
|
153
|
+
// 同参重拉必复现,故 serial-trial 的空转重试会顺手把 max_tokens 抬一档(见 emptyRaiseCap)。
|
|
154
|
+
const _lenCapped = _d.sawFinishReason === "length";
|
|
155
|
+
const _hint = _lenCapped && (Number(_d.reasoningChars) || 0) > 0
|
|
156
|
+
? ` [思考 ${Number(_d.reasoningChars) || 0} 字刷满 max_tokens,正文 0 字]`
|
|
157
|
+
: "";
|
|
158
|
+
try { _logError(actual, 502, `empty turn${_why}${_err}${_hint}`); } catch {}
|
|
159
|
+
_evt("upstream-error", { reqId, model: actual, status: 502, message: "empty turn", timing: null, finish: _d.sawFinishReason || null, reasoningChars: Number(_d.reasoningChars) || 0, wroteFrames: Number(_d.wroteChunks) || 0, heldOpen: out.heldOpen === true, ...upstreamEcho(upRes) });
|
|
160
|
+
_evt("fallback", { reqId, from: actual, to: null, reason: "empty turn" });
|
|
161
|
+
let _errMsg = `EMPTY_MODEL_RESPONSE: upstream returned 200 with no content${_why}${_err}${_hint} — retry or rephrase`;
|
|
162
|
+
if (_d.upstreamErrorText && _d.upstreamErrorText.includes("retryAfterSeconds")) {
|
|
163
|
+
_errMsg = _d.upstreamErrorText;
|
|
164
|
+
}
|
|
165
|
+
return { handled: false, upRes: null, lastErr: { model: actual, upstream: null, status: 502, message: _errMsg, emptyTurn: true, heldOpen: out.heldOpen === true } };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 上报锚点偏移:relay 入口之前消耗掉的等待(上游排队、建连、provider 为取判决预读的首帧)。
|
|
169
|
+
// 只加给 usage 行的时长;闸门计时与 recordModelStats 一律不碰 —— 见 design D2 与 Non-Goals。
|
|
170
|
+
const anchorMs = Number.isFinite(out?.preflightMs) && out.preflightMs > 0 ? out.preflightMs : 0;
|
|
171
|
+
// 5a. 首块超时未写字节 → 回退(显式 timedOut 字段,status 只是 HTTP 语义展示)
|
|
172
|
+
if (out.timedOut === true) {
|
|
173
|
+
const why = out.detail?.upstreamError ? ` (upstream read error: ${out.detail.upstreamError})` : "";
|
|
174
|
+
if (auto) try { await auto.recordError(actual, { status: 502, slow: true, note: `stream timeout ${streamTimeoutMs}ms` }); } catch {}
|
|
175
|
+
try { _logError(actual, 502, `stream timeout ${streamTimeoutMs}ms${why}`); } catch {}
|
|
176
|
+
_evt("upstream-error", { reqId, model: actual, status: 502, message: "stream timeout", timing: null, ...upstreamEcho(upRes) });
|
|
177
|
+
_evt("fallback", { reqId, from: actual, to: null, reason: "stream timeout" });
|
|
178
|
+
return { handled: false, upRes: null, lastErr: { model: actual, upstream: null, status: 502, message: `stream timed out after ${streamTimeoutMs}ms${why}` } };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 5b. 中断(stall 超时 / max 流时长)
|
|
182
|
+
if (out.interrupted) {
|
|
183
|
+
if (auto) {
|
|
184
|
+
try { await auto.recordError(actual, { status: 200, slow: true, note: `stall ${C.STALL_TIMEOUT_MS}ms` }); } catch {}
|
|
185
|
+
try { await auto.recordLatency(actual, out.totalMs ?? (Date.now() - curStartedAt)); } catch {}
|
|
186
|
+
}
|
|
187
|
+
_evt("slow-model", { reqId, model: actual, elapsedMs: out.totalMs ?? (Date.now() - curStartedAt), threshold: C.STALL_TIMEOUT_MS, interrupted: true, detail: out.detail ?? null, ...upstreamEcho(upRes) });
|
|
188
|
+
try { _logCall(actual, 200); } catch {}
|
|
189
|
+
// interrupted 的 200 也是真实消耗(最贵的长生成)——照常落 usage 标 interrupted:1,口径与 5c 一致 — 见 .agents/notes/implemented/feature/2026-09-19-usage-report-jsonl.md
|
|
190
|
+
if (out.status === 200) {
|
|
191
|
+
try {
|
|
192
|
+
const u = out.detail?.usage || null;
|
|
193
|
+
const endMs = Number.isFinite(out.totalMs) && out.totalMs >= 0 ? out.totalMs + anchorMs : (Date.now() - curStartedAt);
|
|
194
|
+
const firstMs = Number.isFinite(out.ttfMs) && out.ttfMs >= 0 ? out.ttfMs + anchorMs : null;
|
|
195
|
+
recordChatUsage({ model: normalizeFullId(actual), via, usage: u, interrupted: 1, ttfbMs: firstMs, totalMs: endMs, tps: null, reasoningChars: out.detail?.reasoningChars ?? 0, stream: Boolean(body?.stream) }).catch(() => {});
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
_evt("result", { reqId, model: actual, status: out.status, via, timing: upRes?._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, interrupted: true, ...upstreamEcho(upRes), detail: out.detail ?? null, fallback, requested, actual });
|
|
199
|
+
_evt("client-response", { requested, actual, via, fallback, status: out.status, reqId, interrupted: true });
|
|
200
|
+
if (plugins?.length) runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(body?.stream), durationMs: Date.now() - curStartedAt, via, status: out.status, actual, interrupted: true, fallback }).catch(() => {});
|
|
201
|
+
return { handled: true, wrotePayload: out.detail?.wrotePayload === true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 5c. 慢速计分 + ok
|
|
205
|
+
const elapsed = Date.now() - curStartedAt;
|
|
206
|
+
const latencyMs = out.totalMs ?? elapsed;
|
|
207
|
+
let scoredSlow = false;
|
|
208
|
+
|
|
209
|
+
if (C.SLOW_TOTAL_MS && auto && elapsed > C.SLOW_TOTAL_MS && out.status === 200) {
|
|
210
|
+
try { await auto.recordError(actual, { status: 200, slow: true, note: `slow ${elapsed}ms` }); } catch {}
|
|
211
|
+
try { await auto.recordLatency(actual, latencyMs); } catch {}
|
|
212
|
+
_evt("slow-model", { reqId, model: actual, elapsedMs: elapsed, threshold: C.SLOW_TOTAL_MS, reason: "total", detail: out.detail ?? null, ...upstreamEcho(upRes) });
|
|
213
|
+
scoredSlow = true;
|
|
214
|
+
}
|
|
215
|
+
if (out.detail?.stallHits > 0 && auto && out.status === 200) {
|
|
216
|
+
try { await auto.recordError(actual, { status: 200, slow: true, note: `stall ${out.detail.stallHits}x gap>${C.SCORE_STALL_MS}ms maxGap ${out.detail.maxGapMs}ms` }); } catch {}
|
|
217
|
+
try { await auto.recordLatency(actual, latencyMs); } catch {}
|
|
218
|
+
_evt("slow-model", { reqId, model: actual, elapsedMs: elapsed, threshold: C.SCORE_STALL_MS, reason: "stall", stallHits: out.detail.stallHits, maxGapMs: out.detail.maxGapMs, detail: out.detail ?? null, ...upstreamEcho(upRes) });
|
|
219
|
+
scoredSlow = true;
|
|
220
|
+
}
|
|
221
|
+
if (!scoredSlow && auto && out.status === 200) {
|
|
222
|
+
try { await auto.recordOk(actual, { latencyMs }); } catch {}
|
|
223
|
+
} else if (!scoredSlow && auto) {
|
|
224
|
+
try { await auto.recordLatency(actual, latencyMs); } catch {}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 每次 8989 正常返回都落体检:count/首字/总耗时/速度(供 -status TopN)
|
|
228
|
+
if (out.status === 200) {
|
|
229
|
+
try {
|
|
230
|
+
const isStream = Boolean(body?.stream);
|
|
231
|
+
// —— 状态口径(-status / -model stats 的终生 EMA):本期刻意不动,仍按转发入口量 ——
|
|
232
|
+
let ttfb = isStream ? (out.ttfMs ?? upRes?._t?.ttfbMs ?? null) : null;
|
|
233
|
+
// out.totalMs 为 0 时(非流式 <1ms 四舍五入)回退到 elapsed/durationMs
|
|
234
|
+
const elapsedFallback = Date.now() - curStartedAt;
|
|
235
|
+
let total = out.totalMs;
|
|
236
|
+
if (!Number.isFinite(total) || total <= 0) total = elapsedFallback;
|
|
237
|
+
if (!Number.isFinite(total) || total <= 0) total = latencyMs;
|
|
238
|
+
if (!Number.isFinite(total) || total <= 0) total = Date.now() - curStartedAt;
|
|
239
|
+
if (isStream && (!Number.isFinite(ttfb) || ttfb <= 0)) ttfb = null;
|
|
240
|
+
const usage = out.detail?.usage || null;
|
|
241
|
+
const chars = out.detail?.chars ?? null;
|
|
242
|
+
const compTok = usage?.completion_tokens ?? null;
|
|
243
|
+
const m = computeMetrics({ ttfbMs: ttfb, totalMs: total, completionTokens: compTok, chars });
|
|
244
|
+
const tps = m.tps ?? m.charsPerSec ?? null;
|
|
245
|
+
const fullId = normalizeFullId(actual);
|
|
246
|
+
recordModelStats(fullId, { ttfbMs: ttfb, totalMs: total, tps, completionTokens: compTok });
|
|
247
|
+
if (fullId !== actual) recordModelStats(actual, { ttfbMs: ttfb, totalMs: total, tps, completionTokens: compTok });
|
|
248
|
+
// —— 报表口径(usage 行 = -stats 的唯一数据源):把 relay 之前消耗掉的等待补回来 ——
|
|
249
|
+
// 首字 = 本次上游尝试起点 → 网关转发首帧;0ms 是有效样本,只有负值判无效;
|
|
250
|
+
// transport 的 _t.ttfbMs 本就从 fetch 起算,不再叠加锚点(否则双计)。
|
|
251
|
+
const rowTtfb = !isStream ? null
|
|
252
|
+
: Number.isFinite(out.ttfMs) ? (out.ttfMs < 0 ? null : out.ttfMs + anchorMs)
|
|
253
|
+
: (Number.isFinite(upRes?._t?.ttfbMs) ? upRes._t.ttfbMs : null);
|
|
254
|
+
const rowTotal = Number.isFinite(total) ? total + anchorMs : total;
|
|
255
|
+
// 速度恒等:首字与总耗时同加一个常数 → (总−首字) 不变,故沿用 m.tps。
|
|
256
|
+
// 窗口报表:逐请求落 usage(行形状由 usage/record.js 拥有,含 prompt/total ——
|
|
257
|
+
// state 的 modelStats 只存 completion 的 EMA)。只按 canonical 名记一次,避免双计。
|
|
258
|
+
recordChatUsage({ model: fullId, via, usage, ttfbMs: rowTtfb, totalMs: rowTotal, tps: m.tps, reasoningChars: out.detail?.reasoningChars ?? 0, stream: isStream }).catch(() => {});
|
|
259
|
+
// cline 旁路记账:流式时 provider 已把账号哈希挂在 upRes 上,这里用消费完的 usage 记一笔。
|
|
260
|
+
// 纯旁路:只调 recordOutput,不改转发/切号/重试;无账号或非 cline 直接跳过。
|
|
261
|
+
if (upRes?.clineAccountId) {
|
|
262
|
+
const clineModel = upRes.clineModel || String(actual).replace(/^cline\//, "");
|
|
263
|
+
const orow = computeOutputRow({ model: clineModel, accountId: upRes.clineAccountId, usage, chars });
|
|
264
|
+
if (orow) recordOutput(orow).catch(() => {});
|
|
265
|
+
}
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
_evt("result", { reqId, model: actual, status: out.status, via, timing: upRes?._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, ...upstreamEcho(upRes), detail: out.detail ?? null, fallback, requested, actual });
|
|
270
|
+
_evt("client-response", { requested, actual, via, fallback, status: out.status, reqId });
|
|
271
|
+
if (plugins?.length) runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(body?.stream), durationMs: Date.now() - curStartedAt, via, status: out.status, actual, fallback }).catch(() => {});
|
|
272
|
+
return { handled: true, wrotePayload: out.detail?.wrotePayload === true };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { execute };
|
|
276
|
+
}
|