mslxdff 0.1.39 → 0.1.41

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 CHANGED
@@ -1179,7 +1179,7 @@ function fmtEvent(e) {
1179
1179
  const t = e?.ts ? new Date(e.ts).toISOString().slice(11, 19) : "--:--:--";
1180
1180
  const head = `[${t}]`;
1181
1181
  const m = (x) => x || "-";
1182
- const fallbackTag = e.fallback ? ` fallback=${e.fallback.requested_model || e.requested}->${e.fallback.actual_model || e.actual}(${e.fallback.reason || e.reason})` : "";
1182
+ const fallbackTag = e.fallback?.fallback ? ` fallback=${e.fallback.requested_model || e.requested}->${e.fallback.actual_model || e.actual}(${e.fallback.reason || e.reason})` : "";
1183
1183
  switch (e?.type) {
1184
1184
  case "request":
1185
1185
  return `${head} request client-> ${m(e.requested || e.model)}${e.auto ? " (auto)" : ""} raw=${m(e.rawModel)} lock=${m(e.lockModel)} hops=${e.hops} from ${e.ip || "?"}${e.stream ? " stream" : ""}${e.prompt ? ` content="${e.prompt}"` : ""} reqId=${e.reqId || ""}`;
@@ -1228,7 +1228,7 @@ function fmtEvent(e) {
1228
1228
  case "result":
1229
1229
  return `${head} result 返回客户端 status=${e.status} model=${m(e.model)} via=${e.via} 响应耗时 ${fmtDur(e.durationMs)}${fallbackTag}`;
1230
1230
  case "client-response":
1231
- return `${head} client res 返回客户端 客户端请求 ${m(e.requested)} 实际返回 ${m(e.actual)} via=${m(e.via)}${e.fallback ? ` fallback=${e.fallback.requested_model}->${e.fallback.actual_model}(${e.fallback.reason})` : " 无fallback"} status=${e.status}`;
1231
+ return `${head} client res 返回客户端 客户端请求 ${m(e.requested)} 实际返回 ${m(e.actual)} via=${m(e.via)}${e.fallback?.fallback ? ` fallback=${e.fallback.requested_model}->${e.fallback.actual_model}(${e.fallback.reason})` : " 无fallback"} status=${e.status}`;
1232
1232
  case "auto-update-enabled":
1233
1233
  return `${head} auto-update enabled every ${Math.round((e.intervalMs||0)/60000)}m current=${e.current}`;
1234
1234
  case "auto-update-disabled":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,278 @@
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
+
10
+ export async function chatHandler({ req, res, upstream, auto, logs, peers, maxHops, groups, bus, token }) {
11
+ let body;
12
+ try {
13
+ body = await readBody(req);
14
+ } catch {
15
+ return json(res, 400, { error: "Invalid JSON body" });
16
+ }
17
+
18
+ const startedAt = Date.now();
19
+ const reqId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
20
+ const perf0 = performance.now();
21
+ const stages = [];
22
+ const mark = (name) => stages.push([name, Math.round(performance.now() - perf0)]);
23
+ const hops = parseHops(req.headers["x-mslxdff-hops"]);
24
+ const lockModel = req.headers["x-mslxdff-model-lock"] || "";
25
+ const rawModel = body.model || "";
26
+ const requested = normalizeModel(lockModel || rawModel || "");
27
+ const useAuto = isAutoModel(requested);
28
+ mark("parsed");
29
+
30
+ let order;
31
+ if (lockModel) {
32
+ order = [requested];
33
+ } else if (useAuto) {
34
+ order = auto ? await auto.candidates() : [""];
35
+ } else {
36
+ order = auto ? await auto.candidatesFor(requested) : [requested];
37
+ }
38
+ if (!order.length) order = [""];
39
+ const canFallback = order.length > 1;
40
+ const canForwardPeers = Boolean(peers) && hops < maxHops;
41
+ mark("ordered");
42
+
43
+ const logCall = (model, status) =>
44
+ logs?.appendCall({ reqId, model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(body.stream), stages });
45
+ const logError = (model, status, message) =>
46
+ logs?.appendError({ reqId, model, auto: useAuto, status, message, stages });
47
+ const evt = (type, data) => {
48
+ const entry = { ts: Date.now(), reqId, type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt, stages: [...stages] };
49
+ if (bus) bus.emit(entry);
50
+ logs?.appendEvent?.(entry);
51
+ };
52
+ evt("request", { reqId, hops, ip: clientIp(req), stream: Boolean(body.stream), prompt: summarizePrompt(body), rawModel, requested, lockModel: lockModel || null });
53
+ evt("ordered", { reqId, order, canFallback, canForwardPeers, useAuto, statuses: auto?.statuses?.() ?? null });
54
+
55
+ const handlerCtx = {
56
+ model: null,
57
+ body,
58
+ hops,
59
+ peers,
60
+ evt,
61
+ logError,
62
+ logCall,
63
+ };
64
+
65
+ let lastErr = null;
66
+ for (let idx = 0; idx < order.length; idx++) {
67
+ const model = order[idx];
68
+ handlerCtx.model = model;
69
+ evt("model-try", { reqId, model, idx, remaining: order.length - idx });
70
+ let upRes = null;
71
+ const forwarded = { ...injectReasoningContent(model, body), model };
72
+ const tUp = performance.now();
73
+ evt("upstream-try", { reqId, model, attempt: idx + 1 });
74
+ try {
75
+ upRes = await upstream.chat(forwarded);
76
+ 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 });
77
+ } catch (err) {
78
+ if (auto) await auto.recordError(model, { message: errMsg(err) });
79
+ lastErr = { model, upstream: null, status: 502, message: errMsg(err) };
80
+ logError(model, 502, errMsg(err));
81
+ evt("upstream-error", { reqId, model, status: 502, message: errMsg(err), timing: err._t ?? { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - tUp) } });
82
+ }
83
+ mark(`up-${model}`);
84
+ if (upRes && upRes.status >= 400) {
85
+ if (auto) await auto.recordError(model, { status: upRes.status });
86
+ lastErr = { model, upstream: upRes, status: upRes.status, message: null };
87
+ logError(model, upRes.status, `upstream ${upRes.status}`);
88
+ evt("upstream-error", { reqId, model, status: upRes.status, message: null, timing: upRes._t ?? null });
89
+ upRes = null;
90
+ }
91
+ if (upRes) {
92
+ logCall(model, upRes.status);
93
+ const fallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "local", useAuto, lockModel });
94
+ if (fallback?.fallback) evt("fallback-notice", { reqId, requested, actual: model, reason: fallback.reason, notice: fallback.notice, via: "local" });
95
+ evt("relay-start", { reqId, model, via: "local", isStream: Boolean(body.stream), fallback });
96
+ const out = await relay(res, upRes, body, {
97
+ fallback,
98
+ onFirstChunk: (delta) => {
99
+ mark(`ttf-${model}`);
100
+ evt("relay-first-chunk", { reqId, model, ttfMs: delta });
101
+ },
102
+ onDownstreamAbort: () => {
103
+ evt("client-abort", { reqId, model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] });
104
+ },
105
+ });
106
+ 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 });
107
+ if (out.status === STREAM_TIMEOUT_MS) {
108
+ if (auto) await auto.recordError(model, { status: 502, slow: true, note: `stream timeout ${STREAM_TIMEOUT_MS}ms` });
109
+ lastErr = { model, upstream: null, status: 502, message: `stream timed out after ${STREAM_TIMEOUT_MS}ms` };
110
+ logError(model, 502, `stream timeout ${STREAM_TIMEOUT_MS}ms`);
111
+ evt("upstream-error", { reqId, model, status: 502, message: "stream timeout", timing: null });
112
+ evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: "stream timeout" });
113
+ upRes = null;
114
+ continue;
115
+ }
116
+ if (out.interrupted) {
117
+ if (auto) {
118
+ await auto.recordError(model, { status: 200, slow: true, note: `stall ${STALL_TIMEOUT_MS}ms` });
119
+ await auto.recordLatency(model, out.totalMs ?? (Date.now() - startedAt));
120
+ }
121
+ evt("slow-model", { model, elapsedMs: out.totalMs ?? (Date.now() - startedAt), threshold: STALL_TIMEOUT_MS, interrupted: true, detail: out.detail ?? null });
122
+ logCall(model, 200);
123
+ 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 });
124
+ evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, interrupted: true, reqId });
125
+ return;
126
+ }
127
+ const elapsed = Date.now() - startedAt;
128
+ const latencyMs = out.totalMs ?? elapsed;
129
+ let scoredSlow = false;
130
+ if (SLOW_TOTAL_MS && auto && elapsed > SLOW_TOTAL_MS && out.status === 200) {
131
+ void auto.recordError(model, { status: 200, slow: true, note: `slow ${elapsed}ms` });
132
+ void auto.recordLatency(model, latencyMs);
133
+ evt("slow-model", { model, elapsedMs: elapsed, threshold: SLOW_TOTAL_MS, reason: "total", detail: out.detail ?? null });
134
+ scoredSlow = true;
135
+ }
136
+ if (out.detail?.stallHits > 0 && auto && out.status === 200) {
137
+ void auto.recordError(model, { status: 200, slow: true, note: `stall ${out.detail.stallHits}x gap>${SCORE_STALL_MS}ms maxGap ${out.detail.maxGapMs}ms` });
138
+ void auto.recordLatency(model, latencyMs);
139
+ evt("slow-model", { model, elapsedMs: elapsed, threshold: SCORE_STALL_MS, reason: "stall", stallHits: out.detail.stallHits, maxGapMs: out.detail.maxGapMs, detail: out.detail ?? null });
140
+ scoredSlow = true;
141
+ }
142
+ if (!scoredSlow && auto && out.status === 200) {
143
+ await auto.recordOk(model, { latencyMs });
144
+ } else if (!scoredSlow && auto) {
145
+ await auto.recordLatency(model, latencyMs);
146
+ }
147
+ 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 });
148
+ evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, reqId });
149
+ return;
150
+ }
151
+
152
+ if (canForwardPeers) {
153
+ evt("peer-race-start", { reqId, model, peers: peers.ordered().length });
154
+ const win =
155
+ (await racePeerCandidates(peers.ordered(), handlerCtx)) ||
156
+ (await racePeerCandidates(peers.orderedByLastError(), handlerCtx));
157
+ if (win) {
158
+ evt("peer-race-win", { reqId, model, winPeer: win.peer.url, winTarget: win.target, latencyMs: win.latencyMs });
159
+ await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
160
+ logCall(win.target, win.res.status);
161
+ const peerFallback = buildFallbackInfo({ requested, actual: win.target, lastErr, via: "peer", useAuto, lockModel });
162
+ if (peerFallback?.fallback) evt("fallback-notice", { reqId, requested, actual: win.target, reason: peerFallback.reason, notice: peerFallback.notice, via: "peer" });
163
+ evt("relay-start", { reqId, model: win.target, via: "peer", isStream: Boolean(body.stream), fallback: peerFallback });
164
+ const out = await relay(res, win.res, body, {
165
+ fallback: peerFallback,
166
+ onFirstChunk: (d) => mark(`ttf-peer-${win.target}`),
167
+ onDownstreamAbort: () => evt("client-abort", { reqId, model: win.target, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
168
+ });
169
+ 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 });
170
+ if (auto && out.status === 200) {
171
+ const latencyMs = out.totalMs ?? win.latencyMs;
172
+ if (out.detail?.stallHits > 0 || (latencyMs && latencyMs > SLOW_TOTAL_MS)) {
173
+ void auto.recordError(win.target, { status: 200, slow: true, note: `peer slow ${latencyMs}ms` });
174
+ void auto.recordLatency(win.target, latencyMs);
175
+ } else {
176
+ await auto.recordOk(win.target, { latencyMs });
177
+ }
178
+ }
179
+ 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 });
180
+ evt("client-response", { requested, actual: win.target, via: "peer", fallback: peerFallback, status: out.status, reqId });
181
+ return;
182
+ }
183
+ evt("peer-race-lose", { reqId, model });
184
+ }
185
+
186
+ if (groups) {
187
+ const bb = await tryBroadbandRelay({ groups, token, model, body, hops, bus, logs, reqId, evt, res, mark, perf0, stages });
188
+ if (bb) {
189
+ const isResponse = bb.result && typeof bb.result.status === "number" && typeof bb.result.headers?.get === "function";
190
+ if (isResponse) {
191
+ const bbFallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "broadband", useAuto, lockModel });
192
+ if (bbFallback?.fallback) evt("fallback-notice", { reqId, requested, actual: model, reason: bbFallback.reason, notice: bbFallback.notice, via: "broadband" });
193
+ evt("relay-start", { reqId, model, via: "broadband", target: bb.target, group: bb.group, fallback: bbFallback });
194
+ const out = await relay(res, bb.result, body, {
195
+ fallback: bbFallback,
196
+ onFirstChunk: (d) => mark(`ttf-bb-${model}`),
197
+ onDownstreamAbort: () => evt("client-abort", { reqId, model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
198
+ });
199
+ 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 });
200
+ if (auto && out.status === 200) {
201
+ const latencyMs = out.totalMs ?? 0;
202
+ if (out.detail?.stallHits > 0 || (latencyMs && latencyMs > SLOW_TOTAL_MS)) {
203
+ void auto.recordError(model, { status: 200, slow: true, note: `broadband slow ${latencyMs}ms` });
204
+ void auto.recordLatency(model, latencyMs);
205
+ } else {
206
+ await auto.recordOk(model, { latencyMs });
207
+ }
208
+ }
209
+ 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 });
210
+ evt("client-response", { requested, actual: model, via: "broadband", fallback: bbFallback, status: out.status, reqId });
211
+ return;
212
+ } else if (bb.result && typeof bb.result.status === "number") {
213
+ const fakeRes = {
214
+ status: bb.result.status,
215
+ headers: { get: (k) => bb.result.headers?.[k] || bb.result.headers?.[k.toLowerCase()] || null },
216
+ text: async () => typeof bb.result.body === "string" ? bb.result.body : JSON.stringify(bb.result.body),
217
+ body: (() => {
218
+ const b = bb.result.body || "";
219
+ const str = typeof b === "string" ? b : JSON.stringify(b);
220
+ const isSSE = bb.result.headers?.["Content-Type"]?.includes("text/event-stream");
221
+ if (isSSE) {
222
+ return (async function* () { yield Buffer.from(str); })();
223
+ }
224
+ return null;
225
+ })(),
226
+ };
227
+ const bbLocalFallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "broadband", useAuto, lockModel });
228
+ if (bbLocalFallback?.fallback) evt("fallback-notice", { reqId, requested, actual: model, reason: bbLocalFallback.reason, notice: bbLocalFallback.notice, via: "broadband" });
229
+ evt("relay-start", { reqId, model, via: "broadband-local", target: bb.target, group: bb.group, fallback: bbLocalFallback });
230
+ const out = await relay(res, fakeRes, body, {
231
+ fallback: bbLocalFallback,
232
+ onFirstChunk: (d) => mark(`ttf-bb-${model}`),
233
+ onDownstreamAbort: () => evt("client-abort", { reqId, model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
234
+ });
235
+ 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 });
236
+ 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 });
237
+ evt("client-response", { requested, actual: model, via: "broadband", fallback: bbLocalFallback, status: out.status, reqId });
238
+ return;
239
+ }
240
+ }
241
+ evt("relay-miss", { reqId, model });
242
+ }
243
+
244
+ if (canFallback) {
245
+ evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: lastErr?.message || `upstream ${lastErr?.status ?? 502}` });
246
+ continue;
247
+ }
248
+ evt("exhausted-local", { reqId, lastModel: lastErr?.model ?? model, lastStatus: lastErr?.status ?? 502, order });
249
+ logCall(lastErr?.model ?? model, lastErr?.status ?? 502);
250
+ if (lastErr?.upstream) {
251
+ evt("relay-start", { reqId, model: lastErr.model, via: "local-exhausted", isStream: Boolean(body.stream) });
252
+ const out = await relay(res, lastErr.upstream, body, {
253
+ onFirstChunk: (d) => mark(`ttf-${lastErr.model}`),
254
+ onDownstreamAbort: () => evt("client-abort", { reqId, model: lastErr.model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
255
+ });
256
+ 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 });
257
+ 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 });
258
+ return;
259
+ }
260
+ evt("result", { reqId, model, status: lastErr?.status ?? 502, via: "none", timing: null });
261
+ return json(res, 502, { error: lastErr?.message || "all auto models failed" });
262
+ }
263
+
264
+ evt("exhausted-all", { reqId, lastModel: lastErr?.model ?? requested, lastStatus: lastErr?.status ?? 502, order });
265
+ logCall(lastErr?.model ?? requested, lastErr?.status ?? 502);
266
+ if (lastErr?.upstream) {
267
+ evt("relay-start", { reqId, model: lastErr.model, via: "local-final", isStream: Boolean(body.stream) });
268
+ const out = await relay(res, lastErr.upstream, body, {
269
+ onFirstChunk: (d) => mark(`ttf-${lastErr.model}`),
270
+ onDownstreamAbort: () => evt("client-abort", { reqId, model: lastErr.model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
271
+ });
272
+ 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 });
273
+ 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 });
274
+ return;
275
+ }
276
+ evt("result", { reqId, model: lastErr?.model ?? requested, status: lastErr?.status ?? 502, via: "none", timing: null });
277
+ return json(res, 502, { error: lastErr?.message || "all auto models failed" });
278
+ }
@@ -0,0 +1,88 @@
1
+ function fallbackReason(lastErr) {
2
+ if (!lastErr) return "cooldown";
3
+ const s = Number(lastErr.status);
4
+ if (s === 429) return "rate_limited";
5
+ if (lastErr.message && /timeout/i.test(String(lastErr.message))) return "timeout";
6
+ if (s === 502 || s === 503 || s === 504) return "upstream_error";
7
+ if (s >= 400) return "upstream_error";
8
+ return "fallback";
9
+ }
10
+
11
+ export function buildFallbackInfo({ requested, actual, lastErr, via, useAuto, lockModel }) {
12
+ if (!requested || !actual) return null;
13
+ const alwaysHeaders = {
14
+ requested_model: requested,
15
+ actual_model: actual,
16
+ via: via || "local",
17
+ };
18
+ if (useAuto || lockModel) {
19
+ return { ...alwaysHeaders, fallback: false, reason: null, notice: null };
20
+ }
21
+ const isFallback = requested !== actual;
22
+ if (!isFallback) {
23
+ return { ...alwaysHeaders, fallback: false, reason: null, notice: null };
24
+ }
25
+ const reason = fallbackReason(lastErr);
26
+ const reasonZh = reason === "rate_limited" ? "限流" : reason === "timeout" ? "超时" : reason === "cooldown" ? "冷却中" : "不可用";
27
+ const notice = `${requested} ${reasonZh},已由 ${actual} 代答`;
28
+ return { ...alwaysHeaders, fallback: true, reason, notice };
29
+ }
30
+
31
+ export function applyFallbackHeaders(res, info) {
32
+ if (!info) return;
33
+ if (info.requested_model) res.setHeader("x-mslxdff-requested-model", info.requested_model);
34
+ if (info.actual_model) res.setHeader("x-mslxdff-actual-model", info.actual_model);
35
+ if (info.via) res.setHeader("x-mslxdff-via", info.via);
36
+ if (info.fallback) {
37
+ res.setHeader("x-mslxdff-fallback", "1");
38
+ if (info.reason) res.setHeader("x-mslxdff-fallback-reason", info.reason);
39
+ if (info.notice) res.setHeader("x-mslxdff-notice", encodeURIComponent(info.notice));
40
+ }
41
+ }
42
+
43
+ export function enrichNonStreamJson(obj, info) {
44
+ if (!info || typeof obj !== "object" || obj === null) return obj;
45
+ if (!info.fallback) return obj;
46
+ if (obj.mslxdff) return obj;
47
+ return {
48
+ ...obj,
49
+ mslxdff: {
50
+ fallback: true,
51
+ requested_model: info.requested_model,
52
+ actual_model: info.actual_model,
53
+ reason: info.reason,
54
+ via: info.via,
55
+ notice: info.notice,
56
+ },
57
+ };
58
+ }
59
+
60
+ export function enrichSseChunkText(text, info) {
61
+ if (!info?.fallback) return text;
62
+ const lines = text.split("\n");
63
+ let changed = false;
64
+ for (let i = 0; i < lines.length; i++) {
65
+ const line = lines[i];
66
+ const m = /^data:\s*(\{.*\})\s*$/.exec(line);
67
+ if (!m) continue;
68
+ try {
69
+ const obj = JSON.parse(m[1]);
70
+ if (obj && typeof obj === "object" && !obj.mslxdff) {
71
+ obj.mslxdff = {
72
+ fallback: true,
73
+ requested_model: info.requested_model,
74
+ actual_model: info.actual_model,
75
+ reason: info.reason,
76
+ via: info.via,
77
+ notice: info.notice,
78
+ };
79
+ lines[i] = `data: ${JSON.stringify(obj)}`;
80
+ changed = true;
81
+ break;
82
+ }
83
+ } catch {
84
+ continue;
85
+ }
86
+ }
87
+ return changed ? lines.join("\n") : text;
88
+ }
@@ -0,0 +1,129 @@
1
+ import { clientIp, json, readBody, parseHops, errMsg } from "./helpers.js";
2
+ import { DEFAULT_MAX_HOPS } from "../peers.js";
3
+ import { enqueueRelay, dequeueRelayForPoll, resolveRelay } from "./relay-queue.js";
4
+
5
+ export async function heartbeatHandler({ req, res, groups, bus, logs }) {
6
+ const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
7
+ if (!auth) return json(res, 401, { error: "bearer token required" });
8
+ let body;
9
+ try { body = await readBody(req); } catch { return json(res, 400, { error: "Invalid JSON body" }); }
10
+ const groupName = body?.name || body?.group;
11
+ if (!groupName) return json(res, 400, { error: "group name is required" });
12
+ const hit = groups?.membersForToken(groupName, auth[1]);
13
+ if (!hit) return json(res, 403, { error: "invalid member token" });
14
+ const ip = clientIp(req);
15
+ try {
16
+ const memberUrl = hit.member?.url;
17
+ const members = groups.list()[groupName]?.members || {};
18
+ const targetId = Object.keys(members).find((k) => members[k].url === memberUrl) || hit.member?.url;
19
+ if (hit.member?.kind === "broadband" || String(memberUrl).startsWith("relay://")) {
20
+ const m = members[targetId] || hit.member;
21
+ if (m) {
22
+ m.publicIp = ip;
23
+ m.lastSeen = Date.now();
24
+ try { groups.upsertMember(groupName, { memberName: targetId, url: m.url, token: m.token, kind: "broadband", publicIp: ip, lastSeen: m.lastSeen }); } catch {}
25
+ }
26
+ const evtData = { ts: Date.now(), type: "relay-heartbeat", member: targetId, ip, lastSeen: m?.lastSeen, group: groupName };
27
+ if (bus) bus.emit(evtData);
28
+ logs?.appendEvent?.(evtData);
29
+ }
30
+ return json(res, 200, { object: "heartbeat", ok: true, ip, lastSeen: Date.now() });
31
+ } catch (err) {
32
+ return json(res, 400, { error: errMsg(err) });
33
+ }
34
+ }
35
+
36
+ export async function pollHandler({ req, res, groups }) {
37
+ const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
38
+ if (!auth) return json(res, 401, { error: "bearer token required" });
39
+ let body;
40
+ try { body = await readBody(req); } catch { return json(res, 400, { error: "Invalid JSON body" }); }
41
+ const groupName = body?.name || body?.group;
42
+ if (!groupName) return json(res, 400, { error: "group name is required" });
43
+ const hit = groups?.membersForToken(groupName, auth[1]);
44
+ if (!hit) return json(res, 403, { error: "invalid member token" });
45
+ const targetUrl = hit.member?.url;
46
+ if (!targetUrl) return json(res, 400, { error: "member url not found" });
47
+ const batch = dequeueRelayForPoll({ group: groupName, target: targetUrl, limit: 10 });
48
+ return json(res, 200, { object: "poll", data: batch });
49
+ }
50
+
51
+ export async function resultHandler({ req, res, groups }) {
52
+ const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
53
+ if (!auth) return json(res, 401, { error: "bearer token required" });
54
+ let body;
55
+ try { body = await readBody(req); } catch { return json(res, 400, { error: "Invalid JSON body" }); }
56
+ const groupName = body?.name || body?.group;
57
+ const reqId = body?.reqId;
58
+ if (!groupName || !reqId) return json(res, 400, { error: "group and reqId required" });
59
+ const hit = groups?.membersForToken(groupName, auth[1]);
60
+ if (!hit) return json(res, 403, { error: "invalid member token" });
61
+ const ok = resolveRelay(reqId, body.result || body);
62
+ if (!ok) return json(res, 404, { error: "pending request not found or timed out" });
63
+ return json(res, 200, { object: "result", ok: true });
64
+ }
65
+
66
+ export async function forwardHandler({ req, res, groups, bus, logs }) {
67
+ let body;
68
+ try { body = await readBody(req); } catch { return json(res, 400, { error: "Invalid JSON body" }); }
69
+ const groupName = body?.group || body?.name;
70
+ const target = body?.target || body?.url;
71
+ const hops = parseHops(req.headers["x-mslxdff-hops"] || body?.hops);
72
+ if (!groupName || !target) return json(res, 400, { error: "group and target required" });
73
+ if (hops >= DEFAULT_MAX_HOPS) return json(res, 429, { error: "max hops exceeded" });
74
+ const members = groups?.list()[groupName]?.members || {};
75
+ const targetMember = Object.values(members).find((m) => m.url === target) || Object.entries(members).find(([id]) => id === target)?.[1];
76
+ if (!targetMember) return json(res, 404, { error: `target ${target} not found in group ${groupName}` });
77
+ const isBb = targetMember.kind === "broadband" || String(targetMember.url).startsWith("relay://");
78
+ if (!isBb) {
79
+ try {
80
+ const fwdBody = body.body || body;
81
+ const r = await fetch(`${targetMember.url}/v1/chat/completions`, {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${targetMember.token || ""}`, "x-mslxdff-hops": String(hops + 1), "x-mslxdff-model-lock": fwdBody.model || "", "Accept": "text/event-stream" },
84
+ body: JSON.stringify(fwdBody),
85
+ });
86
+ const evtData = { ts: Date.now(), type: "relay-forward", target, via: "direct", group: groupName, hops };
87
+ if (bus) bus.emit(evtData);
88
+ logs?.appendEvent?.(evtData);
89
+ res.statusCode = r.status;
90
+ if (r.headers.get("content-type")?.includes("text/event-stream")) {
91
+ res.setHeader("Content-Type", "text/event-stream");
92
+ if (r.body) for await (const c of r.body) res.write(c);
93
+ res.end();
94
+ } else {
95
+ const txt = await r.text();
96
+ res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
97
+ res.end(txt);
98
+ }
99
+ return;
100
+ } catch (err) {
101
+ return json(res, 502, { error: errMsg(err) });
102
+ }
103
+ }
104
+ const reqId = body.reqId || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
105
+ const fwdBody = body.body || { model: body.model, messages: body.messages, stream: body.stream };
106
+ const evtData = { ts: Date.now(), type: "relay-forward", target, via: "leader", group: groupName, hops, reqId, model: fwdBody.model };
107
+ if (bus) bus.emit(evtData);
108
+ logs?.appendEvent?.(evtData);
109
+ const staleMs = Number(process.env.MSLXDFF_BROADBAND_STALE_MS) > 0 ? Number(process.env.MSLXDFF_BROADBAND_STALE_MS) : 90_000;
110
+ if (typeof targetMember.lastSeen === "number" && Date.now() - targetMember.lastSeen > staleMs) {
111
+ return json(res, 502, { error: "broadband member stale (no heartbeat)" });
112
+ }
113
+ try {
114
+ const resultPromise = enqueueRelay({ group: groupName, target: targetMember.url, reqId, body: fwdBody, hops });
115
+ const result = await resultPromise;
116
+ if (result && typeof result.status === "number") {
117
+ res.statusCode = result.status;
118
+ if (result.headers) for (const [k, v] of Object.entries(result.headers)) res.setHeader(k, v);
119
+ if (result.body) {
120
+ if (typeof result.body === "string") res.end(result.body);
121
+ else res.end(JSON.stringify(result.body));
122
+ } else res.end();
123
+ return;
124
+ }
125
+ return json(res, 200, result);
126
+ } catch (err) {
127
+ return json(res, 504, { error: errMsg(err) || "relay timeout" });
128
+ }
129
+ }
@@ -0,0 +1,121 @@
1
+ import { clientIp, json, readBody, errMsg } from "./helpers.js";
2
+
3
+ export async function joinHandler({ req, res, groups, token, bans }) {
4
+ if (!groups) return json(res, 501, { error: "Groups service not configured" });
5
+ const ip = clientIp(req);
6
+ const banned = bans?.isBanned(ip);
7
+ if (banned) {
8
+ return json(res, 403, { error: `banned until ${new Date(banned.until).toISOString()}` });
9
+ }
10
+ let body;
11
+ try {
12
+ body = await readBody(req);
13
+ } catch {
14
+ return json(res, 400, { error: "Invalid JSON body" });
15
+ }
16
+ if (!body?.name) return json(res, 400, { error: "group name is required" });
17
+
18
+ const fail = (msg) => {
19
+ try {
20
+ if (bans) {
21
+ const b = bans.recordFailure(ip);
22
+ if (b) console.error(`${ip} banned (${bans.threshold} failed joins)`);
23
+ }
24
+ } catch {}
25
+ return json(res, 403, { error: msg });
26
+ };
27
+
28
+ if (!body.key) {
29
+ const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
30
+ const hit = auth && groups.membersForToken(body.name, auth[1]);
31
+ if (!hit) return fail("invalid member token");
32
+ try {
33
+ const isBroadbandRe = String(body.url || hit.member?.url || "").startsWith("relay://") || body.kind === "broadband" || hit.member?.kind === "broadband";
34
+ const extra = {};
35
+ if (isBroadbandRe) {
36
+ extra.kind = "broadband";
37
+ extra.publicIp = ip;
38
+ extra.lastSeen = Date.now();
39
+ if (body.url) extra.url = String(body.url);
40
+ }
41
+ const refreshed = groups.upsertMember(body.name, {
42
+ memberName: body.memberName,
43
+ url: body.url || hit.member.url,
44
+ token: body.token || hit.member.token,
45
+ kind: extra.kind,
46
+ publicIp: extra.publicIp,
47
+ lastSeen: extra.lastSeen,
48
+ });
49
+ if (isBroadbandRe && refreshed) {
50
+ const targetId = Object.keys(refreshed).find((k) => refreshed[k].url === (body.url || hit.member.url));
51
+ if (targetId) {
52
+ refreshed[targetId].publicIp = ip;
53
+ refreshed[targetId].lastSeen = Date.now();
54
+ refreshed[targetId].kind = "broadband";
55
+ }
56
+ }
57
+ return json(res, 200, { object: "group", name: body.name, members: refreshed });
58
+ } catch (err) {
59
+ return json(res, 400, { error: errMsg(err) });
60
+ }
61
+ }
62
+
63
+ try {
64
+ const youPort = Number(body.myPort);
65
+ const youUrl = Number.isInteger(youPort) && youPort > 0 ? `http://${ip}:${youPort}` : "";
66
+ let memberUrl = String(body.url || youUrl);
67
+ if (!memberUrl) throw new Error("member url is required");
68
+ const isBroadband = String(memberUrl).startsWith("relay://") || body.kind === "broadband";
69
+ if (isBroadband) {
70
+ memberUrl = String(body.url || memberUrl);
71
+ }
72
+ const members = groups.addMember(body.name, {
73
+ key: body.key,
74
+ memberName: body.memberName,
75
+ url: memberUrl,
76
+ token: body.token,
77
+ kind: isBroadband ? "broadband" : "static",
78
+ publicIp: isBroadband ? ip : undefined,
79
+ lastSeen: isBroadband ? Date.now() : undefined,
80
+ });
81
+ if (bans) bans.clear(ip);
82
+ if (!members.leader) {
83
+ const leaderUrl = String(body.leaderUrl || "").replace(/\/+$/, "");
84
+ if (leaderUrl) {
85
+ groups.upsertMember(body.name, { memberName: "leader", url: leaderUrl, token });
86
+ Object.assign(members, { leader: { url: leaderUrl, token } });
87
+ }
88
+ }
89
+ json(res, 200, { object: "group", name: body.name, members, you: { url: memberUrl } });
90
+ } catch (err) {
91
+ return fail(errMsg(err));
92
+ }
93
+ }
94
+
95
+ export async function leaveHandler({ req, res, groups }) {
96
+ if (!groups) return json(res, 501, { error: "Groups service not configured" });
97
+ let body;
98
+ try {
99
+ body = await readBody(req);
100
+ } catch {
101
+ return json(res, 400, { error: "Invalid JSON body" });
102
+ }
103
+ if (!body?.name) return json(res, 400, { error: "group name is required" });
104
+ const auth = /^Bearer (.+)$/.exec(req.headers["authorization"] || "");
105
+ if (!auth) return json(res, 401, { error: "bearer token required" });
106
+ const group = groups.list()[body.name];
107
+ if (!group) return json(res, 404, { error: `group "${body.name}" not found` });
108
+ const hit = groups.membersForToken(body.name, auth[1]);
109
+ if (!hit) return json(res, 403, { error: "invalid member token" });
110
+ try {
111
+ const removed = groups.removeMember(body.name, { url: hit.member.url });
112
+ return json(res, 200, {
113
+ object: "group",
114
+ name: body.name,
115
+ removed: removed?.removed ?? null,
116
+ members: groups.list()[body.name]?.members ?? {},
117
+ });
118
+ } catch (err) {
119
+ return json(res, 400, { error: errMsg(err) });
120
+ }
121
+ }