mslxdff 0.1.12 → 0.1.14
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 +45 -136
- package/package.json +1 -1
- package/src/daemon.js +5 -1
- package/src/routes.js +8 -46
- package/src/server.js +5 -3
package/bin/mslxdff.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
-
import { readFileSync, existsSync, statSync
|
|
3
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { dirname, join, basename } from "node:path";
|
|
6
6
|
import { startServer, resolvePort } from "../src/server.js";
|
|
@@ -14,7 +14,7 @@ import { createAutoSelector } from "../src/auto.js";
|
|
|
14
14
|
import { createPeersService } from "../src/peers.js";
|
|
15
15
|
import { createEventBus } from "../src/events.js";
|
|
16
16
|
import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../src/groups.js";
|
|
17
|
-
import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent,
|
|
17
|
+
import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent, recentEvents } from "../src/logs.js";
|
|
18
18
|
|
|
19
19
|
const logs = { appendCall, appendError, appendEvent };
|
|
20
20
|
|
|
@@ -132,11 +132,22 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
132
132
|
process.exit(0);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
// -debug:
|
|
136
|
-
//
|
|
135
|
+
// -debug: stop the background daemon and run the server in THIS terminal
|
|
136
|
+
// (foreground), printing every event to stdout in real time via the in-memory
|
|
137
|
+
// event bus — no filesystem polling. Ctrl+C / SIGTERM restarts the daemon in
|
|
138
|
+
// the background, then exits. See the daemon body below for the stream wiring.
|
|
137
139
|
if (args.includes("-debug") || args.includes("--debug")) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
const recent = recentEvents(100);
|
|
141
|
+
if (recent.length) {
|
|
142
|
+
console.log(`--- last ${recent.length} event(s) ---`);
|
|
143
|
+
for (const e of recent) console.log(fmtEvent(e));
|
|
144
|
+
}
|
|
145
|
+
console.log("--- live (Ctrl+C: stop debugging and restore background daemon) ---");
|
|
146
|
+
const { stopped, pid } = stopDaemon();
|
|
147
|
+
if (stopped) console.log(`[debug] stopped background daemon (pid ${pid})`);
|
|
148
|
+
process.env.MSLXDFF_DEBUG = "1";
|
|
149
|
+
process.env.MSLXDFF_DAEMON = "1";
|
|
150
|
+
// fall through to the daemon body below — no process.exit() here
|
|
140
151
|
}
|
|
141
152
|
|
|
142
153
|
// -creategroup <name> | -group create <name> | -group sync | -group leave <name> | -group list |
|
|
@@ -407,9 +418,34 @@ const peers = createPeersService({ cooldownMs: peerCooldownMs(), heatMs: peerHea
|
|
|
407
418
|
const groups = createGroupsService({});
|
|
408
419
|
const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshold() });
|
|
409
420
|
|
|
421
|
+
const isDebug = process.env.MSLXDFF_DEBUG === "1";
|
|
410
422
|
const bus = createEventBus();
|
|
411
423
|
const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans, bus });
|
|
412
|
-
const srv = startServer({ router });
|
|
424
|
+
const srv = startServer({ router, signals: !isDebug });
|
|
425
|
+
|
|
426
|
+
// -debug: push every event straight to this terminal.
|
|
427
|
+
if (isDebug) {
|
|
428
|
+
bus.subscribe((e) => {
|
|
429
|
+
try {
|
|
430
|
+
console.log(fmtEvent(e));
|
|
431
|
+
} catch {
|
|
432
|
+
// malformed event — skip
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
// Ctrl+C / SIGTERM: restore the background daemon, then exit.
|
|
436
|
+
const restore = () => {
|
|
437
|
+
console.log("\n[debug] restoring background daemon...");
|
|
438
|
+
try {
|
|
439
|
+
const restoredPid = startDaemon([]);
|
|
440
|
+
console.log(`[debug] daemon restored (pid ${restoredPid})`);
|
|
441
|
+
} catch (err) {
|
|
442
|
+
console.error(`[debug] could not restore daemon: ${err.message}`);
|
|
443
|
+
}
|
|
444
|
+
setTimeout(() => process.exit(0), 300);
|
|
445
|
+
};
|
|
446
|
+
process.on("SIGINT", restore);
|
|
447
|
+
process.on("SIGTERM", restore);
|
|
448
|
+
}
|
|
413
449
|
|
|
414
450
|
await srv.ready();
|
|
415
451
|
models.startAutoRefresh();
|
|
@@ -709,135 +745,8 @@ function fmtEvent(e) {
|
|
|
709
745
|
}
|
|
710
746
|
}
|
|
711
747
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
// involved on the live path. Falls back to file polling if the stream fails.
|
|
715
|
-
async function liveDebug() {
|
|
716
|
-
const file = eventsFile();
|
|
717
|
-
const dir = dirname(file);
|
|
718
|
-
const recent = recentEvents(100);
|
|
719
|
-
if (recent.length) {
|
|
720
|
-
console.log(`--- last ${recent.length} event(s) ---`);
|
|
721
|
-
for (const e of recent) console.log(fmtEvent(e));
|
|
722
|
-
}
|
|
723
|
-
console.log("--- live (Ctrl+C to exit) ---");
|
|
724
|
-
|
|
725
|
-
try {
|
|
726
|
-
await streamEventsHttp();
|
|
727
|
-
return; // stream kept running until Ctrl+C
|
|
728
|
-
} catch (err) {
|
|
729
|
-
console.error(`[debug] streaming failed (${err.message}) — falling back to file polling`);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// File-poll fallback (daemon too old to expose the SSE endpoint).
|
|
733
|
-
if (!existsSync(file)) {
|
|
734
|
-
try {
|
|
735
|
-
mkdirSync(dir, { recursive: true });
|
|
736
|
-
closeSync(openSync(file, "a"));
|
|
737
|
-
} catch {
|
|
738
|
-
console.error(`cannot create event file: ${file}`);
|
|
739
|
-
process.exit(1);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
let pos = existsSync(file) ? statSync(file).size : 0;
|
|
743
|
-
let buf = "";
|
|
744
|
-
const pump = () => {
|
|
745
|
-
try {
|
|
746
|
-
if (!existsSync(file)) {
|
|
747
|
-
pos = 0;
|
|
748
|
-
buf = "";
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
const size = statSync(file).size;
|
|
752
|
-
if (size < pos) {
|
|
753
|
-
pos = 0;
|
|
754
|
-
buf = "";
|
|
755
|
-
}
|
|
756
|
-
if (size === pos) return;
|
|
757
|
-
buf += readFileSync(file, "utf8").slice(pos);
|
|
758
|
-
pos = size;
|
|
759
|
-
const lines = buf.split("\n");
|
|
760
|
-
buf = lines.pop() || "";
|
|
761
|
-
for (const l of lines) {
|
|
762
|
-
if (!l.trim()) continue;
|
|
763
|
-
try {
|
|
764
|
-
console.log(fmtEvent(JSON.parse(l)));
|
|
765
|
-
} catch {
|
|
766
|
-
// partial/corrupt line while trimming — skip
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
} catch (err) {
|
|
770
|
-
console.error(`[debug] poll error: ${err.message}`);
|
|
771
|
-
}
|
|
772
|
-
};
|
|
773
|
-
setInterval(pump, 500);
|
|
774
|
-
pump();
|
|
775
|
-
await new Promise(() => {}); // run until Ctrl+C
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Connect to the daemon's SSE stream (authenticated), print each event.
|
|
779
|
-
// Resolves when the connection ends; throws on connect failure.
|
|
780
|
-
async function streamEventsHttp() {
|
|
781
|
-
const { token } = await loadToken();
|
|
782
|
-
const port = getPort() || DEFAULT_PORT;
|
|
783
|
-
const url = `http://127.0.0.1:${port}/v1/_debug/stream`;
|
|
784
|
-
// daemon may still be starting when -debug is launched right after it —
|
|
785
|
-
// retry a few times before giving up on the stream.
|
|
786
|
-
let res = null;
|
|
787
|
-
let lastErr = null;
|
|
788
|
-
for (let attempt = 1; attempt <= 10 && !res; attempt++) {
|
|
789
|
-
try {
|
|
790
|
-
res = await fetch(url, {
|
|
791
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
792
|
-
signal: AbortSignal.timeout(10_000),
|
|
793
|
-
});
|
|
794
|
-
} catch (err) {
|
|
795
|
-
lastErr = err;
|
|
796
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
if (!res) throw lastErr || new Error("cannot reach daemon");
|
|
800
|
-
if (res.status === 401) throw new Error("unauthorized (token mismatch?)");
|
|
801
|
-
if (res.status === 404) throw new Error("daemon too old (no stream endpoint)");
|
|
802
|
-
if (!res.ok) throw new Error(`daemon responded ${res.status}`);
|
|
803
|
-
const reader = res.body.getReader();
|
|
804
|
-
const decoder = new TextDecoder();
|
|
805
|
-
let chunk = "";
|
|
806
|
-
const first = await Promise.race([
|
|
807
|
-
reader.read(),
|
|
808
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("no data from stream")), 15_000)),
|
|
809
|
-
]);
|
|
810
|
-
if (first.done) return;
|
|
811
|
-
console.log(`streaming from http://127.0.0.1:${port} (Ctrl+C to exit)`);
|
|
812
|
-
chunk += decoder.decode(first.value, { stream: true });
|
|
813
|
-
const lines = chunk.split("\n");
|
|
814
|
-
chunk = lines.pop() || "";
|
|
815
|
-
for (const l of lines) {
|
|
816
|
-
if (!l.startsWith("data: ")) continue;
|
|
817
|
-
try {
|
|
818
|
-
console.log(fmtEvent(JSON.parse(l.slice(6))));
|
|
819
|
-
} catch {
|
|
820
|
-
// skip malformed frames
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
while (true) {
|
|
824
|
-
const { done, value } = await reader.read();
|
|
825
|
-
if (done) break;
|
|
826
|
-
chunk += decoder.decode(value, { stream: true });
|
|
827
|
-
const frameLines = chunk.split("\n");
|
|
828
|
-
chunk = frameLines.pop() || "";
|
|
829
|
-
for (const l of frameLines) {
|
|
830
|
-
if (!l.startsWith("data: ")) continue;
|
|
831
|
-
try {
|
|
832
|
-
console.log(fmtEvent(JSON.parse(l.slice(6))));
|
|
833
|
-
} catch {
|
|
834
|
-
// skip malformed frames
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function fmtTs(iso) { if (!iso) return "-";
|
|
748
|
+
function fmtTs(iso) {
|
|
749
|
+
if (!iso) return "-";
|
|
841
750
|
try {
|
|
842
751
|
return new Date(iso).toISOString().replace("T", " ").slice(5, 19);
|
|
843
752
|
} catch {
|
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -24,10 +24,14 @@ export function startDaemon(args = []) {
|
|
|
24
24
|
const dir = daemonDir();
|
|
25
25
|
mkdirSync(dir, { recursive: true });
|
|
26
26
|
const logFd = openSync(logFile(), "a", 0o600);
|
|
27
|
+
const env = { ...process.env, MSLXDFF_DAEMON: "1" };
|
|
28
|
+
// a -debug foreground session wouldn't pass MSLXDFF_DEBUG to the
|
|
29
|
+
// background daemon it restores (that flag means "print events to stdout")
|
|
30
|
+
delete env.MSLXDFF_DEBUG;
|
|
27
31
|
const child = spawn(process.execPath, [entry, ...args, "--daemon"], {
|
|
28
32
|
detached: true,
|
|
29
33
|
stdio: ["ignore", logFd, logFd],
|
|
30
|
-
env
|
|
34
|
+
env,
|
|
31
35
|
});
|
|
32
36
|
child.unref();
|
|
33
37
|
return child.pid;
|
package/src/routes.js
CHANGED
|
@@ -13,16 +13,6 @@ export function createRouter({ token, upstream, models, auto, logs, peers, maxHo
|
|
|
13
13
|
|
|
14
14
|
const route = ROUTES.find((r) => r.method === method && r.path === path);
|
|
15
15
|
|
|
16
|
-
if (bus && path === "/v1/_debug/stream") {
|
|
17
|
-
// live debug stream (SSE): auth REQUIRED, then replay backlog + push
|
|
18
|
-
if (!authorized(req, token)) {
|
|
19
|
-
res.statusCode = 401;
|
|
20
|
-
res.setHeader("WWW-Authenticate", "Bearer");
|
|
21
|
-
return json(res, 401, { error: "Unauthorized" });
|
|
22
|
-
}
|
|
23
|
-
return streamEvents(req, res, bus);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
16
|
if (!route) return notFound(res);
|
|
27
17
|
|
|
28
18
|
if (route.requiresAuth && !authorized(req, token)) {
|
|
@@ -35,35 +25,6 @@ export function createRouter({ token, upstream, models, auto, logs, peers, maxHo
|
|
|
35
25
|
};
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
// SSE endpoint consumed by `mslxdff -debug`: replays the buffered backlog,
|
|
39
|
-
// then pushes each new event as it happens. One second heartbeat keeps
|
|
40
|
-
// proxies/NAT from closing the connection.
|
|
41
|
-
function streamEvents(req, res, bus) {
|
|
42
|
-
res.statusCode = 200;
|
|
43
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
44
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
45
|
-
res.setHeader("Connection", "keep-alive");
|
|
46
|
-
res.flushHeaders?.();
|
|
47
|
-
|
|
48
|
-
for (const e of bus.replayAll()) {
|
|
49
|
-
res.write(`data: ${JSON.stringify(e)}\n\n`);
|
|
50
|
-
}
|
|
51
|
-
const unsubscribe = bus.subscribe((e) => {
|
|
52
|
-
res.write(`data: ${JSON.stringify(e)}\n\n`);
|
|
53
|
-
});
|
|
54
|
-
const heartbeat = setInterval(() => {
|
|
55
|
-
res.write(`: ping\n\n`);
|
|
56
|
-
}, 15_000);
|
|
57
|
-
res.on("close", () => {
|
|
58
|
-
clearInterval(heartbeat);
|
|
59
|
-
unsubscribe();
|
|
60
|
-
});
|
|
61
|
-
req.on("close", () => {
|
|
62
|
-
clearInterval(heartbeat);
|
|
63
|
-
unsubscribe();
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
28
|
function clientIp(req) {
|
|
68
29
|
const fwd = req.headers["x-forwarded-for"];
|
|
69
30
|
const head = typeof fwd === "string" ? fwd.split(",")[0].trim() : "";
|
|
@@ -279,14 +240,15 @@ const ROUTES = [
|
|
|
279
240
|
}
|
|
280
241
|
|
|
281
242
|
// local failed for this model: try peers ordered hot-first (reuse
|
|
282
|
-
// their last successful model without probing
|
|
283
|
-
// health probe before the forward
|
|
243
|
+
// their last successful model without probing when it matches the
|
|
244
|
+
// requested model), cold peers get a health probe before the forward
|
|
284
245
|
if (canForwardPeers) {
|
|
285
246
|
for (const peer of peers.ordered()) {
|
|
286
247
|
let target = null;
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
248
|
+
const prevModel = peers.stat(peer.url)?.model;
|
|
249
|
+
const hot = peers.isHot(peer.url) && prevModel === model;
|
|
250
|
+
if (hot) {
|
|
251
|
+
target = prevModel;
|
|
290
252
|
} else {
|
|
291
253
|
const healthy = await peerHealthyModels(peer);
|
|
292
254
|
if (!healthy.length) {
|
|
@@ -297,7 +259,7 @@ const ROUTES = [
|
|
|
297
259
|
continue;
|
|
298
260
|
}
|
|
299
261
|
evt("peer-health", { peer: peer.url, healthy, count: healthy.length });
|
|
300
|
-
target = healthy[0];
|
|
262
|
+
target = healthy.includes(model) ? model : healthy[0];
|
|
301
263
|
}
|
|
302
264
|
|
|
303
265
|
const t0 = performance.now();
|
|
@@ -309,7 +271,7 @@ const ROUTES = [
|
|
|
309
271
|
if (hot) {
|
|
310
272
|
const healthy = await peerHealthyModels(peer);
|
|
311
273
|
if (healthy.length) {
|
|
312
|
-
target = healthy[0];
|
|
274
|
+
target = healthy.includes(model) ? model : healthy[0];
|
|
313
275
|
const t1 = performance.now();
|
|
314
276
|
peerRes = await forwardToPeer(peer, body, target, hops);
|
|
315
277
|
latencyMs = Math.round(performance.now() - t1);
|
package/src/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer as httpCreateServer } from "node:http";
|
|
2
2
|
import { DEFAULT_PORT, getPort } from "./state.js";
|
|
3
3
|
|
|
4
|
-
export function startServer({ router }, port = resolvePort()) {
|
|
4
|
+
export function startServer({ router, signals = true }, port = resolvePort()) {
|
|
5
5
|
const server = httpCreateServer((req, res) => {
|
|
6
6
|
router(req, res).catch((err) => {
|
|
7
7
|
res.statusCode = 500;
|
|
@@ -24,8 +24,10 @@ export function startServer({ router }, port = resolvePort()) {
|
|
|
24
24
|
server.closeAllConnections?.();
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if (signals) {
|
|
28
|
+
process.on("SIGINT", close);
|
|
29
|
+
process.on("SIGTERM", close);
|
|
30
|
+
}
|
|
29
31
|
|
|
30
32
|
return { server, ready, close };
|
|
31
33
|
}
|