mslxdff 0.1.43 → 0.1.45

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.
@@ -1,6 +1,7 @@
1
1
  import { performance } from "node:perf_hooks";
2
2
  import { isAutoModel } from "../auto.js";
3
3
  import { errMsg } from "./helpers.js";
4
+ import { runHook } from "../plugins.js";
4
5
 
5
6
  const PEER_TIMEOUT_MS = 30_000;
6
7
  const PEER_STATUS_TIMEOUT_MS = 2_000;
@@ -86,11 +87,19 @@ export async function racePeerCandidates(candidates, ctx) {
86
87
  const total = prepared.length;
87
88
  for (const { peer, target } of prepared) {
88
89
  ctx.evt("peer-request", { peer: peer.url, model: target, hops: ctx.hops + 1 });
90
+ // 插件 hook:peer:beforeForward — 转发给组员前观察
91
+ if (ctx.plugins?.length) {
92
+ runHook(ctx.plugins, "peer:beforeForward", { reqId: ctx.reqId, peer: peer.url, model: target, hops: ctx.hops + 1 }).catch(() => {});
93
+ }
89
94
  const t0 = performance.now();
90
95
  forwardToPeer(peer, ctx.body, target, ctx.hops).then((res) => {
91
96
  const latencyMs = Math.round(performance.now() - t0);
92
97
  const failed = res instanceof Error || res.status >= 400;
93
98
  ctx.evt("peer-forward", { peer: peer.url, model: target, hops: ctx.hops + 1, latencyMs, ok: !failed });
99
+ // 插件 hook:peer:result — 组员响应后观察
100
+ if (ctx.plugins?.length) {
101
+ runHook(ctx.plugins, "peer:result", { reqId: ctx.reqId, peer: peer.url, model: target, ok: !failed, status: failed ? (res instanceof Error ? 502 : res.status) : res.status, latencyMs }).catch(() => {});
102
+ }
94
103
  if (failed) {
95
104
  const status = res instanceof Error ? 502 : res.status;
96
105
  ctx.logError(ctx.model, status, res instanceof Error ? errMsg(res) : `peer ${status}`);
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, signals = true, host }, port = resolvePort()) {
4
+ export function startServer({ router, signals = true, host, onBeforeClose }, port = resolvePort()) {
5
5
  const listenHost = host ?? resolveHost();
6
6
  const server = httpCreateServer((req, res) => {
7
7
  router(req, res).catch((err) => {
@@ -18,13 +18,16 @@ export function startServer({ router, signals = true, host }, port = resolvePort
18
18
  else server.listen(port, resolve);
19
19
  });
20
20
 
21
- const close = () =>
22
- new Promise((resolve) => {
21
+ const close = async () => {
22
+ // 插件 hook:server:stop — 关闭前触发(fire-and-forget,不阻塞关停)
23
+ try { await onBeforeClose?.(); } catch {}
24
+ await new Promise((resolve) => {
23
25
  server.close(resolve);
24
26
  // SSE debug streams keep connections open — force-close so shutdown
25
27
  // never waits on them.
26
28
  server.closeAllConnections?.();
27
29
  });
30
+ };
28
31
 
29
32
  if (signals) {
30
33
  process.on("SIGINT", close);
package/src/state.js CHANGED
@@ -58,6 +58,17 @@ export function saveModelLatencies(latencies, { file = defaultStateFile() } = {}
58
58
  return latencies;
59
59
  }
60
60
 
61
+ export function loadPreferredModel({ file = defaultStateFile() } = {}) {
62
+ const v = readState(file).preferredModel;
63
+ return typeof v === "string" && v.trim() ? v.trim() : null;
64
+ }
65
+
66
+ export function savePreferredModel(id, { file = defaultStateFile() } = {}) {
67
+ const state = readState(file);
68
+ writeState(file, { ...state, preferredModel: String(id || "").trim() });
69
+ return String(id || "").trim();
70
+ }
71
+
61
72
  export function loadPeers({ file = defaultStateFile() } = {}) {
62
73
  const peers = readState(file).peers;
63
74
  return Array.isArray(peers) ? peers : [];
package/src/upstream.js CHANGED
@@ -38,9 +38,19 @@ export function createUpstreamClient({
38
38
  504: { attempts: 1, delayMs: 500 },
39
39
  },
40
40
  fetchImpl,
41
+ hooks,
41
42
  } = {}) {
42
43
  // 使用 undici 的 fetch 与 Agent 配对,避免 global fetch 与 npm undici Agent 不兼容
43
44
  if (!fetchImpl) fetchImpl = UndiciFetch || fetch;
45
+ // hooks: async (name, ctx) => ({ value, changed }) | null — 由 bin 用 runHook 绑定;错误已在上层隔离
46
+ const applyHook = async (name, ctx) => {
47
+ if (!hooks) return null;
48
+ try {
49
+ return await hooks(name, ctx);
50
+ } catch {
51
+ return null;
52
+ }
53
+ };
44
54
  const baseHeaders = {
45
55
  "Content-Type": "application/json",
46
56
  "Authorization": `Bearer ${authToken}`,
@@ -129,7 +139,17 @@ export function createUpstreamClient({
129
139
  connectTimeoutMs
130
140
  );
131
141
  try {
132
- const headers = buildHeaders(body);
142
+ let reqUrl = url;
143
+ let headers = buildHeaders(body);
144
+ // 插件 hook:upstream:headers — 返回 { headers } 可替换请求头
145
+ const hh = await applyHook("upstream:headers", { url, body, headers });
146
+ if (hh?.changed && hh.value?.headers) headers = hh.value.headers;
147
+ // 插件 hook:upstream:before-request — 返回 { url, headers } 可改目标地址/头(上游不限于 opencode)
148
+ const br = await applyHook("upstream:before-request", { url, method: "POST", body, headers });
149
+ if (br?.changed && br.value) {
150
+ if (typeof br.value.url === "string" && br.value.url) reqUrl = br.value.url;
151
+ if (br.value.headers && typeof br.value.headers === "object") headers = br.value.headers;
152
+ }
133
153
  const opts = {
134
154
  method: "POST",
135
155
  headers,
@@ -137,7 +157,7 @@ export function createUpstreamClient({
137
157
  signal: controller.signal,
138
158
  };
139
159
  if (dispatcher) opts.dispatcher = dispatcher;
140
- const res = await fetchImpl(url, opts);
160
+ const res = await fetchImpl(reqUrl, opts);
141
161
  return res;
142
162
  } catch (err) {
143
163
  return err;