mslxdff 0.1.127 → 0.1.129

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.127",
3
+ "version": "0.1.129",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,7 +7,7 @@ import { runSerialTrial } from "./serial-trial.js";
7
7
  */
8
8
  export function createEngine(deps = {}) {
9
9
  const { raceDeps, serialDeps } = deps;
10
- async function run(_plan, state) {
10
+ async function run(state) {
11
11
  const race = await runAutoRace(state, raceDeps);
12
12
  if (race.done) return;
13
13
  await runSerialTrial({ ...state, order: race.order }, serialDeps);
@@ -1,15 +1,13 @@
1
1
  import { performance } from "node:perf_hooks";
2
2
  import { analyzePolicy } from "./policy.js";
3
- import { planRoute } from "./planner.js";
4
3
  import { createEngine } from "./engine.js";
5
4
  import { runHook } from "../plugins.js";
6
5
  import { isFreeModel } from "../models.js";
7
- import { shouldUseGroupForModel } from "../state/schemas/use-group.js";
8
6
  import { clientIp, summarizePrompt } from "../routes/helpers.js";
9
7
 
10
8
  /**
11
9
  * ChatPipeline 深模块门面 — 对外 execute(req) 单一 inlet
12
- * 内部组合 Policy→Planner→Engine:解析 header/model → 产 order → 委托 engine 执行
10
+ * 内部组合 Policy→Engine:解析 header/model → 产 order → 委托 engine 执行
13
11
  * gateway 仅薄适配:readBody + request:received hook + 调 execute
14
12
  */
15
13
  export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, token, plugins, maxHops } = {}) {
@@ -31,6 +29,20 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
31
29
  req.body = { ...req.body, model: requested };
32
30
  }
33
31
 
32
+ // 事件/日志帮手先于 order 推导声明:evt 在 auto-scope 分支即被使用,
33
+ // 声明置后会让 useAuto && autoProvider 的请求在 TDZ 上崩(线上 -chat 回退路径)
34
+ const logCall = (model, status) => logs?.appendCall({ reqId, model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(req?.body?.stream), stages });
35
+ const logError = (model, status, message) => logs?.appendError({ reqId, model, auto: useAuto, status, message, stages });
36
+ const evt = (type, data) => {
37
+ const entry = { ts: Date.now(), reqId, type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt, stages: [...stages] };
38
+ if (bus) bus.emit(entry);
39
+ logs?.appendEvent?.(entry);
40
+ };
41
+ const done = (info) => {
42
+ if (!plugins?.length) return;
43
+ runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(req?.body?.stream), durationMs: Date.now() - startedAt, ...info }).catch(() => {});
44
+ };
45
+
34
46
  // order 推导 + plugin model:select 可改
35
47
  // 语义:指定模型 = 死锁单模型(本机→组员同款,挂了就报挂,不兜其他 picks);只有 auto 才轮 picks
36
48
  // x-mslxdff-auto-provider 头可把 auto 候选限定到单供应商(opencode=裸 id 免费池),-chat 默认带
@@ -55,18 +67,6 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
55
67
  const canForwardPeers = Boolean(peers) && hops < (maxHops ?? 3);
56
68
  mark("ordered");
57
69
 
58
- const logCall = (model, status) => logs?.appendCall({ reqId, model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(req?.body?.stream), stages });
59
- const logError = (model, status, message) => logs?.appendError({ reqId, model, auto: useAuto, status, message, stages });
60
- const evt = (type, data) => {
61
- const entry = { ts: Date.now(), reqId, type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt, stages: [...stages] };
62
- if (bus) bus.emit(entry);
63
- logs?.appendEvent?.(entry);
64
- };
65
- const done = (info) => {
66
- if (!plugins?.length) return;
67
- runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(req?.body?.stream), durationMs: Date.now() - startedAt, ...info }).catch(() => {});
68
- };
69
-
70
70
  evt("request", { reqId, hops, ip: clientIp(req), stream: Boolean(req?.body?.stream), prompt: summarizePrompt(req?.body), rawModel: policy.rawModel, requested, lockModel: lockModel || null });
71
71
  if (aliasInfo) evt("alias", { reqId, alias: aliasInfo, rawModel: policy.rawModel, requested });
72
72
  if (Object.keys(shareKeys).length) evt("share-keys", { reqId, providers: Object.keys(shareKeys) });
@@ -88,11 +88,7 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
88
88
  const handlerCtx = { reqId, model: null, body: req?.body, hops, peers, plugins, evt, logError, logCall, logs, workbuddyUid, sessionId: clientSession };
89
89
  if (clientSession) evt("client-session", { reqId, sessionId: clientSession.slice(0, 24) });
90
90
 
91
- const plan = planRoute(policy, {
92
- candidates: order,
93
- viaRoute: Boolean(!useAuto && requested.includes("/") && canForwardPeers && shouldUseGroupForModel(requested)) ? { via: true } : null,
94
- });
95
- await engine.run(plan, {
91
+ await engine.run({
96
92
  reqId, startedAt, req, res, body: req?.body, policy,
97
93
  useAuto, lockModel, requested, hops,
98
94
  canFallback, canForwardPeers,
@@ -102,5 +98,5 @@ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, t
102
98
  });
103
99
  }
104
100
 
105
- return { execute, _policy: analyzePolicy, _plan: planRoute, _engine: engine };
101
+ return { execute, _policy: analyzePolicy, _engine: engine };
106
102
  }
@@ -40,7 +40,7 @@ export function analyzePolicy({ headers = {}, body = {} } = {}) {
40
40
  if (!aliasInfo) aliasInfo = null;
41
41
  }
42
42
 
43
- // workbuddy <uid>:model 形式的 uid 钉死在 normalizeFullId 侧处理,这里透传原始 requested 供 planner 二次剥离
43
+ // workbuddy <uid>:model 形式的 uid 钉死在 normalizeFullId 侧处理,这里透传原始 requested
44
44
  // 若 requested 含 workbuddy/ 前缀且含 :,则尝试提取 uid
45
45
  let extractedUid = workbuddyUid;
46
46
  if (!extractedUid && requested.startsWith("workbuddy/") && requested.includes(":")) {
@@ -52,7 +52,7 @@ export function analyzePolicy({ headers = {}, body = {} } = {}) {
52
52
  const useAuto = isAutoModel(requested);
53
53
 
54
54
  // 对 workbuddy 前缀的 model,做 normalizeFullId 归一(剥 uid 供上游)
55
- // 但保留 requested 为完整带前缀形态,供 planner 做 ViaRoute 判定
55
+ // 但保留 requested 为完整带前缀形态,供 serial-trial 做 ViaRoute 判定与组员路由
56
56
  let normalizedForUpstream = requested;
57
57
  try {
58
58
  const norm = normalizeFullId(requested);
@@ -121,7 +121,7 @@ export function createCapabilitiesService({
121
121
  return { ready, get, list, providers, npmIndex: () => new Map(npmIndex) };
122
122
  }
123
123
 
124
- // 模块级单例(与 globalDedup 同模式):HTTP handler 懒加载,测试 _reset 后注入
124
+ // 模块级单例:HTTP handler 懒加载,测试 _reset 后注入
125
125
  let _global = null;
126
126
  export function globalCapabilities() {
127
127
  if (!_global) _global = createCapabilitiesService({ cacheFile: defaultCacheFile() });
@@ -117,10 +117,10 @@ export function createClineProvider({
117
117
  return list;
118
118
  }
119
119
 
120
- const { preheat } = (() => {
121
- try { return modelsSvc; } catch { return { preheat: async () => ({ ok: false }) }; }
120
+ const { preheat, checkFreeUpdates } = (() => {
121
+ try { return modelsSvc; } catch { return { preheat: async () => ({ ok: false }), checkFreeUpdates: async () => ({ ok: false }) }; }
122
122
  })();
123
123
 
124
124
  async function close() { if (agent?.close) try { await agent.close(); } catch {} }
125
- return { id, chat, chatWithKeys, listModels, preheat, close, agent, keyRing: ring, baseUrl: resolvedBase, _authPool: authPool };
125
+ return { id, chat, chatWithKeys, listModels, preheat, checkFreeUpdates, close, agent, keyRing: ring, baseUrl: resolvedBase, _authPool: authPool };
126
126
  }
@@ -71,9 +71,9 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
71
71
  } catch { return fallbackList(); } finally { clearTimeout(timer); }
72
72
  }
73
73
 
74
- // 每次 daemon 启动(server-lifecycle → dispatcher.preheat)自检 free 列表:
74
+ // 每次 daemon 启动自检 free 列表(server-lifecycle 显式调 checkFreeUpdates,不经 dispatcher.preheat):
75
75
  // 对比快照报增删并留痕(daemon.log),顺带把结果填缓存(省一次 listModels 请求)。
76
- // 快照路径由 index.js 注入 logDir 下文件;未注入时仅跳过自检,不影响预热。
76
+ // 快照路径由 index.js 注入 logDir 下文件;未注入时仅跳过自检,不影响拉取。
77
77
  function readSnapshotFree() {
78
78
  try { const j = JSON.parse(readFileSync(snapshotPath, "utf8")); return Array.isArray(j?.free) ? j.free : null; } catch { return null; }
79
79
  }
@@ -104,7 +104,8 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
104
104
  return { added, removed };
105
105
  }
106
106
 
107
- async function preheat() {
107
+ // 启动自检入口:拉 recommended-models → 填缓存 → 对比快照报 free 增删。
108
+ async function checkFreeUpdates() {
108
109
  const url = resolveModelsUrl();
109
110
  const t0 = performance.now();
110
111
  try {
@@ -142,5 +143,10 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
142
143
  }
143
144
  }
144
145
 
145
- return { listModels, preheat };
146
+ // preheat 保留为别名:dispatcher 已不再调 clinebot,手动/测试/未来钩子仍可用,行为与自检一致
147
+ async function preheat() {
148
+ return checkFreeUpdates();
149
+ }
150
+
151
+ return { listModels, preheat, checkFreeUpdates };
146
152
  }
@@ -92,17 +92,18 @@ export function createProviderDispatcher(providers = [], opts = {}) {
92
92
  return out;
93
93
  }
94
94
 
95
+ // 只预热默认供应商(opencode):连接池与模型缓存预热是其主链路收益;
96
+ // 其他供应商按需在首次请求时自拉(10min 缓存)。不再逐家预热,避免 daemon 每次启动
97
+ // 对所有上游各发一次 GET;MSLXDFF_PREHEAT=0 的关闭由唯一被调的 opencode preheat 自行尊重。
98
+ // 见 .agents/notes/implemented/simplification/2026-09-16-preheat-opencode-only.md
95
99
  async function preheat() {
96
- const results = [];
97
- for (const p of providers) {
98
- if (typeof p.preheat !== "function") continue;
99
- try {
100
- results.push(await p.preheat());
101
- } catch {
102
- results.push({ ok: false, error: "preheat failed" });
103
- }
100
+ const p = byId.get(DEFAULT_PROVIDER);
101
+ if (!p || typeof p.preheat !== "function") return { ok: false, skipped: true };
102
+ try {
103
+ return await p.preheat();
104
+ } catch {
105
+ return { ok: false, error: "preheat failed" };
104
106
  }
105
- return results.length ? results[0] : { ok: false, skipped: true };
106
107
  }
107
108
 
108
109
  async function close() {
@@ -123,6 +123,12 @@ export async function startServerLifecycle({ VERSION, token, created, upstream,
123
123
  else if (r.ok) console.log(`[preheat] opencode models ok ${r.status} ${r.ms}ms`);
124
124
  else console.log(`[preheat] opencode models failed ${r.error || r.status || ""} ${r.ms || 0}ms`);
125
125
  }).catch(() => {});
126
+ // clinebot free 目录自检(独立于 opencode 连接预热:dispatcher.preheat 只做默认供应商)
127
+ // 见 .agents/notes/implemented/simplification/2026-09-16-preheat-opencode-only.md
128
+ try {
129
+ const cline = upstream?.byId?.get?.("clinebot") ?? upstream?.byId?.get?.("cline");
130
+ if (cline?.checkFreeUpdates) void cline.checkFreeUpdates().catch(() => {});
131
+ } catch {}
126
132
  }, 100).unref?.();
127
133
 
128
134
  // responses 模型判定改元数据驱动(models.dev provider.npm):就绪后注入,每小时重查使新模型自动识别
@@ -1,83 +0,0 @@
1
- import { createHash } from "node:crypto";
2
-
3
- /**
4
- * 请求去重(防前端双击/重试风暴)
5
- * key = ip | requested | stream | bodyHash(messages+model变体)
6
- * 窗口内重复到达的相同请求直接 429 返回,提示前端去重
7
- * 默认窗口 1000ms,可用 MSLXDFF_DEDUP_WINDOW_MS 覆盖,0 为关闭
8
- */
9
- export function dedupWindowMs() {
10
- const raw = process.env.MSLXDFF_DEDUP_WINDOW_MS;
11
- if (raw != null && String(raw).trim() !== "") {
12
- const n = Number(raw);
13
- if (Number.isFinite(n) && n >= 0) return Math.floor(n);
14
- }
15
- return 1000;
16
- }
17
-
18
- function hashBody(body) {
19
- try {
20
- const m = body?.messages;
21
- const s = JSON.stringify({
22
- model: body?.model || "",
23
- stream: Boolean(body?.stream),
24
- max_tokens: body?.max_tokens ?? body?.maxTokens ?? null,
25
- messages: Array.isArray(m) ? m.map((x) => ({ role: x.role, content: typeof x.content === "string" ? x.content.slice(0, 4000) : JSON.stringify(x.content).slice(0, 4000) })) : [],
26
- // 工具调用等也纳入,避免误判
27
- tools: body?.tools ? JSON.stringify(body.tools).slice(0, 1000) : "",
28
- });
29
- return createHash("sha1").update(s).digest("hex").slice(0, 16);
30
- } catch {
31
- return String(body?.model || "").slice(0, 32);
32
- }
33
- }
34
-
35
- let _global = null;
36
- export function globalDedup() {
37
- if (!_global) _global = createDedup({ windowMs: dedupWindowMs() });
38
- // 若环境变量在运行时被改,同步窗口
39
- const want = dedupWindowMs();
40
- if (_global.windowMs !== want) {
41
- _global.windowMs = want;
42
- }
43
- return _global;
44
- }
45
- export function _resetGlobalDedup() { _global = null; }
46
-
47
- export function createDedup({ windowMs = dedupWindowMs(), now = Date.now } = {}) {
48
- const map = new Map(); // key -> at
49
- let sweepAt = 0;
50
-
51
- function sweep() {
52
- const t = now();
53
- if (t - sweepAt < windowMs) return;
54
- sweepAt = t;
55
- for (const [k, at] of map) {
56
- if (t - at > windowMs) map.delete(k);
57
- }
58
- }
59
-
60
- function keyFor({ ip, requested, body }) {
61
- const h = hashBody(body);
62
- const stream = body?.stream ? "1" : "0";
63
- return `${ip || "-"}|${requested || "-"}|${stream}|${h}`;
64
- }
65
-
66
- function check({ ip, requested, body }) {
67
- if (!windowMs) return { dup: false, key: null };
68
- sweep();
69
- const key = keyFor({ ip, requested, body });
70
- const at = map.get(key);
71
- const t = now();
72
- if (at != null && t - at < windowMs) {
73
- return { dup: true, key, ageMs: t - at };
74
- }
75
- map.set(key, t);
76
- return { dup: false, key };
77
- }
78
-
79
- function _size() { return map.size; }
80
- function _clear() { map.clear(); }
81
-
82
- return { check, keyFor, _size, _clear, windowMs };
83
- }
@@ -1,31 +0,0 @@
1
- import { DEFAULT_AUTO_MODELS, rankModels } from "../auto.js";
2
-
3
- /**
4
- * RoutePlanner 纯决策 — 根据 Policy + auto 状态产 RoutePlan
5
- * 无网络副作用,供单测注入 FakeAuto
6
- */
7
- export function planRoute(policy, autoState = {}) {
8
- const { requested, useAuto, lockModel } = policy;
9
- const { candidates = [], errors = {}, latencies = {}, viaRoute = null } = autoState;
10
-
11
- // 锁模型时直接单点
12
- if (lockModel) {
13
- return { strategy: "direct", order: [requested], concLimit: 1, hedgeDelayMs: 0 };
14
- }
15
- // ViaRoute 单路径(显式锁模型且 via 表命中)
16
- if (viaRoute && !useAuto && requested.includes("/")) {
17
- return { strategy: "via", order: [requested], via: viaRoute, concLimit: 1, hedgeDelayMs: 0 };
18
- }
19
- // Auto 并发择优
20
- if (useAuto && candidates.length > 1) {
21
- const concLimit = Math.min(candidates.length, 5);
22
- return { strategy: "autoRace", order: candidates, concLimit, hedgeDelayMs: 1000 };
23
- }
24
- // 显式模型回退链
25
- if (!useAuto && candidates.length) {
26
- const others = candidates.filter((m) => m !== requested);
27
- const order = requested ? [requested, ...others] : candidates;
28
- return { strategy: "direct", order, concLimit: 1, hedgeDelayMs: 0 };
29
- }
30
- return { strategy: "direct", order: requested ? [requested] : [""], concLimit: 1, hedgeDelayMs: 0 };
31
- }