mslxdff 0.1.45 → 0.1.55

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.
@@ -1,342 +1,3 @@
1
- import { performance } from "node:perf_hooks";
2
- import { injectReasoningContent, normalizeModel } from "../reasoning.js";
3
- import { isAutoModel } from "../auto.js";
4
- import { clientIp, json, readBody, parseHops, summarizePrompt, errMsg } from "./helpers.js";
5
- import { buildFallbackInfo } from "./fallback.js";
6
- import { relay, SLOW_TOTAL_MS, STREAM_TIMEOUT_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS } from "./stream.js";
7
- import { racePeerCandidates } from "./peers.js";
8
- import { tryBroadbandRelay } from "./relay-queue.js";
9
- import { runHook } from "../plugins.js";
10
-
11
- export async function chatHandler({ req, res, upstream, auto, logs, peers, maxHops, groups, bus, token, plugins }) {
12
- let body;
13
- try {
14
- body = await readBody(req);
15
- } catch {
16
- return json(res, 400, { error: "Invalid JSON body" });
17
- }
18
-
19
- // 插件 hook:request:received — 读到 body 后触发;返回 { respond:{status,body} } 可短路请求
20
- if (plugins?.length) {
21
- const rc = await runHook(plugins, "request:received", { ip: clientIp(req), hops: parseHops(req.headers["x-mslxdff-hops"]), headers: { "content-type": req.headers["content-type"] }, body });
22
- for (const e of rc.errors) logs?.appendEvent?.({ ts: Date.now(), type: "plugin-hook-error", hook: "request:received", plugin: e.plugin, error: e.error });
23
- const respond = rc.value?.respond;
24
- if (respond && typeof respond === "object") {
25
- return json(res, respond.status || 200, respond.body ?? {});
26
- }
27
- }
28
-
29
- const startedAt = Date.now();
30
- const reqId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
31
- const perf0 = performance.now();
32
- const stages = [];
33
- const mark = (name) => stages.push([name, Math.round(performance.now() - perf0)]);
34
- const hops = parseHops(req.headers["x-mslxdff-hops"]);
35
- const lockModel = req.headers["x-mslxdff-model-lock"] || "";
36
- const rawModel = body.model || "";
37
- const requested = normalizeModel(lockModel || rawModel || "");
38
- const useAuto = isAutoModel(requested);
39
- mark("parsed");
40
-
41
- let order;
42
- if (lockModel) {
43
- order = [requested];
44
- } else if (useAuto) {
45
- order = auto ? await auto.candidates() : [""];
46
- } else {
47
- order = auto ? await auto.candidatesFor(requested) : [requested];
48
- }
49
- if (!order.length) order = [""];
50
- const canFallback = order.length > 1;
51
- const canForwardPeers = Boolean(peers) && hops < maxHops;
52
- mark("ordered");
53
-
54
- const logCall = (model, status) =>
55
- logs?.appendCall({ reqId, model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(body.stream), stages });
56
- const logError = (model, status, message) =>
57
- logs?.appendError({ reqId, model, auto: useAuto, status, message, stages });
58
- const evt = (type, data) => {
59
- const entry = { ts: Date.now(), reqId, type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt, stages: [...stages] };
60
- if (bus) bus.emit(entry);
61
- logs?.appendEvent?.(entry);
62
- };
63
- // 插件 hook:request:completed — 每个请求出口触发一次(只观察)
64
- const done = (info) => {
65
- if (!plugins?.length) return;
66
- runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(body.stream), durationMs: Date.now() - startedAt, ...info }).catch(() => {});
67
- };
68
- evt("request", { reqId, hops, ip: clientIp(req), stream: Boolean(body.stream), prompt: summarizePrompt(body), rawModel, requested, lockModel: lockModel || null });
69
- evt("ordered", { reqId, order, canFallback, canForwardPeers, useAuto, statuses: auto?.statuses?.() ?? null });
70
-
71
- // 插件 hook:model:select — 可返回新数组替换候选顺序(lockModel 时不可改)
72
- if (plugins?.length && !lockModel) {
73
- const sel = await runHook(plugins, "model:select", { reqId, requested, useAuto, order: [...order], hops, stream: Boolean(body.stream) });
74
- if (sel.changed && Array.isArray(sel.value) && sel.value.length) {
75
- order = sel.value.filter(Boolean);
76
- if (!order.length) order = [requested];
77
- evt("plugin-hook", { reqId, hook: "model:select", applied: true, order: [...order] });
78
- }
79
- for (const e of sel.errors) evt("plugin-hook-error", { reqId, hook: "model:select", plugin: e.plugin, error: e.error });
80
- }
81
-
82
- const handlerCtx = {
83
- reqId,
84
- model: null,
85
- body,
86
- hops,
87
- peers,
88
- plugins,
89
- evt,
90
- logError,
91
- logCall,
92
- };
93
-
94
- let lastErr = null;
95
- for (let idx = 0; idx < order.length; idx++) {
96
- const model = order[idx];
97
- handlerCtx.model = model;
98
- evt("model-try", { reqId, model, idx, remaining: order.length - idx });
99
- // 插件 hook:model:beforeTry — 返回 false 可跳过该候选
100
- if (plugins?.length) {
101
- const bt = await runHook(plugins, "model:beforeTry", { reqId, requested, model, idx, hops });
102
- for (const e of bt.errors) evt("plugin-hook-error", { reqId, hook: "model:beforeTry", plugin: e.plugin, error: e.error });
103
- if (bt.value === false || bt.value?.skip === true) {
104
- evt("plugin-hook", { reqId, hook: "model:beforeTry", applied: true, skipped: model });
105
- continue;
106
- }
107
- }
108
- let upRes = null;
109
- let forwarded = { ...injectReasoningContent(model, body), model };
110
- // 插件 hook:upstream:request — 返回 { payload } 可替换本次发往上游的负载
111
- if (plugins?.length) {
112
- const ur = await runHook(plugins, "upstream:request", { reqId, requested, model, payload: forwarded, stream: Boolean(body.stream) });
113
- for (const e of ur.errors) evt("plugin-hook-error", { reqId, hook: "upstream:request", plugin: e.plugin, error: e.error });
114
- if (ur.changed && ur.value?.payload && typeof ur.value.payload === "object") {
115
- forwarded = ur.value.payload;
116
- evt("plugin-hook", { reqId, hook: "upstream:request", applied: true, model, rewrittenModel: forwarded.model ?? null });
117
- }
118
- }
119
- const tUp = performance.now();
120
- evt("upstream-try", { reqId, model, attempt: idx + 1 });
121
- try {
122
- upRes = await upstream.chat(forwarded);
123
- evt("upstream-done", { reqId, model, ok: !(upRes instanceof Error) && upRes.status < 400, status: upRes instanceof Error ? null : upRes.status, timing: upRes._t ?? null, error: null });
124
- } catch (err) {
125
- if (auto) await auto.recordError(model, { message: errMsg(err) });
126
- lastErr = { model, upstream: null, status: 502, message: errMsg(err) };
127
- logError(model, 502, errMsg(err));
128
- evt("upstream-error", { reqId, model, status: 502, message: errMsg(err), timing: err._t ?? { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - tUp) } });
129
- }
130
- // 插件 hook:upstream:response — 上游响应(或错误)后观察
131
- if (plugins?.length) {
132
- runHook(plugins, "upstream:response", {
133
- reqId, requested, model,
134
- status: upRes instanceof Error ? null : upRes instanceof Object ? (upRes.status ?? null) : null,
135
- ok: !(upRes instanceof Error) && upRes ? upRes.status < 400 : false,
136
- error: upRes instanceof Error ? errMsg(upRes) : null,
137
- timing: upRes?._t ?? null,
138
- }).catch(() => {});
139
- }
140
- mark(`up-${model}`);
141
- if (upRes && upRes.status >= 400) {
142
- if (auto) await auto.recordError(model, { status: upRes.status });
143
- lastErr = { model, upstream: upRes, status: upRes.status, message: null };
144
- logError(model, upRes.status, `upstream ${upRes.status}`);
145
- evt("upstream-error", { reqId, model, status: upRes.status, message: null, timing: upRes._t ?? null });
146
- upRes = null;
147
- }
148
- if (upRes) {
149
- logCall(model, upRes.status);
150
- const fallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "local", useAuto, lockModel });
151
- if (fallback?.fallback) evt("fallback-notice", { reqId, requested, actual: model, reason: fallback.reason, notice: fallback.notice, via: "local" });
152
- evt("relay-start", { reqId, model, via: "local", isStream: Boolean(body.stream), fallback });
153
- const out = await relay(res, upRes, body, {
154
- fallback,
155
- onFirstChunk: (delta) => {
156
- mark(`ttf-${model}`);
157
- evt("relay-first-chunk", { reqId, model, ttfMs: delta });
158
- if (plugins?.length) runHook(plugins, "relay:first-chunk", { reqId, requested, model, via: "local", ttfMs: delta }).catch(() => {});
159
- },
160
- onDownstreamAbort: () => {
161
- evt("client-abort", { reqId, model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] });
162
- },
163
- });
164
- evt("relay-done", { reqId, model, via: "local", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
165
- if (out.status === STREAM_TIMEOUT_MS) {
166
- if (auto) await auto.recordError(model, { status: 502, slow: true, note: `stream timeout ${STREAM_TIMEOUT_MS}ms` });
167
- lastErr = { model, upstream: null, status: 502, message: `stream timed out after ${STREAM_TIMEOUT_MS}ms` };
168
- logError(model, 502, `stream timeout ${STREAM_TIMEOUT_MS}ms`);
169
- evt("upstream-error", { reqId, model, status: 502, message: "stream timeout", timing: null });
170
- evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: "stream timeout" });
171
- upRes = null;
172
- continue;
173
- }
174
- if (out.interrupted) {
175
- if (auto) {
176
- await auto.recordError(model, { status: 200, slow: true, note: `stall ${STALL_TIMEOUT_MS}ms` });
177
- await auto.recordLatency(model, out.totalMs ?? (Date.now() - startedAt));
178
- }
179
- evt("slow-model", { model, elapsedMs: out.totalMs ?? (Date.now() - startedAt), threshold: STALL_TIMEOUT_MS, interrupted: true, detail: out.detail ?? null });
180
- logCall(model, 200);
181
- evt("result", { model, status: out.status, via: "local", timing: upRes._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, interrupted: true, detail: out.detail ?? null, fallback, requested, actual: model });
182
- evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, interrupted: true, reqId });
183
- done({ via: "local", status: out.status, actual: model, interrupted: true, fallback });
184
- return;
185
- }
186
- const elapsed = Date.now() - startedAt;
187
- const latencyMs = out.totalMs ?? elapsed;
188
- let scoredSlow = false;
189
- if (SLOW_TOTAL_MS && auto && elapsed > SLOW_TOTAL_MS && out.status === 200) {
190
- void auto.recordError(model, { status: 200, slow: true, note: `slow ${elapsed}ms` });
191
- void auto.recordLatency(model, latencyMs);
192
- evt("slow-model", { model, elapsedMs: elapsed, threshold: SLOW_TOTAL_MS, reason: "total", detail: out.detail ?? null });
193
- scoredSlow = true;
194
- }
195
- if (out.detail?.stallHits > 0 && auto && out.status === 200) {
196
- void auto.recordError(model, { status: 200, slow: true, note: `stall ${out.detail.stallHits}x gap>${SCORE_STALL_MS}ms maxGap ${out.detail.maxGapMs}ms` });
197
- void auto.recordLatency(model, latencyMs);
198
- evt("slow-model", { model, elapsedMs: elapsed, threshold: SCORE_STALL_MS, reason: "stall", stallHits: out.detail.stallHits, maxGapMs: out.detail.maxGapMs, detail: out.detail ?? null });
199
- scoredSlow = true;
200
- }
201
- if (!scoredSlow && auto && out.status === 200) {
202
- await auto.recordOk(model, { latencyMs });
203
- } else if (!scoredSlow && auto) {
204
- await auto.recordLatency(model, latencyMs);
205
- }
206
- evt("result", { model, status: out.status, via: "local", timing: upRes._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null, fallback, requested, actual: model });
207
- evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, reqId });
208
- done({ via: "local", status: out.status, actual: model, fallback });
209
- return;
210
- }
211
-
212
- if (canForwardPeers) {
213
- evt("peer-race-start", { reqId, model, peers: peers.ordered().length });
214
- const win =
215
- (await racePeerCandidates(peers.ordered(), handlerCtx)) ||
216
- (await racePeerCandidates(peers.orderedByLastError(), handlerCtx));
217
- if (win) {
218
- evt("peer-race-win", { reqId, model, winPeer: win.peer.url, winTarget: win.target, latencyMs: win.latencyMs });
219
- await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
220
- logCall(win.target, win.res.status);
221
- const peerFallback = buildFallbackInfo({ requested, actual: win.target, lastErr, via: "peer", useAuto, lockModel });
222
- if (peerFallback?.fallback) evt("fallback-notice", { reqId, requested, actual: win.target, reason: peerFallback.reason, notice: peerFallback.notice, via: "peer" });
223
- evt("relay-start", { reqId, model: win.target, via: "peer", isStream: Boolean(body.stream), fallback: peerFallback });
224
- const out = await relay(res, win.res, body, {
225
- fallback: peerFallback,
226
- onFirstChunk: (d) => mark(`ttf-peer-${win.target}`),
227
- onDownstreamAbort: () => evt("client-abort", { reqId, model: win.target, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
228
- });
229
- evt("relay-done", { reqId, model: win.target, via: "peer", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
230
- if (auto && out.status === 200) {
231
- const latencyMs = out.totalMs ?? win.latencyMs;
232
- if (out.detail?.stallHits > 0 || (latencyMs && latencyMs > SLOW_TOTAL_MS)) {
233
- void auto.recordError(win.target, { status: 200, slow: true, note: `peer slow ${latencyMs}ms` });
234
- void auto.recordLatency(win.target, latencyMs);
235
- } else {
236
- await auto.recordOk(win.target, { latencyMs });
237
- }
238
- }
239
- evt("result", { model: win.target, status: out.status, via: "peer", timing: win.res._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null, fallback: peerFallback, requested, actual: win.target });
240
- evt("client-response", { requested, actual: win.target, via: "peer", fallback: peerFallback, status: out.status, reqId });
241
- done({ via: "peer", status: out.status, actual: win.target, fallback: peerFallback });
242
- return;
243
- }
244
- evt("peer-race-lose", { reqId, model });
245
- }
246
-
247
- if (groups) {
248
- const bb = await tryBroadbandRelay({ groups, token, model, body, hops, bus, logs, reqId, evt, res, mark, perf0, stages });
249
- if (bb) {
250
- const isResponse = bb.result && typeof bb.result.status === "number" && typeof bb.result.headers?.get === "function";
251
- if (isResponse) {
252
- const bbFallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "broadband", useAuto, lockModel });
253
- if (bbFallback?.fallback) evt("fallback-notice", { reqId, requested, actual: model, reason: bbFallback.reason, notice: bbFallback.notice, via: "broadband" });
254
- evt("relay-start", { reqId, model, via: "broadband", target: bb.target, group: bb.group, fallback: bbFallback });
255
- const out = await relay(res, bb.result, body, {
256
- fallback: bbFallback,
257
- onFirstChunk: (d) => mark(`ttf-bb-${model}`),
258
- onDownstreamAbort: () => evt("client-abort", { reqId, model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
259
- });
260
- evt("relay-done", { reqId, model, via: "broadband", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
261
- if (auto && out.status === 200) {
262
- const latencyMs = out.totalMs ?? 0;
263
- if (out.detail?.stallHits > 0 || (latencyMs && latencyMs > SLOW_TOTAL_MS)) {
264
- void auto.recordError(model, { status: 200, slow: true, note: `broadband slow ${latencyMs}ms` });
265
- void auto.recordLatency(model, latencyMs);
266
- } else {
267
- await auto.recordOk(model, { latencyMs });
268
- }
269
- }
270
- evt("result", { model, status: out.status, via: "broadband", timing: bb.result._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null, fallback: bbFallback, requested, actual: model });
271
- evt("client-response", { requested, actual: model, via: "broadband", fallback: bbFallback, status: out.status, reqId });
272
- done({ via: "broadband", status: out.status, actual: model, fallback: bbFallback });
273
- return;
274
- } else if (bb.result && typeof bb.result.status === "number") {
275
- const fakeRes = {
276
- status: bb.result.status,
277
- headers: { get: (k) => bb.result.headers?.[k] || bb.result.headers?.[k.toLowerCase()] || null },
278
- text: async () => typeof bb.result.body === "string" ? bb.result.body : JSON.stringify(bb.result.body),
279
- body: (() => {
280
- const b = bb.result.body || "";
281
- const str = typeof b === "string" ? b : JSON.stringify(b);
282
- const isSSE = bb.result.headers?.["Content-Type"]?.includes("text/event-stream");
283
- if (isSSE) {
284
- return (async function* () { yield Buffer.from(str); })();
285
- }
286
- return null;
287
- })(),
288
- };
289
- const bbLocalFallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "broadband", useAuto, lockModel });
290
- if (bbLocalFallback?.fallback) evt("fallback-notice", { reqId, requested, actual: model, reason: bbLocalFallback.reason, notice: bbLocalFallback.notice, via: "broadband" });
291
- evt("relay-start", { reqId, model, via: "broadband-local", target: bb.target, group: bb.group, fallback: bbLocalFallback });
292
- const out = await relay(res, fakeRes, body, {
293
- fallback: bbLocalFallback,
294
- onFirstChunk: (d) => mark(`ttf-bb-${model}`),
295
- onDownstreamAbort: () => evt("client-abort", { reqId, model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
296
- });
297
- evt("relay-done", { reqId, model, via: "broadband-local", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
298
- evt("result", { model, status: out.status, via: "broadband", timing: null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null, fallback: bbLocalFallback, requested, actual: model });
299
- evt("client-response", { requested, actual: model, via: "broadband", fallback: bbLocalFallback, status: out.status, reqId });
300
- done({ via: "broadband", status: out.status, actual: model, fallback: bbLocalFallback });
301
- return;
302
- }
303
- }
304
- evt("relay-miss", { reqId, model });
305
- }
306
-
307
- if (canFallback) {
308
- evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: lastErr?.message || `upstream ${lastErr?.status ?? 502}` });
309
- continue;
310
- }
311
- evt("exhausted-local", { reqId, lastModel: lastErr?.model ?? model, lastStatus: lastErr?.status ?? 502, order });
312
- logCall(lastErr?.model ?? model, lastErr?.status ?? 502);
313
- if (lastErr?.upstream) {
314
- evt("relay-start", { reqId, model: lastErr.model, via: "local-exhausted", isStream: Boolean(body.stream) });
315
- const out = await relay(res, lastErr.upstream, body, {
316
- onFirstChunk: (d) => mark(`ttf-${lastErr.model}`),
317
- onDownstreamAbort: () => evt("client-abort", { reqId, model: lastErr.model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
318
- });
319
- evt("relay-done", { reqId, model: lastErr.model, via: "local-exhausted", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
320
- evt("result", { reqId, model: lastErr.model, status: out.status, via: "local", timing: lastErr.upstream._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null });
321
- return;
322
- }
323
- evt("result", { reqId, model, status: lastErr?.status ?? 502, via: "none", timing: null });
324
- done({ via: "none", status: lastErr?.status ?? 502, error: lastErr?.message || "all auto models failed" });
325
- return json(res, 502, { error: lastErr?.message || "all auto models failed" });
326
- }
327
-
328
- evt("exhausted-all", { reqId, lastModel: lastErr?.model ?? requested, lastStatus: lastErr?.status ?? 502, order });
329
- logCall(lastErr?.model ?? requested, lastErr?.status ?? 502);
330
- if (lastErr?.upstream) {
331
- evt("relay-start", { reqId, model: lastErr.model, via: "local-final", isStream: Boolean(body.stream) });
332
- const out = await relay(res, lastErr.upstream, body, {
333
- onFirstChunk: (d) => mark(`ttf-${lastErr.model}`),
334
- onDownstreamAbort: () => evt("client-abort", { reqId, model: lastErr.model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
335
- });
336
- evt("relay-done", { reqId, model: lastErr.model, via: "local-final", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
337
- evt("result", { reqId, model: lastErr.model, status: out.status, via: "local", timing: lastErr.upstream._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null });
338
- return;
339
- }
340
- evt("result", { reqId, model: lastErr?.model ?? requested, status: lastErr?.status ?? 502, via: "none", timing: null });
341
- return json(res, 502, { error: lastErr?.message || "all auto models failed" });
342
- }
1
+ // Facade — chat 逻辑已拆至 ./chat/ 子模块,按需加载保持单文件 <10KB
2
+ export * from "./chat/index.js";
3
+ export { chatHandler } from "./chat/index.js";
@@ -0,0 +1,251 @@
1
+ function hedgeDelayMs() {
2
+ const raw = process.env.MSLXDFF_HEDGE_DELAY_MS;
3
+ if (raw === undefined || raw === null || raw === "") return 1000;
4
+ const s = String(raw).trim().toLowerCase();
5
+ if (s === "0" || s === "off" || s === "false" || s === "no" || s === "disable" || s === "disabled") return 0;
6
+ const n = Number(s);
7
+ if (Number.isInteger(n) && n >= 0) return n;
8
+ return 1000;
9
+ }
10
+
11
+ function isFastFailStatus(status) {
12
+ const s = Number(status);
13
+ return s === 429 || s === 502 || s === 503 || s === 504;
14
+ }
15
+
16
+ function shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers }) {
17
+ if (!isStream) return false;
18
+ if (!canForwardPeers) return false;
19
+ if (!hasPeers) return false;
20
+ if (!d || d <= 0) return false;
21
+ return true;
22
+ }
23
+
24
+ function withTimeout(promise, ms) {
25
+ if (ms <= 0) return promise;
26
+ let timer;
27
+ const timeout = new Promise((_, rej) => {
28
+ timer = setTimeout(() => rej(new Error("hedge-timeout")), ms);
29
+ });
30
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
31
+ }
32
+
33
+ function makeBufferedBody(chunk, handle, kind) {
34
+ if (kind === "web") {
35
+ const reader = handle;
36
+ return (async function* () {
37
+ yield chunk;
38
+ while (true) {
39
+ const { value, done } = await reader.read();
40
+ if (done) break;
41
+ yield value;
42
+ }
43
+ })();
44
+ }
45
+ const it = handle;
46
+ return (async function* () {
47
+ yield chunk;
48
+ for await (const c of { [Symbol.asyncIterator]: () => it }) {
49
+ yield c;
50
+ }
51
+ })();
52
+ }
53
+
54
+ async function cancelBody(body, handle, kind) {
55
+ try {
56
+ if (body && typeof body.cancel === "function") await body.cancel().catch(() => {});
57
+ if (kind === "web" && handle && typeof handle.cancel === "function") await handle.cancel().catch(() => {});
58
+ else if (handle && typeof handle.return === "function") await handle.return().catch(() => {});
59
+ } catch {}
60
+ }
61
+
62
+ function getFirstChunkPromise(body) {
63
+ if (!body) throw new Error("no-body");
64
+ if (typeof body.getReader === "function") {
65
+ const reader = body.getReader();
66
+ const p = reader.read().then((r) => {
67
+ if (r.done) throw new Error("no-chunk");
68
+ return { chunk: r.value, handle: reader, kind: "web", reader };
69
+ });
70
+ return { promise: p, handle: reader, kind: "web" };
71
+ }
72
+ const it = body[Symbol.asyncIterator]?.();
73
+ if (!it) throw new Error("no-iterator");
74
+ const p = it.next().then((r) => {
75
+ if (r.done) throw new Error("no-chunk");
76
+ return { chunk: r.value, handle: it, kind: "node", it };
77
+ });
78
+ return { promise: p, handle: it, kind: "node" };
79
+ }
80
+
81
+ export { hedgeDelayMs, isFastFailStatus, shouldHedge };
82
+
83
+ export async function hedgedFirstChunkRace({
84
+ localUpRes,
85
+ peers,
86
+ handlerCtx,
87
+ hedgeDelayMs: hedgeDelay,
88
+ evt,
89
+ }) {
90
+ const t0 = Date.now();
91
+ const debugHedge = process.env.MSLXDFF_DEBUG === "1";
92
+ if (debugHedge) try { console.log(`[hedge] start delay=${hedgeDelay} peers=${peers?.ordered?.().length ?? 0} hasBody=${Boolean(localUpRes.body)} getReader=${typeof localUpRes.body?.getReader}`); } catch {}
93
+ let localHandle = null;
94
+ let localKind = null;
95
+ let localBody = localUpRes.body;
96
+ let localFirstPromise = null;
97
+ try {
98
+ const { promise, handle, kind } = getFirstChunkPromise(localBody);
99
+ localHandle = handle;
100
+ localKind = kind;
101
+ localFirstPromise = promise;
102
+ } catch (e) {
103
+ evt?.("hedge-trigger", { reason: "fast-fail-no-body", hedgeDelayMs: hedgeDelay, triggerAtMs: Date.now() - t0 });
104
+ return { needsPeer: true };
105
+ }
106
+
107
+ try {
108
+ const local = await withTimeout(localFirstPromise, hedgeDelay);
109
+ if (debugHedge) try { console.log(`[hedge] local win before hedge ttf=${Date.now() - t0}ms`); } catch {}
110
+ evt?.("hedge-win", { via: "local", reason: "no-hedge-needed", ttfMs: Date.now() - t0, hedgeDelayMs: hedgeDelay });
111
+ return {
112
+ winner: "local",
113
+ upRes: localUpRes,
114
+ bufferedBody: makeBufferedBody(local.chunk, local.handle, local.kind),
115
+ peerInfo: null,
116
+ ttfMs: Date.now() - t0,
117
+ hedged: false,
118
+ };
119
+ } catch (err) {
120
+ if (err.message !== "hedge-timeout") {
121
+ if (debugHedge) try { console.log(`[hedge] local fast-fail ${err.message}`); } catch {}
122
+ evt?.("hedge-trigger", { reason: "fast-fail", hedgeDelayMs: hedgeDelay, triggerAtMs: Date.now() - t0, error: String(err.message).slice(0, 200) });
123
+ return { needsPeer: true, localError: err };
124
+ }
125
+ }
126
+
127
+ if (debugHedge) try { console.log(`[hedge] trigger slow hedge after ${hedgeDelay}ms`); } catch {}
128
+ evt?.("hedge-trigger", { reason: "slow", hedgeDelayMs: hedgeDelay, triggerAtMs: Date.now() - t0 });
129
+
130
+ const { racePeerCandidates } = await import("./peers.js");
131
+ const candidates = handlerCtx.peers.ordered();
132
+ let peerWin = null;
133
+ try {
134
+ peerWin =
135
+ (await racePeerCandidates(candidates, handlerCtx)) ||
136
+ (await racePeerCandidates(handlerCtx.peers.orderedByLastError(), handlerCtx));
137
+ } catch (_) {
138
+ peerWin = null;
139
+ }
140
+ if (debugHedge) try { console.log(`[hedge] peerWin=${peerWin ? peerWin.peer.url : "none"}`); } catch {}
141
+
142
+ if (!peerWin) {
143
+ const remaining = 25_000 - hedgeDelay;
144
+ try {
145
+ const local = await withTimeout(localFirstPromise, Math.max(0, remaining));
146
+ evt?.("hedge-win", { via: "local", reason: "peer-miss", ttfMs: Date.now() - t0, hedgeDelayMs: hedgeDelay });
147
+ if (debugHedge) try { console.log(`[hedge] local win after peer miss`); } catch {}
148
+ return {
149
+ winner: "local",
150
+ upRes: localUpRes,
151
+ bufferedBody: makeBufferedBody(local.chunk, local.handle, local.kind),
152
+ peerInfo: null,
153
+ ttfMs: Date.now() - t0,
154
+ hedged: true,
155
+ };
156
+ } catch (e) {
157
+ evt?.("hedge-lose", { via: "local", reason: "both-fail", error: String(e.message).slice(0, 200) });
158
+ await cancelBody(localBody, localHandle, localKind);
159
+ return null;
160
+ }
161
+ }
162
+
163
+ let peerHandle = null;
164
+ let peerKind = null;
165
+ let peerBody = peerWin.res.body;
166
+ let peerFirstPromise = null;
167
+ try {
168
+ const { promise, handle, kind } = getFirstChunkPromise(peerBody);
169
+ peerHandle = handle;
170
+ peerKind = kind;
171
+ peerFirstPromise = promise.then((r) => ({ ...r, peer: peerWin }));
172
+ } catch (e) {
173
+ evt?.("hedge-lose", { via: "peer", reason: "peer-no-body", error: String(e.message).slice(0, 200) });
174
+ try {
175
+ const local = await localFirstPromise;
176
+ return {
177
+ winner: "local",
178
+ upRes: localUpRes,
179
+ bufferedBody: makeBufferedBody(local.chunk, local.handle, local.kind),
180
+ peerInfo: null,
181
+ ttfMs: Date.now() - t0,
182
+ hedged: true,
183
+ };
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+
189
+ try {
190
+ const winner = await Promise.race([
191
+ localFirstPromise.then((local) => ({ via: "local", data: { ...local, kind: localKind, handle: localHandle } })),
192
+ peerFirstPromise.then((peer) => ({ via: "peer", data: peer })),
193
+ ]);
194
+ if (debugHedge) try { console.log(`[hedge] race winner=${winner.via}`); } catch {}
195
+
196
+ if (winner.via === "local") {
197
+ await cancelBody(peerBody, peerHandle, peerKind);
198
+ evt?.("hedge-win", { via: "local", reason: "race", ttfMs: Date.now() - t0, hedgeDelayMs: hedgeDelay, peer: peerWin.peer.url });
199
+ evt?.("hedge-loser-cancel", { via: "peer", peer: peerWin.peer.url });
200
+ return {
201
+ winner: "local",
202
+ upRes: localUpRes,
203
+ bufferedBody: makeBufferedBody(winner.data.chunk, winner.data.handle, winner.data.kind),
204
+ peerInfo: null,
205
+ ttfMs: Date.now() - t0,
206
+ hedged: true,
207
+ };
208
+ } else {
209
+ await cancelBody(localBody, localHandle, localKind);
210
+ evt?.("hedge-win", { via: "peer", reason: "race", ttfMs: Date.now() - t0, hedgeDelayMs: hedgeDelay, peer: winner.data.peer.peer.url });
211
+ evt?.("hedge-loser-cancel", { via: "local" });
212
+ return {
213
+ winner: "peer",
214
+ upRes: winner.data.peer.res,
215
+ bufferedBody: makeBufferedBody(winner.data.chunk, winner.data.handle, winner.data.kind),
216
+ peerInfo: winner.data.peer,
217
+ ttfMs: Date.now() - t0,
218
+ hedged: true,
219
+ };
220
+ }
221
+ } catch (e) {
222
+ if (debugHedge) try { console.log(`[hedge] race both failed, fallback sequential`); } catch {}
223
+ try {
224
+ const local = await localFirstPromise;
225
+ await cancelBody(peerBody, peerHandle, peerKind);
226
+ return {
227
+ winner: "local",
228
+ upRes: localUpRes,
229
+ bufferedBody: makeBufferedBody(local.chunk, local.handle, local.kind),
230
+ peerInfo: null,
231
+ ttfMs: Date.now() - t0,
232
+ hedged: true,
233
+ };
234
+ } catch {
235
+ try {
236
+ const peer = await peerFirstPromise;
237
+ await cancelBody(localBody, localHandle, localKind);
238
+ return {
239
+ winner: "peer",
240
+ upRes: peer.peer.res,
241
+ bufferedBody: makeBufferedBody(peer.chunk, peer.handle, peer.kind),
242
+ peerInfo: peer.peer,
243
+ ttfMs: Date.now() - t0,
244
+ hedged: true,
245
+ };
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+ }
251
+ }
@@ -6,23 +6,57 @@ import { runHook } from "../plugins.js";
6
6
  const PEER_TIMEOUT_MS = 30_000;
7
7
  const PEER_STATUS_TIMEOUT_MS = 2_000;
8
8
 
9
+ function peerHealthTtlMs() {
10
+ const n = Number(process.env.MSLXDFF_PEER_HEALTH_TTL_MS);
11
+ return Number.isInteger(n) && n >= 0 ? n : 30_000;
12
+ }
13
+
14
+ const healthCache = new Map(); // url -> { at, data }
15
+ const healthInflight = new Map(); // url -> Promise
16
+
17
+ export function clearPeerHealthCache() {
18
+ healthCache.clear();
19
+ healthInflight.clear();
20
+ }
21
+
9
22
  export async function peerHealthyModels(peer, { timeoutMs = PEER_STATUS_TIMEOUT_MS, fetchImpl = fetch } = {}) {
10
- try {
11
- const res = await fetchImpl(`${peer.url}/v1/models/status`, {
12
- headers: {
13
- "Authorization": `Bearer ${peer.token || ""}`,
14
- "Accept": "application/json",
15
- },
16
- signal: AbortSignal.timeout(timeoutMs),
17
- });
18
- if (!res.ok) return [];
19
- const j = await res.json().catch(() => ({}));
20
- return (j.data || [])
21
- .filter((m) => m && typeof m.id === "string" && m.status === "normal")
22
- .map((m) => m.id);
23
- } catch {
24
- return [];
23
+ const key = peer?.url || "";
24
+ const ttl = peerHealthTtlMs();
25
+ if (ttl > 0) {
26
+ const hit = healthCache.get(key);
27
+ if (hit && Date.now() - hit.at < ttl) return hit.data;
28
+ const inflight = healthInflight.get(key);
29
+ if (inflight) return inflight;
30
+ }
31
+ const p = (async () => {
32
+ try {
33
+ const res = await fetchImpl(`${peer.url}/v1/models/status`, {
34
+ headers: {
35
+ "Authorization": `Bearer ${peer.token || ""}`,
36
+ "Accept": "application/json",
37
+ },
38
+ signal: AbortSignal.timeout(timeoutMs),
39
+ });
40
+ if (!res.ok) return [];
41
+ const j = await res.json().catch(() => ({}));
42
+ return (j.data || [])
43
+ .filter((m) => m && typeof m.id === "string" && m.status === "normal")
44
+ .map((m) => m.id);
45
+ } catch {
46
+ return [];
47
+ }
48
+ })();
49
+ if (ttl > 0) {
50
+ healthInflight.set(key, p);
51
+ try {
52
+ const data = await p;
53
+ healthCache.set(key, { at: Date.now(), data });
54
+ return data;
55
+ } finally {
56
+ healthInflight.delete(key);
57
+ }
25
58
  }
59
+ return p;
26
60
  }
27
61
 
28
62
  async function forwardToPeer(peer, body, model, hops) {