mslxdff 0.1.11 → 0.1.12
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 +86 -28
- package/package.json +1 -1
- package/src/events.js +39 -0
- package/src/routes.js +48 -4
- package/src/server.js +3 -0
package/bin/mslxdff.js
CHANGED
|
@@ -4,6 +4,7 @@ import { readFileSync, existsSync, statSync, watch, openSync, closeSync, mkdirSy
|
|
|
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";
|
|
7
|
+
import { DEFAULT_PORT } from "../src/state.js";
|
|
7
8
|
import { createRouter } from "../src/routes.js";
|
|
8
9
|
import { createUpstreamClient } from "../src/upstream.js";
|
|
9
10
|
import { createModelsService } from "../src/models.js";
|
|
@@ -11,6 +12,7 @@ import { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroups
|
|
|
11
12
|
import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid, readPidVersion, isPidAlive } from "../src/daemon.js";
|
|
12
13
|
import { createAutoSelector } from "../src/auto.js";
|
|
13
14
|
import { createPeersService } from "../src/peers.js";
|
|
15
|
+
import { createEventBus } from "../src/events.js";
|
|
14
16
|
import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../src/groups.js";
|
|
15
17
|
import { logDir, recentCalls, lastError, appendCall, appendError, appendEvent, eventsFile, recentEvents } from "../src/logs.js";
|
|
16
18
|
|
|
@@ -405,7 +407,8 @@ const peers = createPeersService({ cooldownMs: peerCooldownMs(), heatMs: peerHea
|
|
|
405
407
|
const groups = createGroupsService({});
|
|
406
408
|
const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshold() });
|
|
407
409
|
|
|
408
|
-
const
|
|
410
|
+
const bus = createEventBus();
|
|
411
|
+
const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans, bus });
|
|
409
412
|
const srv = startServer({ router });
|
|
410
413
|
|
|
411
414
|
await srv.ready();
|
|
@@ -706,12 +709,27 @@ function fmtEvent(e) {
|
|
|
706
709
|
}
|
|
707
710
|
}
|
|
708
711
|
|
|
709
|
-
// Live-follow the daemon
|
|
710
|
-
//
|
|
712
|
+
// Live-follow the daemon: replay the file backlog, then stream events over
|
|
713
|
+
// HTTP (SSE) which the daemon pushes from memory — no filesystem watch/poll
|
|
714
|
+
// involved on the live path. Falls back to file polling if the stream fails.
|
|
711
715
|
async function liveDebug() {
|
|
712
716
|
const file = eventsFile();
|
|
713
|
-
// ensure the event file exists so watch() doesn't fail on a fresh daemon dir
|
|
714
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).
|
|
715
733
|
if (!existsSync(file)) {
|
|
716
734
|
try {
|
|
717
735
|
mkdirSync(dir, { recursive: true });
|
|
@@ -721,15 +739,6 @@ async function liveDebug() {
|
|
|
721
739
|
process.exit(1);
|
|
722
740
|
}
|
|
723
741
|
}
|
|
724
|
-
const recent = recentEvents(100);
|
|
725
|
-
if (recent.length) {
|
|
726
|
-
console.log(`--- last ${recent.length} event(s) ---`);
|
|
727
|
-
for (const e of recent) console.log(fmtEvent(e));
|
|
728
|
-
}
|
|
729
|
-
console.log("--- live (Ctrl+C to exit) ---");
|
|
730
|
-
console.log(`watching: ${file}`);
|
|
731
|
-
if (!existsSync(file)) console.log("(no events yet — trigger a chat request to see the flow)");
|
|
732
|
-
|
|
733
742
|
let pos = existsSync(file) ? statSync(file).size : 0;
|
|
734
743
|
let buf = "";
|
|
735
744
|
const pump = () => {
|
|
@@ -741,7 +750,7 @@ async function liveDebug() {
|
|
|
741
750
|
}
|
|
742
751
|
const size = statSync(file).size;
|
|
743
752
|
if (size < pos) {
|
|
744
|
-
pos = 0;
|
|
753
|
+
pos = 0;
|
|
745
754
|
buf = "";
|
|
746
755
|
}
|
|
747
756
|
if (size === pos) return;
|
|
@@ -761,24 +770,73 @@ async function liveDebug() {
|
|
|
761
770
|
console.error(`[debug] poll error: ${err.message}`);
|
|
762
771
|
}
|
|
763
772
|
};
|
|
764
|
-
|
|
765
|
-
// so polling is the source of truth (200ms); the directory watch just adds
|
|
766
|
-
// low-latency wake-ups and silently degrades when it misbehaves.
|
|
767
|
-
try {
|
|
768
|
-
mkdirSync(dir, { recursive: true });
|
|
769
|
-
watch(dir, (_evt, filename) => {
|
|
770
|
-
if (!filename || basename(String(filename)) !== basename(file)) return;
|
|
771
|
-
pump();
|
|
772
|
-
});
|
|
773
|
-
} catch {
|
|
774
|
-
console.error(`cannot watch event dir: ${dir} (falling back to polling only)`);
|
|
775
|
-
}
|
|
776
|
-
// Poll is the source of truth; 200ms keeps human-perceived latency negligible.
|
|
777
|
-
setInterval(pump, 200);
|
|
773
|
+
setInterval(pump, 500);
|
|
778
774
|
pump();
|
|
779
775
|
await new Promise(() => {}); // run until Ctrl+C
|
|
780
776
|
}
|
|
781
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
|
+
|
|
782
840
|
function fmtTs(iso) { if (!iso) return "-";
|
|
783
841
|
try {
|
|
784
842
|
return new Date(iso).toISOString().replace("T", " ").slice(5, 19);
|
package/package.json
CHANGED
package/src/events.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// In-memory event bus for live debugging: routes.js emits, -debug streams
|
|
2
|
+
// over HTTP. Keeps a small ring buffer so late subscribers can catch up.
|
|
3
|
+
|
|
4
|
+
const MAX_BUFFERED = 500;
|
|
5
|
+
|
|
6
|
+
export function createEventBus() {
|
|
7
|
+
let buffer = [];
|
|
8
|
+
const listeners = new Set();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
// emit an event to every current subscriber (sync, never throws)
|
|
12
|
+
emit(event) {
|
|
13
|
+
buffer.push(event);
|
|
14
|
+
if (buffer.length > MAX_BUFFERED) buffer.splice(0, buffer.length - MAX_BUFFERED);
|
|
15
|
+
for (const fn of listeners) {
|
|
16
|
+
try {
|
|
17
|
+
fn(event);
|
|
18
|
+
} catch {
|
|
19
|
+
// subscriber errors must never break the request path
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
// subscribe; returns an unsubscribe fn
|
|
24
|
+
subscribe(fn) {
|
|
25
|
+
listeners.add(fn);
|
|
26
|
+
return () => listeners.delete(fn);
|
|
27
|
+
},
|
|
28
|
+
// buffered events since start (or from offset), consumed left-to-right
|
|
29
|
+
replay({ since = 0 } = {}) {
|
|
30
|
+
return buffer.slice(since);
|
|
31
|
+
},
|
|
32
|
+
replayAll() {
|
|
33
|
+
return buffer.slice();
|
|
34
|
+
},
|
|
35
|
+
size() {
|
|
36
|
+
return buffer.length;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
package/src/routes.js
CHANGED
|
@@ -6,12 +6,23 @@ import { DEFAULT_MAX_HOPS } from "./peers.js";
|
|
|
6
6
|
|
|
7
7
|
export const errMsg = (err) => String(err?.message || err);
|
|
8
8
|
|
|
9
|
-
export function createRouter({ token, upstream, models, auto, logs, peers, maxHops = DEFAULT_MAX_HOPS, groups, bans }) {
|
|
9
|
+
export function createRouter({ token, upstream, models, auto, logs, peers, maxHops = DEFAULT_MAX_HOPS, groups, bans, bus }) {
|
|
10
10
|
return async function router(req, res) {
|
|
11
11
|
const method = req.method || "GET";
|
|
12
12
|
const path = (req.url || "").split("?")[0];
|
|
13
13
|
|
|
14
14
|
const route = ROUTES.find((r) => r.method === method && r.path === path);
|
|
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
|
+
|
|
15
26
|
if (!route) return notFound(res);
|
|
16
27
|
|
|
17
28
|
if (route.requiresAuth && !authorized(req, token)) {
|
|
@@ -20,10 +31,39 @@ export function createRouter({ token, upstream, models, auto, logs, peers, maxHo
|
|
|
20
31
|
return json(res, 401, { error: "Unauthorized" });
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
await route.handler({ req, res, upstream, models, auto, logs, peers, maxHops, groups, bans, token });
|
|
34
|
+
await route.handler({ req, res, upstream, models, auto, logs, peers, maxHops, groups, bans, token, bus });
|
|
24
35
|
};
|
|
25
36
|
}
|
|
26
37
|
|
|
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
|
+
|
|
27
67
|
function clientIp(req) {
|
|
28
68
|
const fwd = req.headers["x-forwarded-for"];
|
|
29
69
|
const head = typeof fwd === "string" ? fwd.split(",")[0].trim() : "";
|
|
@@ -175,7 +215,7 @@ const ROUTES = [
|
|
|
175
215
|
method: "POST",
|
|
176
216
|
path: "/v1/chat/completions",
|
|
177
217
|
requiresAuth: true,
|
|
178
|
-
handler: async ({ req, res, upstream, auto, logs, peers, maxHops }) => {
|
|
218
|
+
handler: async ({ req, res, upstream, auto, logs, peers, maxHops, bus }) => {
|
|
179
219
|
let body;
|
|
180
220
|
try {
|
|
181
221
|
body = await readBody(req);
|
|
@@ -205,7 +245,11 @@ const ROUTES = [
|
|
|
205
245
|
logs?.appendCall({ model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(body.stream) });
|
|
206
246
|
const logError = (model, status, message) =>
|
|
207
247
|
logs?.appendError({ model, auto: useAuto, status, message });
|
|
208
|
-
const evt = (type, data) =>
|
|
248
|
+
const evt = (type, data) => {
|
|
249
|
+
const entry = { ts: Date.now(), type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt };
|
|
250
|
+
if (bus) bus.emit(entry);
|
|
251
|
+
logs?.appendEvent?.(entry);
|
|
252
|
+
};
|
|
209
253
|
evt("request", { hops, ip: clientIp(req), stream: Boolean(body.stream), prompt: summarizePrompt(body) });
|
|
210
254
|
|
|
211
255
|
let lastErr = null;
|
package/src/server.js
CHANGED
|
@@ -19,6 +19,9 @@ export function startServer({ router }, port = resolvePort()) {
|
|
|
19
19
|
const close = () =>
|
|
20
20
|
new Promise((resolve) => {
|
|
21
21
|
server.close(resolve);
|
|
22
|
+
// SSE debug streams keep connections open — force-close so shutdown
|
|
23
|
+
// never waits on them.
|
|
24
|
+
server.closeAllConnections?.();
|
|
22
25
|
});
|
|
23
26
|
|
|
24
27
|
process.on("SIGINT", close);
|