mslxdff 0.1.124 → 0.1.125
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 +1 -1
- package/src/routes/peers.js +28 -5
package/package.json
CHANGED
package/src/routes/peers.js
CHANGED
|
@@ -8,6 +8,12 @@ import { compatFetch, timeoutSignal, getUndici } from "../compat.js";
|
|
|
8
8
|
const PEER_TIMEOUT_MS = 30_000;
|
|
9
9
|
const PEER_STATUS_TIMEOUT_MS = 2_000;
|
|
10
10
|
const DEFAULT_PEER_CONNECT_TIMEOUT_MS = 3_000;
|
|
11
|
+
// SSE 响应体数据间隔超时(undici bodyTimeout)。慢模型首块后可静默思考远超 30s,
|
|
12
|
+
// 沿用 30s 会把组内转发的中继活流掐成 "terminated"(实测首块后 ~31s 必断,本地直连
|
|
13
|
+
// 无此限制——组内因此比直连"不流畅")。与应用层 MAX_STREAM_MS(120s) 对齐:
|
|
14
|
+
// undici 层只做最后兜底,掐流交给应用层的首块/总时长策略。
|
|
15
|
+
const PEER_BODY_TIMEOUT_MS = 120_000;
|
|
16
|
+
export { PEER_BODY_TIMEOUT_MS };
|
|
11
17
|
|
|
12
18
|
function peerConnectTimeoutMs() {
|
|
13
19
|
const n = Number(process.env.MSLXDFF_PEER_CONNECT_TIMEOUT_MS);
|
|
@@ -25,7 +31,7 @@ function getPeerDispatcher() {
|
|
|
25
31
|
peerDispatcher = new Agent({
|
|
26
32
|
connect: { timeout: peerConnectTimeoutMs() },
|
|
27
33
|
headersTimeout: PEER_TIMEOUT_MS,
|
|
28
|
-
bodyTimeout:
|
|
34
|
+
bodyTimeout: PEER_BODY_TIMEOUT_MS,
|
|
29
35
|
keepAliveTimeout: 30_000,
|
|
30
36
|
keepAliveMaxTimeout: 60_000,
|
|
31
37
|
});
|
|
@@ -104,8 +110,9 @@ export async function peerHealthyModels(peer, { timeoutMs = PEER_STATUS_TIMEOUT_
|
|
|
104
110
|
return p;
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
// controller 由调用方持有:赛跑 winner 确定后可主动 abort 其余 in-flight,
|
|
114
|
+
// 避免迟到流挂满 bodyTimeout(并给对方省下无用负载)。30s timer 仅覆盖"等响应头"。
|
|
115
|
+
async function forwardToPeer(peer, body, model, hops, controller = new AbortController()) {
|
|
109
116
|
const timer = setTimeout(() => controller.abort(), PEER_TIMEOUT_MS);
|
|
110
117
|
try {
|
|
111
118
|
const headers = {
|
|
@@ -178,23 +185,39 @@ export async function racePeerCandidates(candidates, ctx) {
|
|
|
178
185
|
const completed = await new Promise((resolve) => {
|
|
179
186
|
const order = [];
|
|
180
187
|
const total = prepared.length;
|
|
188
|
+
const ctrls = new Map(); // url -> controller(winner 后统一取消其余)
|
|
181
189
|
let settled = false;
|
|
182
190
|
const finish = (winner) => {
|
|
183
191
|
if (settled) return;
|
|
184
192
|
settled = true;
|
|
193
|
+
if (winner) {
|
|
194
|
+
for (const c of ctrls.values()) {
|
|
195
|
+
if (c === winner.ctrl) continue;
|
|
196
|
+
c.killedByUs = true;
|
|
197
|
+
c.abort();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
185
200
|
resolve({ list: order, winner });
|
|
186
201
|
};
|
|
187
202
|
for (const { peer, target } of prepared) {
|
|
188
203
|
tried.add(peer.url);
|
|
204
|
+
const ctrl = new AbortController();
|
|
205
|
+
ctrls.set(peer.url, ctrl);
|
|
189
206
|
ctx.evt("peer-request", { peer: peer.url, model: target, hops: ctx.hops + 1 });
|
|
190
207
|
// 插件 hook:peer:beforeForward — 转发给组员前观察
|
|
191
208
|
if (ctx.plugins?.length) {
|
|
192
209
|
runHook(ctx.plugins, "peer:beforeForward", { reqId: ctx.reqId, peer: peer.url, model: target, hops: ctx.hops + 1 }).catch(() => {});
|
|
193
210
|
}
|
|
194
211
|
const t0 = performance.now();
|
|
195
|
-
forwardToPeer(peer, ctx.body, target, ctx.hops).then((res) => {
|
|
212
|
+
forwardToPeer(peer, ctx.body, target, ctx.hops, ctrl).then((res) => {
|
|
196
213
|
const latencyMs = Math.round(performance.now() - t0);
|
|
197
214
|
const failed = res instanceof Error || res.status >= 400;
|
|
215
|
+
// winner 已定后被我们主动取消:静默,不记账不报错(否则污染健康统计与错误日志)
|
|
216
|
+
if (res instanceof Error && ctrl.killedByUs) {
|
|
217
|
+
order.push({ ok: false, peer, target, res, status: 0, killed: true });
|
|
218
|
+
if (order.length === total) finish(null);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
198
221
|
ctx.evt("peer-forward", { peer: peer.url, model: target, hops: ctx.hops + 1, latencyMs, ok: !failed });
|
|
199
222
|
// 插件 hook:peer:result — 组员响应后观察
|
|
200
223
|
if (ctx.plugins?.length) {
|
|
@@ -211,7 +234,7 @@ export async function racePeerCandidates(candidates, ctx) {
|
|
|
211
234
|
recordLater(() => ctx.peers.recordResult(peer.url, { ok: false }));
|
|
212
235
|
if (order.length === total) finish(null);
|
|
213
236
|
} else {
|
|
214
|
-
const entry = { ok: true, peer, target, res, latencyMs };
|
|
237
|
+
const entry = { ok: true, peer, target, res, latencyMs, ctrl };
|
|
215
238
|
order.push(entry);
|
|
216
239
|
if (!settled) {
|
|
217
240
|
// 第一个成功立即返回:不等慢/黑洞候选(迟到者的记账由各分支自理)
|