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.
@@ -0,0 +1,178 @@
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 { hedgeDelayMs, shouldHedge } from "../hedge.js";
6
+ import { runHook } from "../../plugins.js";
7
+ import { handleHedge } from "./hedge-handler.js";
8
+ import { handleLocalRelay } from "./local-handler.js";
9
+ import { handlePeerRelay } from "./peer-handler.js";
10
+ import { handleBroadbandRelay } from "./broadband-handler.js";
11
+ import { handleExhaustedLocal, handleExhaustedAll } from "./exhausted-handler.js";
12
+
13
+ export async function chatHandler({ req, res, upstream, auto, logs, peers, maxHops, groups, bus, token, plugins }) {
14
+ let body;
15
+ try {
16
+ body = await readBody(req);
17
+ } catch {
18
+ return json(res, 400, { error: "Invalid JSON body" });
19
+ }
20
+
21
+ if (plugins?.length) {
22
+ 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 });
23
+ for (const e of rc.errors) logs?.appendEvent?.({ ts: Date.now(), type: "plugin-hook-error", hook: "request:received", plugin: e.plugin, error: e.error });
24
+ const respond = rc.value?.respond;
25
+ if (respond && typeof respond === "object") {
26
+ return json(res, respond.status || 200, respond.body ?? {});
27
+ }
28
+ }
29
+
30
+ const startedAt = Date.now();
31
+ const reqId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
32
+ const perf0 = performance.now();
33
+ const stages = [];
34
+ const mark = (name) => stages.push([name, Math.round(performance.now() - perf0)]);
35
+ const hops = parseHops(req.headers["x-mslxdff-hops"]);
36
+ const lockModel = req.headers["x-mslxdff-model-lock"] || "";
37
+ const rawModel = body.model || "";
38
+ const requested = normalizeModel(lockModel || rawModel || "");
39
+ const useAuto = isAutoModel(requested);
40
+ mark("parsed");
41
+
42
+ let order;
43
+ if (lockModel) {
44
+ order = [requested];
45
+ } else if (useAuto) {
46
+ order = auto ? await auto.candidates() : [""];
47
+ } else {
48
+ order = auto ? await auto.candidatesFor(requested) : [requested];
49
+ }
50
+ if (!order.length) order = [""];
51
+ const canFallback = order.length > 1;
52
+ const canForwardPeers = Boolean(peers) && hops < maxHops;
53
+ mark("ordered");
54
+
55
+ const logCall = (model, status) =>
56
+ logs?.appendCall({ reqId, model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(body.stream), stages });
57
+ const logError = (model, status, message) =>
58
+ logs?.appendError({ reqId, model, auto: useAuto, status, message, stages });
59
+ const evt = (type, data) => {
60
+ const entry = { ts: Date.now(), reqId, type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt, stages: [...stages] };
61
+ if (bus) bus.emit(entry);
62
+ logs?.appendEvent?.(entry);
63
+ };
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
+ if (plugins?.length && !lockModel) {
72
+ const sel = await runHook(plugins, "model:select", { reqId, requested, useAuto, order: [...order], hops, stream: Boolean(body.stream) });
73
+ if (sel.changed && Array.isArray(sel.value) && sel.value.length) {
74
+ order = sel.value.filter(Boolean);
75
+ if (!order.length) order = [requested];
76
+ evt("plugin-hook", { reqId, hook: "model:select", applied: true, order: [...order] });
77
+ }
78
+ for (const e of sel.errors) evt("plugin-hook-error", { reqId, hook: "model:select", plugin: e.plugin, error: e.error });
79
+ }
80
+
81
+ const handlerCtx = { reqId, model: null, body, hops, peers, plugins, evt, logError, logCall };
82
+
83
+ let lastErr = null;
84
+ for (let idx = 0; idx < order.length; idx++) {
85
+ const model = order[idx];
86
+ handlerCtx.model = model;
87
+ evt("model-try", { reqId, model, idx, remaining: order.length - idx });
88
+ if (plugins?.length) {
89
+ const bt = await runHook(plugins, "model:beforeTry", { reqId, requested, model, idx, hops });
90
+ for (const e of bt.errors) evt("plugin-hook-error", { reqId, hook: "model:beforeTry", plugin: e.plugin, error: e.error });
91
+ if (bt.value === false || bt.value?.skip === true) {
92
+ evt("plugin-hook", { reqId, hook: "model:beforeTry", applied: true, skipped: model });
93
+ continue;
94
+ }
95
+ }
96
+ let upRes = null;
97
+ let forwarded = { ...injectReasoningContent(model, body), model };
98
+ if (plugins?.length) {
99
+ const ur = await runHook(plugins, "upstream:request", { reqId, requested, model, payload: forwarded, stream: Boolean(body.stream) });
100
+ for (const e of ur.errors) evt("plugin-hook-error", { reqId, hook: "upstream:request", plugin: e.plugin, error: e.error });
101
+ if (ur.changed && ur.value?.payload && typeof ur.value.payload === "object") {
102
+ forwarded = ur.value.payload;
103
+ evt("plugin-hook", { reqId, hook: "upstream:request", applied: true, model, rewrittenModel: forwarded.model ?? null });
104
+ }
105
+ }
106
+ const tUp = performance.now();
107
+ evt("upstream-try", { reqId, model, attempt: idx + 1 });
108
+ try {
109
+ upRes = await upstream.chat(forwarded);
110
+ 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 });
111
+ } catch (err) {
112
+ if (auto) await auto.recordError(model, { message: errMsg(err) });
113
+ lastErr = { model, upstream: null, status: 502, message: errMsg(err) };
114
+ logError(model, 502, errMsg(err));
115
+ evt("upstream-error", { reqId, model, status: 502, message: errMsg(err), timing: err._t ?? { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - tUp) } });
116
+ }
117
+ if (plugins?.length) {
118
+ runHook(plugins, "upstream:response", {
119
+ reqId, requested, model,
120
+ status: upRes instanceof Error ? null : upRes instanceof Object ? (upRes.status ?? null) : null,
121
+ ok: !(upRes instanceof Error) && upRes ? upRes.status < 400 : false,
122
+ error: upRes instanceof Error ? errMsg(upRes) : null,
123
+ timing: upRes?._t ?? null,
124
+ }).catch(() => {});
125
+ }
126
+ mark(`up-${model}`);
127
+ if (upRes && upRes.status >= 400) {
128
+ if (auto) await auto.recordError(model, { status: upRes.status });
129
+ lastErr = { model, upstream: upRes, status: upRes.status, message: null };
130
+ logError(model, upRes.status, `upstream ${upRes.status}`);
131
+ evt("upstream-error", { reqId, model, status: upRes.status, message: null, timing: upRes._t ?? null });
132
+ upRes = null;
133
+ }
134
+ if (upRes) {
135
+ const isStream = Boolean(body.stream);
136
+ const d = hedgeDelayMs();
137
+ const hasPeers = Boolean(peers) && peers.ordered().length > 0;
138
+ const doHedge = shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers }) && upRes.status === 200 && upRes.body;
139
+ if (doHedge) {
140
+ const hr = await handleHedge({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res, hedgeDelayMs: d });
141
+ if (hr.handled) return;
142
+ if (hr.lastErr) lastErr = hr.lastErr;
143
+ if (hr.upRes === null) upRes = null;
144
+ else if (hr.upRes) upRes = hr.upRes;
145
+ if (upRes === null) {
146
+ // hedge 触发后走下面的 peer/broadband 分支,不再走本地串行
147
+ } else {
148
+ // hedge 未决出胜负,回退到串行本地
149
+ }
150
+ }
151
+ if (upRes) {
152
+ const lr = await handleLocalRelay({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res });
153
+ if (lr.handled) return;
154
+ if (lr.lastErr) { lastErr = lr.lastErr; continue; }
155
+ return;
156
+ }
157
+ }
158
+
159
+ if (canForwardPeers) {
160
+ const pr = await handlePeerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, stages, startedAt, plugins, res });
161
+ if (pr.handled) return;
162
+ }
163
+
164
+ if (groups) {
165
+ const br = await handleBroadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, stages, res, startedAt, plugins });
166
+ if (br.handled) return;
167
+ }
168
+
169
+ if (canFallback) {
170
+ evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: lastErr?.message || `upstream ${lastErr?.status ?? 502}` });
171
+ continue;
172
+ }
173
+ await handleExhaustedLocal({ res, body, lastErr, order, handlerCtx: { ...handlerCtx, model, reqId }, evt, logCall, mark, perf0, stages, done, requested, useAuto });
174
+ return;
175
+ }
176
+
177
+ await handleExhaustedAll({ res, body, lastErr, order, requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
178
+ }
@@ -0,0 +1,88 @@
1
+ import { buildFallbackInfo } from "../fallback.js";
2
+ import { relay, SLOW_TOTAL_MS, STREAM_TIMEOUT_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS } from "../stream.js";
3
+ import { runHook } from "../../plugins.js";
4
+ import { performance } from "node:perf_hooks";
5
+
6
+ export async function handleLocalRelay({
7
+ upRes,
8
+ model,
9
+ body,
10
+ order,
11
+ idx,
12
+ lastErr,
13
+ requested,
14
+ useAuto,
15
+ lockModel,
16
+ auto,
17
+ handlerCtx,
18
+ evt,
19
+ logCall,
20
+ logError,
21
+ mark,
22
+ perf0,
23
+ stages,
24
+ startedAt,
25
+ plugins,
26
+ res,
27
+ }) {
28
+ logCall(model, upRes.status);
29
+ const fallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "local", useAuto, lockModel });
30
+ if (fallback?.fallback) evt("fallback-notice", { reqId: handlerCtx.reqId, requested, actual: model, reason: fallback.reason, notice: fallback.notice, via: "local" });
31
+ evt("relay-start", { reqId: handlerCtx.reqId, model, via: "local", isStream: Boolean(body.stream), fallback });
32
+ const out = await relay(res, upRes, body, {
33
+ fallback,
34
+ onFirstChunk: (delta) => {
35
+ mark(`ttf-${model}`);
36
+ evt("relay-first-chunk", { reqId: handlerCtx.reqId, model, ttfMs: delta });
37
+ if (plugins?.length) runHook(plugins, "relay:first-chunk", { reqId: handlerCtx.reqId, requested, model, via: "local", ttfMs: delta }).catch(() => {});
38
+ },
39
+ onDownstreamAbort: () => {
40
+ evt("client-abort", { reqId: handlerCtx.reqId, model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] });
41
+ },
42
+ });
43
+ evt("relay-done", { reqId: handlerCtx.reqId, model, via: "local", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
44
+ if (out.status === STREAM_TIMEOUT_MS) {
45
+ if (auto) await auto.recordError(model, { status: 502, slow: true, note: `stream timeout ${STREAM_TIMEOUT_MS}ms` });
46
+ const err = { model, upstream: null, status: 502, message: `stream timed out after ${STREAM_TIMEOUT_MS}ms` };
47
+ logError(model, 502, `stream timeout ${STREAM_TIMEOUT_MS}ms`);
48
+ evt("upstream-error", { reqId: handlerCtx.reqId, model, status: 502, message: "stream timeout", timing: null });
49
+ evt("fallback", { reqId: handlerCtx.reqId, from: model, to: order[idx + 1] ?? null, reason: "stream timeout" });
50
+ return { handled: false, upRes: null, lastErr: err };
51
+ }
52
+ if (out.interrupted) {
53
+ if (auto) {
54
+ await auto.recordError(model, { status: 200, slow: true, note: `stall ${STALL_TIMEOUT_MS}ms` });
55
+ await auto.recordLatency(model, out.totalMs ?? (Date.now() - startedAt));
56
+ }
57
+ evt("slow-model", { model, elapsedMs: out.totalMs ?? (Date.now() - startedAt), threshold: STALL_TIMEOUT_MS, interrupted: true, detail: out.detail ?? null });
58
+ logCall(model, 200);
59
+ 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 });
60
+ evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, interrupted: true, reqId: handlerCtx.reqId });
61
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId: handlerCtx.reqId, requested, useAuto, hops: handlerCtx.hops, stream: Boolean(body.stream), durationMs: Date.now() - startedAt, via: "local", status: out.status, actual: model, interrupted: true, fallback }).catch(() => {});
62
+ return { handled: true };
63
+ }
64
+ const elapsed = Date.now() - startedAt;
65
+ const latencyMs = out.totalMs ?? elapsed;
66
+ let scoredSlow = false;
67
+ if (SLOW_TOTAL_MS && auto && elapsed > SLOW_TOTAL_MS && out.status === 200) {
68
+ void auto.recordError(model, { status: 200, slow: true, note: `slow ${elapsed}ms` });
69
+ void auto.recordLatency(model, latencyMs);
70
+ evt("slow-model", { model, elapsedMs: elapsed, threshold: SLOW_TOTAL_MS, reason: "total", detail: out.detail ?? null });
71
+ scoredSlow = true;
72
+ }
73
+ if (out.detail?.stallHits > 0 && auto && out.status === 200) {
74
+ void auto.recordError(model, { status: 200, slow: true, note: `stall ${out.detail.stallHits}x gap>${SCORE_STALL_MS}ms maxGap ${out.detail.maxGapMs}ms` });
75
+ void auto.recordLatency(model, latencyMs);
76
+ evt("slow-model", { model, elapsedMs: elapsed, threshold: SCORE_STALL_MS, reason: "stall", stallHits: out.detail.stallHits, maxGapMs: out.detail.maxGapMs, detail: out.detail ?? null });
77
+ scoredSlow = true;
78
+ }
79
+ if (!scoredSlow && auto && out.status === 200) {
80
+ await auto.recordOk(model, { latencyMs });
81
+ } else if (!scoredSlow && auto) {
82
+ await auto.recordLatency(model, latencyMs);
83
+ }
84
+ 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 });
85
+ evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, reqId: handlerCtx.reqId });
86
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId: handlerCtx.reqId, requested, useAuto, hops: handlerCtx.hops, stream: Boolean(body.stream), durationMs: Date.now() - startedAt, via: "local", status: out.status, actual: model, fallback }).catch(() => {});
87
+ return { handled: true };
88
+ }
@@ -0,0 +1,58 @@
1
+ import { buildFallbackInfo } from "../fallback.js";
2
+ import { relay, SLOW_TOTAL_MS } from "../stream.js";
3
+ import { racePeerCandidates } from "../peers.js";
4
+ import { runHook } from "../../plugins.js";
5
+
6
+ export async function handlePeerRelay({
7
+ model,
8
+ body,
9
+ lastErr,
10
+ requested,
11
+ useAuto,
12
+ lockModel,
13
+ auto,
14
+ peers,
15
+ handlerCtx,
16
+ evt,
17
+ logCall,
18
+ mark,
19
+ perf0,
20
+ stages,
21
+ startedAt,
22
+ plugins,
23
+ res,
24
+ }) {
25
+ evt("peer-race-start", { reqId: handlerCtx.reqId, model, peers: peers.ordered().length });
26
+ const win =
27
+ (await racePeerCandidates(peers.ordered(), handlerCtx)) ||
28
+ (await racePeerCandidates(peers.orderedByLastError(), handlerCtx));
29
+ if (!win) {
30
+ evt("peer-race-lose", { reqId: handlerCtx.reqId, model });
31
+ return { handled: false };
32
+ }
33
+ evt("peer-race-win", { reqId: handlerCtx.reqId, model, winPeer: win.peer.url, winTarget: win.target, latencyMs: win.latencyMs });
34
+ await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
35
+ logCall(win.target, win.res.status);
36
+ const peerFallback = buildFallbackInfo({ requested, actual: win.target, lastErr, via: "peer", useAuto, lockModel });
37
+ if (peerFallback?.fallback) evt("fallback-notice", { reqId: handlerCtx.reqId, requested, actual: win.target, reason: peerFallback.reason, notice: peerFallback.notice, via: "peer" });
38
+ evt("relay-start", { reqId: handlerCtx.reqId, model: win.target, via: "peer", isStream: Boolean(body.stream), fallback: peerFallback });
39
+ const out = await relay(res, win.res, body, {
40
+ fallback: peerFallback,
41
+ onFirstChunk: (d) => mark(`ttf-peer-${win.target}`),
42
+ onDownstreamAbort: () => evt("client-abort", { reqId: handlerCtx.reqId, model: win.target, totalMs: Math.round(Date.now() - startedAt), stages: [...stages] }),
43
+ });
44
+ evt("relay-done", { reqId: handlerCtx.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 });
45
+ if (auto && out.status === 200) {
46
+ const latencyMs = out.totalMs ?? win.latencyMs;
47
+ if (out.detail?.stallHits > 0 || (latencyMs && latencyMs > SLOW_TOTAL_MS)) {
48
+ void auto.recordError(win.target, { status: 200, slow: true, note: `peer slow ${latencyMs}ms` });
49
+ void auto.recordLatency(win.target, latencyMs);
50
+ } else {
51
+ await auto.recordOk(win.target, { latencyMs });
52
+ }
53
+ }
54
+ 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 });
55
+ evt("client-response", { requested, actual: win.target, via: "peer", fallback: peerFallback, status: out.status, reqId: handlerCtx.reqId });
56
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId: handlerCtx.reqId, requested, useAuto, hops: handlerCtx.hops, stream: Boolean(body.stream), durationMs: Date.now() - startedAt, via: "peer", status: out.status, actual: win.target, fallback: peerFallback }).catch(() => {});
57
+ return { handled: true };
58
+ }