mslxdff 0.1.10 → 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 +110 -45
- 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,70 +709,132 @@ 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
|
-
|
|
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).
|
|
714
733
|
if (!existsSync(file)) {
|
|
715
734
|
try {
|
|
716
|
-
mkdirSync(
|
|
735
|
+
mkdirSync(dir, { recursive: true });
|
|
717
736
|
closeSync(openSync(file, "a"));
|
|
718
737
|
} catch {
|
|
719
738
|
console.error(`cannot create event file: ${file}`);
|
|
720
739
|
process.exit(1);
|
|
721
740
|
}
|
|
722
741
|
}
|
|
723
|
-
const recent = recentEvents(100);
|
|
724
|
-
if (recent.length) {
|
|
725
|
-
console.log(`--- last ${recent.length} event(s) ---`);
|
|
726
|
-
for (const e of recent) console.log(fmtEvent(e));
|
|
727
|
-
}
|
|
728
|
-
console.log("--- live (Ctrl+C to exit) ---");
|
|
729
|
-
if (!existsSync(file)) console.log("(no events yet — trigger a chat request to see the flow)");
|
|
730
|
-
|
|
731
742
|
let pos = existsSync(file) ? statSync(file).size : 0;
|
|
732
743
|
let buf = "";
|
|
733
744
|
const pump = () => {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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}`);
|
|
738
771
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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));
|
|
743
797
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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;
|
|
751
831
|
try {
|
|
752
|
-
console.log(fmtEvent(JSON.parse(l)));
|
|
832
|
+
console.log(fmtEvent(JSON.parse(l.slice(6))));
|
|
753
833
|
} catch {
|
|
754
|
-
//
|
|
834
|
+
// skip malformed frames
|
|
755
835
|
}
|
|
756
836
|
}
|
|
757
|
-
};
|
|
758
|
-
try {
|
|
759
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
760
|
-
watch(dirname(file), (_evt, filename) => {
|
|
761
|
-
if (!filename || basename(String(filename)) !== basename(file)) return;
|
|
762
|
-
pump();
|
|
763
|
-
});
|
|
764
|
-
} catch {
|
|
765
|
-
console.error(`cannot watch event dir: ${dirname(file)}`);
|
|
766
|
-
process.exit(1);
|
|
767
837
|
}
|
|
768
|
-
// fs.watch is unreliable for freshly-created files on Windows — poll as a
|
|
769
|
-
// safety net so events are never missed.
|
|
770
|
-
setInterval(pump, 250);
|
|
771
|
-
pump();
|
|
772
|
-
await new Promise(() => {}); // run until Ctrl+C
|
|
773
838
|
}
|
|
774
839
|
|
|
775
840
|
function fmtTs(iso) { if (!iso) return "-";
|
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);
|