mslxdff 0.1.65 → 0.1.67

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.
Files changed (65) hide show
  1. package/bin/mslxdff.js +3 -3159
  2. package/package.json +1 -1
  3. package/src/chat/cooling.js +99 -0
  4. package/src/chat/direct.js +57 -0
  5. package/src/chat/gateway.js +148 -0
  6. package/src/chat/orchestrator.js +218 -0
  7. package/src/chat/sse.js +69 -0
  8. package/src/chat/upstream.js +59 -501
  9. package/src/cli/bootstrap.js +1 -0
  10. package/src/cli/commands/daemon.js +151 -0
  11. package/src/cli/commands/group.js +272 -0
  12. package/src/cli/commands/model.js +405 -0
  13. package/src/cli/commands/provider/add.js +106 -0
  14. package/src/cli/commands/provider/allowlist.js +99 -0
  15. package/src/cli/commands/provider/config.js +82 -0
  16. package/src/cli/commands/provider/index.js +193 -0
  17. package/src/cli/commands/provider/keys.js +135 -0
  18. package/src/cli/commands/provider/models.js +99 -0
  19. package/src/cli/commands/provider.js +1 -0
  20. package/src/cli/commands/sync.js +143 -0
  21. package/src/cli/commands/system.js +236 -0
  22. package/src/cli/commands/workbuddy.js +91 -0
  23. package/src/cli/format.js +119 -0
  24. package/src/cli/group-helpers.js +69 -0
  25. package/src/cli/help.js +66 -0
  26. package/src/cli/index.js +59 -0
  27. package/src/cli/interactive.js +84 -0
  28. package/src/cli/policy.js +119 -0
  29. package/src/cli/provider-row.js +73 -0
  30. package/src/cli/status.js +283 -0
  31. package/src/cli/util.js +24 -0
  32. package/src/providers/base.js +159 -0
  33. package/src/providers/dispatcher.js +18 -6
  34. package/src/providers/generic.js +28 -166
  35. package/src/providers/openrouter.js +19 -205
  36. package/src/providers/workbuddy/auth.js +175 -0
  37. package/src/providers/workbuddy/balance.js +84 -0
  38. package/src/providers/workbuddy/chat.js +310 -0
  39. package/src/providers/workbuddy/index.js +263 -0
  40. package/src/providers/workbuddy/models.js +111 -0
  41. package/src/providers/workbuddy/rotation-log.js +54 -0
  42. package/src/providers/workbuddy.js +2 -677
  43. package/src/routes/chat/broadband-handler.js +25 -46
  44. package/src/routes/chat/exhausted-handler.js +2 -2
  45. package/src/routes/chat/gateway.js +301 -0
  46. package/src/routes/chat/hedge-handler.js +65 -83
  47. package/src/routes/chat/index.js +1 -384
  48. package/src/routes/chat/local-handler.js +32 -61
  49. package/src/routes/chat/peer-handler.js +32 -23
  50. package/src/routes/chat/relay-pipeline.js +151 -0
  51. package/src/runtime/bootstrap.js +408 -0
  52. package/src/state/facade.js +57 -0
  53. package/src/state/memory.js +161 -0
  54. package/src/state/merge.js +26 -0
  55. package/src/state/persist.js +42 -0
  56. package/src/state/provider-config.js +143 -0
  57. package/src/state/schemas/allowlist.js +87 -0
  58. package/src/state/schemas/group.js +31 -0
  59. package/src/state/schemas/model.js +53 -0
  60. package/src/state/schemas/peer.js +31 -0
  61. package/src/state/schemas/port.js +10 -0
  62. package/src/state/schemas/provider.js +204 -0
  63. package/src/state/schemas/token.js +43 -0
  64. package/src/state/store.js +176 -0
  65. package/src/state.js +1 -711
@@ -0,0 +1,151 @@
1
+ import { runHook } from "../../plugins.js";
2
+
3
+ /**
4
+ * RelayPipeline 深模块
5
+ * 把 5 个 handler 各自的 fallback→relay→scoring→事件 6段流水收敛为单一真相。
6
+ * 对外 1 接口:createRelayPipeline(deps) => { execute(ctx) }
7
+ * 设计要点:全部外部可注入,便于在 pipeline seam 上做行为测试;constants 注入便于单测加速。
8
+ */
9
+ export function createRelayPipeline({
10
+ relay,
11
+ buildFallbackInfo,
12
+ auto,
13
+ plugins,
14
+ evt,
15
+ mark,
16
+ logCall,
17
+ logError,
18
+ constants,
19
+ startedAt: defaultStartedAt,
20
+ stages: defaultStages,
21
+ perfNow,
22
+ } = {}) {
23
+ const C = {
24
+ STREAM_TIMEOUT_MS: 25_000,
25
+ SLOW_TOTAL_MS: 20_000,
26
+ STALL_TIMEOUT_MS: 0,
27
+ SCORE_STALL_MS: 15_000,
28
+ ...(constants || {}),
29
+ };
30
+ const _relay = relay;
31
+ const _build = buildFallbackInfo;
32
+ const _evt = evt || (() => {});
33
+ const _mark = mark || (() => {});
34
+ const _logCall = logCall || (() => {});
35
+ const _logError = logError || (() => {});
36
+ const _perfNow = perfNow || (() => Date.now());
37
+
38
+ async function execute({
39
+ res,
40
+ upRes,
41
+ body,
42
+ requested,
43
+ actual,
44
+ lastErr,
45
+ via,
46
+ lockModel,
47
+ useAuto,
48
+ handlerCtx,
49
+ mark: m2,
50
+ perf0,
51
+ stages: s2,
52
+ startedAt: sa2,
53
+ } = {}) {
54
+ const markFn = m2 || _mark;
55
+ const curStartedAt = sa2 ?? defaultStartedAt ?? Date.now();
56
+ const curStages = s2 ?? defaultStages ?? [];
57
+ const reqId = handlerCtx?.reqId;
58
+ const hops = handlerCtx?.hops;
59
+
60
+ // 1. logCall(pre) — 保持原 handler 的 logCall→fallback→relay-start 时序
61
+ try { _logCall(actual, upRes?.status); } catch {}
62
+ // 2. fallback + relay-start
63
+ let fallback = null;
64
+ try {
65
+ if (_build) fallback = _build({ requested, actual, lastErr, via, useAuto, lockModel });
66
+ } catch {}
67
+ if (fallback?.fallback) {
68
+ _evt("fallback-notice", { reqId, requested, actual, reason: fallback.reason, notice: fallback.notice, via, fallback: true });
69
+ }
70
+ _evt("relay-start", { reqId, model: actual, via, isStream: Boolean(body?.stream), fallback });
71
+
72
+ // 3. relay
73
+ const out = await _relay(res, upRes, body, {
74
+ fallback,
75
+ onFirstChunk: (delta) => {
76
+ try { markFn(`ttf-${actual}`); } catch {}
77
+ _evt("relay-first-chunk", { reqId, model: actual, ttfMs: delta, via });
78
+ if (plugins?.length) runHook(plugins, "relay:first-chunk", { reqId, requested, model: actual, via, ttfMs: delta }).catch(() => {});
79
+ },
80
+ onDownstreamAbort: () => {
81
+ _evt("client-abort", { reqId, model: actual, totalMs: Math.round(_perfNow() - (perf0 ?? 0)), stages: [...curStages] });
82
+ },
83
+ });
84
+
85
+ // 4. relay-done
86
+ _evt("relay-done", {
87
+ reqId,
88
+ model: actual,
89
+ via,
90
+ status: out.status,
91
+ ttfMs: out.ttfMs,
92
+ totalMs: out.totalMs,
93
+ aborted: out.aborted,
94
+ interrupted: out.interrupted ?? false,
95
+ detail: out.detail ?? null,
96
+ });
97
+
98
+ // 5a. 首块超时未写字节 → 回退
99
+ if (out.status === C.STREAM_TIMEOUT_MS) {
100
+ if (auto) try { await auto.recordError(actual, { status: 502, slow: true, note: `stream timeout ${C.STREAM_TIMEOUT_MS}ms` }); } catch {}
101
+ try { _logError(actual, 502, `stream timeout ${C.STREAM_TIMEOUT_MS}ms`); } catch {}
102
+ _evt("upstream-error", { reqId, model: actual, status: 502, message: "stream timeout", timing: null });
103
+ _evt("fallback", { reqId, from: actual, to: null, reason: "stream timeout" });
104
+ return { handled: false, upRes: null, lastErr: { model: actual, upstream: null, status: 502, message: `stream timed out after ${C.STREAM_TIMEOUT_MS}ms` } };
105
+ }
106
+
107
+ // 5b. 中断(stall 超时 / max 流时长)
108
+ if (out.interrupted) {
109
+ if (auto) {
110
+ try { await auto.recordError(actual, { status: 200, slow: true, note: `stall ${C.STALL_TIMEOUT_MS}ms` }); } catch {}
111
+ try { await auto.recordLatency(actual, out.totalMs ?? (Date.now() - curStartedAt)); } catch {}
112
+ }
113
+ _evt("slow-model", { reqId, model: actual, elapsedMs: out.totalMs ?? (Date.now() - curStartedAt), threshold: C.STALL_TIMEOUT_MS, interrupted: true, detail: out.detail ?? null });
114
+ try { _logCall(actual, 200); } catch {}
115
+ _evt("result", { reqId, model: actual, status: out.status, via, timing: upRes?._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, interrupted: true, detail: out.detail ?? null, fallback, requested, actual });
116
+ _evt("client-response", { requested, actual, via, fallback, status: out.status, reqId, interrupted: true });
117
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(body?.stream), durationMs: Date.now() - curStartedAt, via, status: out.status, actual, interrupted: true, fallback }).catch(() => {});
118
+ return { handled: true };
119
+ }
120
+
121
+ // 5c. 慢速计分 + ok
122
+ const elapsed = Date.now() - curStartedAt;
123
+ const latencyMs = out.totalMs ?? elapsed;
124
+ let scoredSlow = false;
125
+
126
+ if (C.SLOW_TOTAL_MS && auto && elapsed > C.SLOW_TOTAL_MS && out.status === 200) {
127
+ try { await auto.recordError(actual, { status: 200, slow: true, note: `slow ${elapsed}ms` }); } catch {}
128
+ try { await auto.recordLatency(actual, latencyMs); } catch {}
129
+ _evt("slow-model", { reqId, model: actual, elapsedMs: elapsed, threshold: C.SLOW_TOTAL_MS, reason: "total", detail: out.detail ?? null });
130
+ scoredSlow = true;
131
+ }
132
+ if (out.detail?.stallHits > 0 && auto && out.status === 200) {
133
+ try { await auto.recordError(actual, { status: 200, slow: true, note: `stall ${out.detail.stallHits}x gap>${C.SCORE_STALL_MS}ms maxGap ${out.detail.maxGapMs}ms` }); } catch {}
134
+ try { await auto.recordLatency(actual, latencyMs); } catch {}
135
+ _evt("slow-model", { reqId, model: actual, elapsedMs: elapsed, threshold: C.SCORE_STALL_MS, reason: "stall", stallHits: out.detail.stallHits, maxGapMs: out.detail.maxGapMs, detail: out.detail ?? null });
136
+ scoredSlow = true;
137
+ }
138
+ if (!scoredSlow && auto && out.status === 200) {
139
+ try { await auto.recordOk(actual, { latencyMs }); } catch {}
140
+ } else if (!scoredSlow && auto) {
141
+ try { await auto.recordLatency(actual, latencyMs); } catch {}
142
+ }
143
+
144
+ _evt("result", { reqId, model: actual, status: out.status, via, timing: upRes?._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null, fallback, requested, actual });
145
+ _evt("client-response", { requested, actual, via, fallback, status: out.status, reqId });
146
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(body?.stream), durationMs: Date.now() - curStartedAt, via, status: out.status, actual, fallback }).catch(() => {});
147
+ return { handled: true };
148
+ }
149
+
150
+ return { execute };
151
+ }
@@ -0,0 +1,408 @@
1
+ import { join, dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { startServer } from "../server.js";
4
+ import { defaultStateFile } from "../state.js";
5
+ import { createRouter } from "../routes.js";
6
+ import { createUpstreamClient } from "../upstream.js";
7
+ import { createModelsService } from "../models.js";
8
+ import { loadToken } from "../state.js";
9
+ import { createAutoSelector } from "../auto.js";
10
+ import { createPeersService } from "../peers.js";
11
+ import { createEventBus } from "../events.js";
12
+ import { createGroupsService, createBansService, refreshGroupMembers, syncPeersFromMembers } from "../groups.js";
13
+ import { logDir, appendCall, appendError, appendEvent } from "../logs.js";
14
+ import { loadPlugins, runHook, resolvePluginDirs } from "../plugins.js";
15
+ import { createOpenCodeProvider } from "../providers/opencode.js";
16
+ import { loadProviderKeys, loadProviderAuths, loadProviderConfigs } from "../state.js";
17
+ import { effectiveHost, refreshIntervalMs, modelCooldownMs, slowCooldownMs, peerCooldownMs, peerHeatMs, maxHopsValue, groupSyncIntervalMs, autoUpdateIntervalMs, banWindowMs, banThreshold } from "./policy.js";
18
+ import { fmtEvent } from "./format.js";
19
+ import { errMsg, npmCmd, run } from "./util.js";
20
+ import { loadGroupsJoined } from "../state.js";
21
+ import { writePid } from "../daemon.js";
22
+ import { resolvePort } from "../server.js";
23
+
24
+ const logs = { appendCall, appendError, appendEvent };
25
+
26
+ export async function startDaemonMain(VERSION) {
27
+ const { token, created } = await loadToken();
28
+ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
29
+ // src/cli/bootstrap.js -> 2 levels up to src, then 1 to root
30
+ const pkgRoot2 = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
31
+ const pluginDirs = resolvePluginDirs({ pkgRoot: pkgRoot2 });
32
+ const { plugins: loadedPlugins, errors: pluginErrors } = await loadPlugins({ dirs: pluginDirs });
33
+ for (const e of pluginErrors) {
34
+ console.log(`plugin load failed: ${e.file} — ${e.error}`);
35
+ appendEvent({ ts: Date.now(), type: "plugin-load-error", file: e.file, error: e.error });
36
+ }
37
+ if (loadedPlugins.length) {
38
+ console.log(`plugins loaded (${loadedPlugins.length}): ${loadedPlugins.map((p) => `${p.name}${p.version ? `@${p.version}` : ""}`).join(", ")}`);
39
+ appendEvent({ ts: Date.now(), type: "plugins-loaded", plugins: loadedPlugins.map((p) => ({ name: p.name, version: p.version })) });
40
+ }
41
+ const upstreamHooks = loadedPlugins.length
42
+ ? (name, ctx) => runHook(loadedPlugins, name, ctx)
43
+ : null;
44
+ const providerPlugin = loadedPlugins.find((p) => typeof p.createUpstream === "function");
45
+ let upstream;
46
+ const baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
47
+ let providers = [];
48
+ if (providerPlugin) {
49
+ if (!upstream) {
50
+ try {
51
+ upstream = await providerPlugin.createUpstream({ baseUrl, authToken: process.env.UPSTREAM_AUTH_TOKEN || "public", env: process.env });
52
+ console.log(`upstream provider replaced by plugin: ${providerPlugin.name}`);
53
+ appendEvent({ ts: Date.now(), type: "plugin-upstream-active", plugin: providerPlugin.name });
54
+ } catch (err) {
55
+ console.log(`plugin upstream (${providerPlugin.name}) failed: ${errMsg(err)} — falling back to default`);
56
+ appendEvent({ ts: Date.now(), type: "plugin-upstream-error", plugin: providerPlugin.name, error: errMsg(err) });
57
+ }
58
+ }
59
+ if (!upstream) upstream = createUpstreamClient({ hooks: upstreamHooks });
60
+ } else {
61
+ const opencodeClient = createUpstreamClient({ hooks: upstreamHooks });
62
+ const opencodeModels = createModelsService({
63
+ baseUrl,
64
+ headers: opencodeClient.headers,
65
+ refreshMs: refreshIntervalMs(),
66
+ cacheFile: join(logDir(), "models.json"),
67
+ });
68
+ providers.push(createOpenCodeProvider({ upstream: opencodeClient, modelsService: opencodeModels }));
69
+ const orKeys = loadProviderKeys("openrouter");
70
+ if (orKeys.length) {
71
+ const { createOpenRouterProvider } = await import("../providers/openrouter.js");
72
+ providers.push(createOpenRouterProvider({ apiKeys: orKeys }));
73
+ console.log(`provider enabled: openrouter (${orKeys.length} key${orKeys.length > 1 ? "s" : ""})`);
74
+ appendEvent({ ts: Date.now(), type: "provider-enabled", provider: "openrouter", keys: orKeys.length });
75
+ }
76
+ const genericConfigs = loadProviderConfigs();
77
+ for (const [gid, cfg] of Object.entries(genericConfigs)) {
78
+ if (gid === "opencode" || gid === "openrouter") continue;
79
+ if (gid === "workbuddy") {
80
+ const base = String(cfg?.baseUrl || "").trim() || "https://copilot.tencent.com";
81
+ const keys = Array.isArray(cfg?.keys) ? cfg.keys.filter((k) => typeof k === "string" && k.trim()) : [];
82
+ const auths = Array.isArray(cfg?.auths) ? cfg.auths : [];
83
+ if (!keys.length) continue;
84
+ try {
85
+ const { createWorkbuddyProvider } = await import("../providers/workbuddy.js");
86
+ providers.push(createWorkbuddyProvider({ baseUrl: base, apiKeys: keys, auths }));
87
+ console.log(`provider enabled: workbuddy (${keys.length} key${keys.length > 1 ? "s" : ""}) baseUrl=${base}`);
88
+ appendEvent({ ts: Date.now(), type: "provider-enabled", provider: gid, keys: keys.length, baseUrl: base });
89
+ } catch (err) {
90
+ console.log(`provider ${gid} failed: ${err?.message || err}`);
91
+ appendEvent({ ts: Date.now(), type: "provider-error", provider: gid, error: String(err?.message || err) });
92
+ }
93
+ continue;
94
+ }
95
+ const base = String(cfg?.baseUrl || "").trim();
96
+ const keys = Array.isArray(cfg?.keys) ? cfg.keys.filter((k) => typeof k === "string" && k.trim()) : [];
97
+ if (!base || !keys.length) continue;
98
+ try {
99
+ const { createGenericProvider } = await import("../providers/generic.js");
100
+ providers.push(createGenericProvider({ id: gid, baseUrl: base, apiKeys: keys }));
101
+ console.log(`provider enabled: ${gid} (${keys.length} key${keys.length > 1 ? "s" : ""}) baseUrl=${base}`);
102
+ appendEvent({ ts: Date.now(), type: "provider-enabled", provider: gid, keys: keys.length, baseUrl: base });
103
+ } catch (err) {
104
+ console.log(`provider ${gid} failed: ${err?.message || err}`);
105
+ appendEvent({ ts: Date.now(), type: "provider-error", provider: gid, error: String(err?.message || err) });
106
+ }
107
+ }
108
+ if (!genericConfigs["workbuddy"]) {
109
+ const wbKeys = loadProviderKeys("workbuddy");
110
+ if (wbKeys.length) {
111
+ try {
112
+ const { createWorkbuddyProvider } = await import("../providers/workbuddy.js");
113
+ const wbAuths = loadProviderAuths("workbuddy");
114
+ providers.push(createWorkbuddyProvider({ apiKeys: wbKeys, auths: wbAuths }));
115
+ console.log(`provider enabled: workbuddy (${wbKeys.length} key${wbKeys.length > 1 ? "s" : ""}) baseUrl=https://copilot.tencent.com (env)`);
116
+ appendEvent({ ts: Date.now(), type: "provider-enabled", provider: "workbuddy", keys: wbKeys.length, baseUrl: "https://copilot.tencent.com" });
117
+ } catch (err) {
118
+ console.log(`provider workbuddy failed: ${err?.message || err}`);
119
+ }
120
+ }
121
+ }
122
+ const { createProviderDispatcher } = await import("../providers/dispatcher.js");
123
+ upstream = createProviderDispatcher(providers);
124
+ appendEvent({ ts: Date.now(), type: "providers", providers: providers.map((p) => p.id) });
125
+ }
126
+ const opencodeProvider = providers.find((p) => p.id === "opencode");
127
+ const models = createModelsService({
128
+ providers: providers.length > 1 ? providers : undefined,
129
+ baseUrl,
130
+ headers: upstream.headers || opencodeProvider?.upstream?.headers,
131
+ refreshMs: refreshIntervalMs(),
132
+ cacheFile: join(logDir(), "models.json"),
133
+ });
134
+ const auto = createAutoSelector({
135
+ cooldownMs: modelCooldownMs(),
136
+ slowCooldownMs: slowCooldownMs(),
137
+ file: defaultStateFile(),
138
+ loadCandidates: async () => {
139
+ try {
140
+ return (await models.get()).data.map((m) => m.id);
141
+ } catch {
142
+ return null;
143
+ }
144
+ },
145
+ });
146
+ const peers = createPeersService({ cooldownMs: peerCooldownMs(), heatMs: peerHeatMs() });
147
+ const groups = createGroupsService({});
148
+ const bans = createBansService({ windowMs: banWindowMs(), threshold: banThreshold() });
149
+
150
+ const isDebug = process.env.MSLXDFF_DEBUG === "1";
151
+ const bus = createEventBus();
152
+
153
+ try {
154
+ const { loadModelAliases } = await import("../providers/model-id.js");
155
+ loadModelAliases();
156
+ } catch {}
157
+
158
+ const router = createRouter({ token, upstream, models, auto, logs, peers, maxHops: maxHopsValue(), groups, bans, bus, plugins: loadedPlugins });
159
+ const listenHost = effectiveHost();
160
+ const srv = startServer({
161
+ router,
162
+ signals: !isDebug,
163
+ host: listenHost,
164
+ onBeforeClose: loadedPlugins.length
165
+ ? () => runHook(loadedPlugins, "server:stop", { version: VERSION }).then(() => {})
166
+ : undefined,
167
+ });
168
+
169
+ if (isDebug) {
170
+ bus.subscribe((e) => {
171
+ try {
172
+ console.log(fmtEvent(e));
173
+ } catch {}
174
+ });
175
+ const { startDaemon: sd } = await import("../daemon.js");
176
+ const restore2 = () => {
177
+ console.log("\n[debug] restoring background daemon...");
178
+ try {
179
+ const restoredPid = sd([]);
180
+ console.log(`[debug] daemon restored (pid ${restoredPid})`);
181
+ } catch (err) {
182
+ console.error(`[debug] could not restore daemon: ${err.message}`);
183
+ }
184
+ setTimeout(() => process.exit(0), 300);
185
+ };
186
+ process.on("SIGINT", restore2);
187
+ process.on("SIGTERM", restore2);
188
+ }
189
+
190
+ await srv.ready();
191
+
192
+ if (loadedPlugins.length) {
193
+ runHook(loadedPlugins, "server:start", { port: srv.server.address()?.port, host: listenHost, version: VERSION }).catch(() => {});
194
+ const eventPlugins = loadedPlugins.filter((p) => typeof p.onEvent === "function");
195
+ if (eventPlugins.length) {
196
+ bus.subscribe((e) => {
197
+ for (const p of eventPlugins) {
198
+ try { p.onEvent(e); } catch {}
199
+ }
200
+ });
201
+ }
202
+ }
203
+
204
+ setTimeout(() => {
205
+ upstream.preheat().then((r) => {
206
+ const entry = { ts: Date.now(), type: "upstream-preheat", ...r, baseUrl };
207
+ try { bus.emit(entry); } catch {}
208
+ try { logs.appendEvent(entry); } catch {}
209
+ if (r.skipped) console.log(`[preheat] skipped (MSLXDFF_PREHEAT disabled)`);
210
+ else if (r.ok) console.log(`[preheat] opencode models ok ${r.status} ${r.ms}ms`);
211
+ else console.log(`[preheat] opencode models failed ${r.error || r.status || ""} ${r.ms || 0}ms`);
212
+ }).catch(() => {});
213
+ }, 100).unref?.();
214
+
215
+ models.startAutoRefresh();
216
+ if (process.env.MSLXDFF_DAEMON) {
217
+ writePid(process.pid, VERSION);
218
+ }
219
+ const addr = srv.server.address();
220
+ const host = addr.address === "0.0.0.0" || addr.address === "::" ? "localhost" : addr.address;
221
+ console.log(`mslxdff v${VERSION} listening on http://${host}:${addr.port}`);
222
+ if (created) {
223
+ console.log(`auth token: ${token}`);
224
+ }
225
+ console.log(`endpoint: http://${host}:${addr.port}/v1`);
226
+ try {
227
+ const { hedgeDelayMs } = await import("../routes/hedge.js");
228
+ const hd = hedgeDelayMs();
229
+ console.log(`hedge: ${hd ? `${hd}ms` : "off"} (MSLXDFF_HEDGE_DELAY_MS)`);
230
+ } catch {}
231
+
232
+ // group sync
233
+ const { syncAllJoinedGroups } = await import("./group-helpers.js");
234
+ syncAllJoinedGroups({ peers, groups })
235
+ .then((results) => {
236
+ for (const r of results) {
237
+ if (r.error) console.log(`group sync ${r.name}: failed — ${r.error}`);
238
+ else console.log(`group sync ${r.name}: ${r.total} member(s), ${r.added} peer(s)`);
239
+ }
240
+ })
241
+ .catch((err) => console.log(`group sync: ${errMsg(err)}`));
242
+ const groupSyncTimer = setInterval(() => {
243
+ syncAllJoinedGroups({ peers, groups })
244
+ .then((results) => {
245
+ for (const r of results) {
246
+ if (r.error) console.log(`group sync ${r.name}: failed — ${r.error}`);
247
+ else if (r.added) console.log(`group sync ${r.name}: ${r.total} member(s), ${r.added} peer(s)`);
248
+ }
249
+ })
250
+ .catch((err) => console.log(`group sync: ${errMsg(err)}`));
251
+ }, groupSyncIntervalMs());
252
+ groupSyncTimer.unref();
253
+
254
+ const broadbandGroups = () => loadGroupsJoined().filter((g) => g.kind === "broadband" && g.leaderUrl);
255
+ if (broadbandGroups().length) {
256
+ const doHeartbeat = async () => {
257
+ for (const g of broadbandGroups()) {
258
+ try {
259
+ const res = await fetch(`${g.leaderUrl}/v1/groups/relay/heartbeat`, {
260
+ method: "POST",
261
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
262
+ body: JSON.stringify({ name: g.name, group: g.name }),
263
+ signal: AbortSignal.timeout(5000),
264
+ });
265
+ if (!res.ok) {
266
+ const txt = await res.text().catch(() => "");
267
+ console.log(`broadband heartbeat ${g.name}: ${res.status} ${txt.slice(0, 100)}`);
268
+ }
269
+ } catch (err) {
270
+ console.log(`broadband heartbeat ${g.name}: failed — ${errMsg(err)}`);
271
+ }
272
+ }
273
+ };
274
+ const doPoll = async () => {
275
+ for (const g of broadbandGroups()) {
276
+ try {
277
+ const pollRes = await fetch(`${g.leaderUrl}/v1/groups/relay/poll`, {
278
+ method: "POST",
279
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
280
+ body: JSON.stringify({ name: g.name, group: g.name }),
281
+ signal: AbortSignal.timeout(8000),
282
+ });
283
+ if (!pollRes.ok) continue;
284
+ const data = await pollRes.json().catch(() => ({}));
285
+ const items = data.data || [];
286
+ for (const item of items) {
287
+ const { reqId, body } = item;
288
+ let result;
289
+ try {
290
+ const upRes = await upstream.chat(body);
291
+ const ct = upRes.headers.get("content-type") || "";
292
+ const isStream = Boolean(body?.stream) || ct.includes("text/event-stream");
293
+ if (isStream && upRes.body) {
294
+ let collected = "";
295
+ for await (const chunk of upRes.body) {
296
+ collected += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
297
+ }
298
+ result = { status: upRes.status, headers: { "Content-Type": "text/event-stream" }, body: collected };
299
+ } else {
300
+ const txt = await upRes.text();
301
+ let parsed;
302
+ try { parsed = JSON.parse(txt); } catch { parsed = txt; }
303
+ result = { status: upRes.status, headers: { "Content-Type": upRes.headers.get("content-type") || "application/json" }, body: typeof parsed === "string" ? parsed : JSON.stringify(parsed) };
304
+ }
305
+ } catch (err) {
306
+ result = { status: 502, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error: errMsg(err) }) };
307
+ }
308
+ try {
309
+ await fetch(`${g.leaderUrl}/v1/groups/relay/result`, {
310
+ method: "POST",
311
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}` },
312
+ body: JSON.stringify({ name: g.name, group: g.name, reqId, result }),
313
+ signal: AbortSignal.timeout(5000),
314
+ });
315
+ } catch {}
316
+ }
317
+ } catch {}
318
+ }
319
+ };
320
+ doHeartbeat().catch(() => {});
321
+ const hbTimer = setInterval(doHeartbeat, 30_000);
322
+ hbTimer.unref();
323
+ const pollTimer = setInterval(doPoll, 1000);
324
+ pollTimer.unref();
325
+ console.log(`broadband relay: heartbeat 30s + poll 1s for ${broadbandGroups().length} group(s)`);
326
+ }
327
+
328
+ const autoUpdateMs = autoUpdateIntervalMs();
329
+ function emitAutoUpdate(type, data = {}) {
330
+ const entry = { ts: Date.now(), type, ...data };
331
+ try { bus?.emit(entry); } catch {}
332
+ try { logs?.appendEvent?.(entry); } catch {}
333
+ const line = `[auto-update] ${type} ${JSON.stringify(data)}`;
334
+ console.log(line);
335
+ }
336
+ if (autoUpdateMs) {
337
+ console.log(`auto-update enabled: checking every ${Math.round(autoUpdateMs / 60000)}m`);
338
+ emitAutoUpdate("auto-update-enabled", { intervalMs: autoUpdateMs, current: VERSION });
339
+ setTimeout(() => {
340
+ emitAutoUpdate("auto-update-check", { current: VERSION });
341
+ checkAndAutoUpdate().catch((err) => {
342
+ console.log(`auto-update check failed: ${errMsg(err)}`);
343
+ emitAutoUpdate("auto-update-failed", { error: errMsg(err) });
344
+ });
345
+ }, 30_000).unref?.();
346
+ const autoUpdateTimer = setInterval(() => {
347
+ emitAutoUpdate("auto-update-check", { current: VERSION });
348
+ checkAndAutoUpdate().catch((err) => {
349
+ console.log(`auto-update check failed: ${errMsg(err)}`);
350
+ emitAutoUpdate("auto-update-failed", { error: errMsg(err) });
351
+ });
352
+ }, autoUpdateMs);
353
+ autoUpdateTimer.unref();
354
+ } else {
355
+ console.log(`auto-update disabled (set MSLXDFF_AUTO_UPDATE=1 to enable hourly)`);
356
+ emitAutoUpdate("auto-update-disabled", { current: VERSION });
357
+ }
358
+
359
+ async function checkAndAutoUpdate() {
360
+ emitAutoUpdate("auto-update-query", { current: VERSION });
361
+ const info = await run(npmCmd(), ["view", "mslxdff", "dist-tags.latest", "--json"]);
362
+ if (info.err) {
363
+ emitAutoUpdate("auto-update-query-failed", { error: info.err.message || String(info.stderr || "").slice(0, 500) });
364
+ throw new Error(info.err.message || String(info.stderr || "").slice(0, 500));
365
+ }
366
+ let latest = "";
367
+ try {
368
+ latest = JSON.parse(String(info.stdout || "").trim());
369
+ if (Array.isArray(latest)) latest = latest[latest.length - 1];
370
+ latest = String(latest || "").replace(/^v/, "").trim();
371
+ } catch {
372
+ const raw = String(info.stdout || "").trim();
373
+ const m = raw.match(/(\d+\.\d+\.\d+[^\s'"]*)/);
374
+ latest = m ? m[1] : raw.split(/\s+/).pop()?.replace(/['"]/g, "") || "";
375
+ }
376
+ latest = latest.replace(/['"]/g, "").trim();
377
+ emitAutoUpdate("auto-update-queried", { current: VERSION, latest, stdout: String(info.stdout || "").trim().slice(0, 200) });
378
+ if (!latest || latest === VERSION) {
379
+ emitAutoUpdate("auto-update-noop", { current: VERSION, latest });
380
+ return;
381
+ }
382
+ const { compareSemver } = await import("./policy.js");
383
+ if (compareSemver(latest, VERSION) <= 0) {
384
+ emitAutoUpdate("auto-update-noop", { current: VERSION, latest, reason: "not newer" });
385
+ return;
386
+ }
387
+ emitAutoUpdate("auto-update-found", { current: VERSION, latest });
388
+ console.log(`auto-update: v${VERSION} -> v${latest}, installing...`);
389
+ emitAutoUpdate("auto-update-installing", { current: VERSION, latest });
390
+ const up = await run(npmCmd(), ["install", "-g", `mslxdff@${latest}`]);
391
+ if (up.err) {
392
+ emitAutoUpdate("auto-update-install-failed", { current: VERSION, latest, error: up.err.message || String(up.stderr || "").slice(0, 500) });
393
+ throw new Error(up.err.message || String(up.stderr || "").slice(0, 500));
394
+ }
395
+ emitAutoUpdate("auto-update-installed", { current: VERSION, latest, stdout: String(up.stdout || "").slice(0, 500) });
396
+ console.log(`auto-update: installed v${latest}, restarting daemon...`);
397
+ emitAutoUpdate("auto-update-restarting", { current: VERSION, latest });
398
+ const { stopDaemon, startDaemon } = await import("../daemon.js");
399
+ try { stopDaemon(); } catch (e) { emitAutoUpdate("auto-update-stop-failed", { error: errMsg(e) }); }
400
+ const { waitForHealth } = await import("./policy.js");
401
+ const newPid = startDaemon([]);
402
+ await waitForHealth(resolvePort(), 8000);
403
+ console.log(`auto-update: restarted as v${latest} (pid ${newPid})`);
404
+ emitAutoUpdate("auto-update-restarted", { current: VERSION, latest, newPid });
405
+ }
406
+ }
407
+
408
+
@@ -0,0 +1,57 @@
1
+ export { DEFAULT_PORT, defaultStateFile, tokenFile, generateToken, flushStateSync, clearStateCache, readState, writeStateImmediate, writeStateDeferred } from "./store.js";
2
+ export { loadToken, refreshToken } from "./schemas/token.js";
3
+ export { getPort, setPort } from "./schemas/port.js";
4
+ export {
5
+ WORKBUDDY_DEFAULT_BASE_URL,
6
+ providerKeyEnv,
7
+ providerBaseUrlEnv,
8
+ providerShareEnv,
9
+ loadProviderKeys,
10
+ loadProviderKey,
11
+ saveProviderKeys,
12
+ saveProviderKey,
13
+ addProviderKey,
14
+ removeProviderKey,
15
+ removeProviderKeys,
16
+ loadProviderShareKeys,
17
+ saveProviderShareKeys,
18
+ loadProviderConfigs,
19
+ loadProviderConfig,
20
+ loadProviderBaseUrl,
21
+ loadProviderAuths,
22
+ saveProviderAuths,
23
+ saveProviderBaseUrl,
24
+ saveProviderConfig,
25
+ normalizeAllowedModel,
26
+ normalizeBaseUrl,
27
+ normalizeAuths,
28
+ normalizeEndpointPath,
29
+ defaultModelsPath,
30
+ defaultChatPath,
31
+ loadProviderModelsPath,
32
+ loadProviderChatPath,
33
+ } from "./schemas/provider.js";
34
+ export {
35
+ loadProviderAllowedModels,
36
+ saveProviderAllowedModels,
37
+ addProviderAllowedModel,
38
+ removeProviderAllowedModel,
39
+ removeProviderAllowedModels,
40
+ loadProviderAllowAnyModels,
41
+ saveProviderAllowAnyModels,
42
+ isModelAllowed,
43
+ } from "./schemas/allowlist.js";
44
+ export {
45
+ loadModelErrors,
46
+ saveModelErrors,
47
+ loadModelLatencies,
48
+ saveModelLatencies,
49
+ loadModelStats,
50
+ saveModelStats,
51
+ loadPreferredModel,
52
+ loadModelPicks,
53
+ saveModelPicks,
54
+ savePreferredModel,
55
+ } from "./schemas/model.js";
56
+ export { loadPeers, savePeers, loadPeerErrors, savePeerErrors, loadPeerStats, savePeerStats } from "./schemas/peer.js";
57
+ export { loadGroups, loadGroupsJoined, saveGroupsJoined, loadBans, saveBans, saveGroups } from "./schemas/group.js";