mslxdff 0.1.155 → 0.1.158

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 (151) hide show
  1. package/bin/mslxdff.js +4 -4
  2. package/docs/ARCHITECTURE.md +426 -0
  3. package/docs/FEATURE_TREE.md +164 -0
  4. package/docs/MOBILE.md +82 -0
  5. package/docs/adr/0001-reasoning-content-injection.md +14 -0
  6. package/docs/adr/0002-models-free-filter.md +12 -0
  7. package/docs/adr/0003-zero-state-no-auth.md +10 -0
  8. package/docs/adr/0004-bearer-token.md +18 -0
  9. package/docs/adr/0005-peer-mesh.md +53 -0
  10. package/docs/adr/0006-broadband-member.md +103 -0
  11. package/docs/adr/0007-multi-provider-prefix.md +25 -0
  12. package/docs/adr/0008-share-keys-to-peers.md +52 -0
  13. package/docs/adr/0009-chat-repl.md +37 -0
  14. package/docs/adr/0010-allowlist.md +36 -0
  15. package/docs/adr/0011-broadband-stream.md +30 -0
  16. package/docs/adr/0012-responses-endpoint-codex-sync.md +48 -0
  17. package/docs/adr/0013-node16-compat.md +41 -0
  18. package/docs/adr/0014-deepseek-provider.md +52 -0
  19. package/docs/adr/0015-upstream-probe-routing.md +50 -0
  20. package/docs/adr/0016-model-capabilities.md +27 -0
  21. package/docs/adr/0017-ai-sdk-upstream-engine.md +59 -0
  22. package/docs/adr/0018-zen-client-identity.md +52 -0
  23. package/docs/adr/0019-share-keys-always-lend.md +59 -0
  24. package/docs/adr/0020-zen-free-lane-agent-shape.md +52 -0
  25. package/docs/adr/0021-usage-report-jsonl.md +47 -0
  26. package/docs/adr/0022-models-capability-merge.md +61 -0
  27. package/docs/adr/0023-key-provider-default-direct.md +58 -0
  28. package/docs/adr/0024-node18-baseline.md +63 -0
  29. package/docs/adr/0025-workbuddy-authdir-follows-state.md +71 -0
  30. package/docs/adr/0026-cline-provider-id-unify.md +67 -0
  31. package/docs/adr/0027-codearts-provider.md +48 -0
  32. package/docs/adr/0028-traework-provider.md +34 -0
  33. package/docs/adr/0029-qoder-native-provider.md +60 -0
  34. package/docs/adr/0030-models-list-scoped-by-picks.md +53 -0
  35. package/docs/adr/0031-qoder-true-streaming.md +50 -0
  36. package/docs/adr/0032-generic-responses-channel.md +72 -0
  37. package/docs/adr/0033-cline-allowlist-auto-sync.md +75 -0
  38. package/docs/adr/0034-request-level-human-readable-observability.md +60 -0
  39. package/docs/adr/0035-sdk-channel-headers-timeout.md +49 -0
  40. package/docs/adr/0036-qoder-per-request-sticky-account.md +82 -0
  41. package/docs/adr/0037-qwenwork-independent-provider.md +82 -0
  42. package/docs/adr/0038-zcode-provider.md +140 -0
  43. package/docs/agents/domain.md +51 -0
  44. package/docs/agents/issue-tracker.md +30 -0
  45. package/docs/agents/triage-labels.md +15 -0
  46. package/docs/cli_help.md +1391 -0
  47. package/docs/cli_help_mini.md +133 -0
  48. package/docs/plans/bench-via-latency-2026-09-01.md +215 -0
  49. package/docs/plugins.md +187 -0
  50. package/package.json +1 -1
  51. package/src/auto.js +254 -254
  52. package/src/bench/cline-bench.js +42 -42
  53. package/src/bench/probe.js +70 -70
  54. package/src/bench/report.js +162 -162
  55. package/src/bench/runner.js +77 -77
  56. package/src/bench/via-probe.js +124 -124
  57. package/src/bench/via-routes.js +87 -87
  58. package/src/bench/workbuddy-bench.js +54 -54
  59. package/src/chat/engine.js +160 -160
  60. package/src/chat/gateway.js +163 -163
  61. package/src/chat/orchestrator.js +234 -234
  62. package/src/chat/prompt.js +70 -70
  63. package/src/chat/repl.js +88 -88
  64. package/src/chat/terminal.js +135 -135
  65. package/src/chat/tools.js +306 -306
  66. package/src/chat-pipeline/index.js +123 -123
  67. package/src/chat-pipeline/policy.js +76 -76
  68. package/src/chat-pipeline/serial-trial.js +210 -210
  69. package/src/cli/commands/group.js +249 -249
  70. package/src/cli/commands/model/list-providers.js +4 -3
  71. package/src/cli/commands/model/list-render.js +7 -9
  72. package/src/cli/commands/model/list-sort.js +52 -0
  73. package/src/cli/commands/model/list-tty.js +76 -0
  74. package/src/cli/commands/model/list.js +5 -62
  75. package/src/cli/commands/model/picks.js +50 -50
  76. package/src/cli/commands/provider/bench-via.js +247 -247
  77. package/src/cli/commands/provider/bench.js +141 -141
  78. package/src/cli/commands/provider/index.js +124 -116
  79. package/src/cli/commands/provider/models.js +139 -128
  80. package/src/cli/commands/provider/qwenwork-login.js +119 -0
  81. package/src/cli/commands/provider/zcode-login.js +77 -0
  82. package/src/cli/commands/provider/zcode-quota.js +55 -0
  83. package/src/cli/commands/sync.js +232 -232
  84. package/src/cli/provider-row.js +2 -2
  85. package/src/cli/status.js +279 -279
  86. package/src/daemon.js +96 -96
  87. package/src/model-capabilities/enrich.js +86 -86
  88. package/src/model-capabilities/index.js +183 -183
  89. package/src/model-capabilities/parse.js +70 -70
  90. package/src/model-trace.js +1 -0
  91. package/src/models.js +225 -225
  92. package/src/providers/classify.js +1 -1
  93. package/src/providers/cline/auth.js +228 -228
  94. package/src/providers/cline/chat.js +307 -307
  95. package/src/providers/cline.js +2 -2
  96. package/src/providers/keyring.js +60 -56
  97. package/src/providers/qoder/chat.js +183 -100
  98. package/src/providers/qoder/index.js +230 -184
  99. package/src/providers/qoder/sse.js +103 -46
  100. package/src/providers/qoder/sticky.js +1 -0
  101. package/src/providers/qoder/stream.js +32 -9
  102. package/src/providers/qwenwork/account-store.js +133 -0
  103. package/src/providers/qwenwork/constants.js +67 -0
  104. package/src/providers/qwenwork/cosy.js +120 -0
  105. package/src/providers/qwenwork/crypto.js +218 -0
  106. package/src/providers/qwenwork/http.js +20 -0
  107. package/src/providers/qwenwork/index.js +327 -0
  108. package/src/providers/qwenwork/payload.js +142 -0
  109. package/src/providers/qwenwork/rsa.js +54 -0
  110. package/src/providers/qwenwork/sse.js +268 -0
  111. package/src/providers/qwenwork/stream.js +130 -0
  112. package/src/providers/qwenwork/upstream.js +120 -0
  113. package/src/providers/qwenwork.js +1 -0
  114. package/src/providers/registry.js +66 -56
  115. package/src/providers/share-keys.js +2 -2
  116. package/src/providers/workbuddy/chat.js +248 -248
  117. package/src/providers/workbuddy/reshape.js +152 -152
  118. package/src/providers/workbuddy.js +2 -2
  119. package/src/providers/zcode/account-store.js +129 -0
  120. package/src/providers/zcode/auth.js +28 -0
  121. package/src/providers/zcode/chat.js +171 -0
  122. package/src/providers/zcode/const.js +54 -0
  123. package/src/providers/zcode/headers.js +55 -0
  124. package/src/providers/zcode/index.js +124 -0
  125. package/src/providers/zcode/models.js +65 -0
  126. package/src/providers/zcode/oauth.js +120 -0
  127. package/src/providers/zcode/quota.js +176 -0
  128. package/src/providers/zcode/sse.js +179 -0
  129. package/src/reasoning.js +32 -32
  130. package/src/routes/chat/gateway.js +46 -46
  131. package/src/routes/chat/relay-pipeline.js +250 -246
  132. package/src/routes/chat/via-route-handler.js +144 -144
  133. package/src/routes/hedge.js +255 -255
  134. package/src/routes/models-route.js +167 -167
  135. package/src/routes/peers.js +273 -273
  136. package/src/routes/stream.js +438 -399
  137. package/src/runtime/bootstrap.js +45 -45
  138. package/src/runtime/provider-gate.js +33 -30
  139. package/src/runtime/providers-setup.js +165 -165
  140. package/src/server.js +64 -64
  141. package/src/state/schemas/allowlist.js +92 -92
  142. package/src/sync-opencode.js +280 -280
  143. package/src/transport/index.js +244 -244
  144. package/src/transport/pool.js +56 -56
  145. package/src/transport/retry.js +24 -24
  146. package/src/transport/sse.js +93 -93
  147. package/src/upstream-probe/display.js +52 -52
  148. package/src/upstream-probe/probe.js +49 -49
  149. package/src/upstream-probe/rotate.js +110 -110
  150. package/src/upstream-probe/start.js +45 -45
  151. package/src/upstream.js +289 -289
@@ -1,124 +1,124 @@
1
- import { performance } from "node:perf_hooks";
2
- import { analyzePolicy } from "./policy.js";
3
- import { createEngine } from "./engine.js";
4
- import { runHook } from "../plugins.js";
5
- import { isFreeModel } from "../models.js";
6
- import { clientIp, summarizePrompt } from "../routes/helpers.js";
7
- import { formatTimeline } from "../timeline.js";
8
- import { summarizeRequest, shouldTraceModel } from "../model-trace.js";
9
-
10
- /**
11
- * ChatPipeline 深模块门面 — 对外 execute(req) 单一 inlet
12
- * 内部组合 Policy→Engine:解析 header/model → 产 order → 委托 engine 执行
13
- * gateway 仅薄适配:readBody + request:received hook + 调 execute
14
- */
15
- export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, token, plugins, maxHops } = {}) {
16
- const engine = createEngine();
17
-
18
- async function execute({ req, res } = {}) {
19
- const startedAt = Date.now();
20
- const reqId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
21
- const perf0 = performance.now();
22
- const stages = [];
23
- const mark = (name) => stages.push([name, Math.round(performance.now() - perf0)]);
24
-
25
- const policy = analyzePolicy({ headers: req?.headers || {}, body: req?.body || {} });
26
- const { requested, useAuto, autoProvider, lockModel, hops, shareKeys, workbuddyUid, aliasInfo } = policy;
27
- mark("parsed");
28
- if (aliasInfo) { try { res?.setHeader?.("x-mslxdff-alias", aliasInfo); } catch {} }
29
- // mslxdff/ 前缀或 alias 命中时,把 body.model 改写为还原后的模型(与原 gateway 语义一致)
30
- const timeline = { direct: [], peers: [], retries: 0, result: null };
31
- if (aliasInfo && req?.body && req.body.model !== requested) {
32
- req.body = { ...req.body, model: requested };
33
- }
34
-
35
- // 事件/日志帮手先于 order 推导声明:evt 在 auto-scope 分支即被使用,
36
- // 声明置后会让 useAuto && autoProvider 的请求在 TDZ 上崩(线上 -chat 回退路径)
37
- const logCall = (model, status) => logs?.appendCall({ reqId, model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(req?.body?.stream), stages });
38
- const logError = (model, status, message) => logs?.appendError({ reqId, model, auto: useAuto, status, message, stages });
39
- const evt = (type, data) => {
40
- const entry = { ts: Date.now(), reqId, type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt, stages: [...stages] };
41
- const traceModel = entry.model || requested;
42
- if (shouldTraceModel(type)) {
43
- // Note: 模型日志只投影安全字段,不能把含 prompt 的 entry 原样下传 — 见 .agents/notes/implemented/feature/2026-09-25-model-trace-log.md
44
- const safeTraceData = { ...entry };
45
- if (safeTraceData.prompt !== undefined) delete safeTraceData.prompt;
46
- logs?.appendModelTrace?.(traceModel, { type, reqId, model: traceModel, data: safeTraceData, request: type === "request" ? summarizeRequest(req?.body) : null, totalMs: entry.durationMs });
47
- }
48
- if (type === "peer-forward") timeline.peers.push({ peer: entry.peer, ok: entry.ok === true, status: entry.status, latencyMs: entry.latencyMs, message: entry.message || entry.error || "" });
49
- if (type === "upstream-done" || type === "upstream-error") timeline.direct.push({ status: entry.status, reason: entry.message || entry.error || "" });
50
- if (type === "peer-error") {
51
- const known = timeline.peers.find((p) => p.peer === entry.peer);
52
- if (known) { known.ok = false; known.status = entry.status ?? known.status; known.message = entry.message || entry.error || known.message; }
53
- else timeline.peers.push({ peer: entry.peer, ok: false, status: entry.status, message: entry.message || entry.error || "" });
54
- }
55
- if (type === "empty-turn-retry") timeline.retries += 1;
56
- if (type === "result" || (type === "client-response" && !timeline.result)) {
57
- timeline.result = { status: entry.status, detail: entry.detail || null };
58
- try { logs?.appendTimeline?.(formatTimeline({ reqId, model: entry.model || requested, ...timeline, totalMs: Date.now() - startedAt })); } catch {}
59
- }
60
- if (bus) bus.emit(entry);
61
- logs?.appendEvent?.(entry);
62
- };
63
- const done = (info) => {
64
- if (!plugins?.length) return;
65
- runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(req?.body?.stream), durationMs: Date.now() - startedAt, ...info }).catch(() => {});
66
- };
67
-
68
- // order 推导 + plugin model:select 可改
69
- // 语义:指定模型 = 死锁单模型(本机→组员同款,挂了就报挂,不兜其他 picks);只有 auto 才轮 picks
70
- // x-mslxdff-auto-provider 头可把 auto 候选限定到单供应商(opencode=裸 id 免费池),-chat 默认带
71
- let order;
72
- if (lockModel) order = [requested];
73
- else if (useAuto) {
74
- let cands = auto ? await auto.candidates() : [""];
75
- if (autoProvider) {
76
- const before = cands.length;
77
- cands = cands.filter((m) => (autoProvider === "opencode"
78
- ? !String(m).includes("/") && isFreeModel(m)
79
- : String(m).startsWith(`${autoProvider}/`)));
80
- evt("auto-scope", { reqId, provider: autoProvider, before, after: cands.length });
81
- }
82
- order = cands;
83
- } else {
84
- if (auto && requested) { try { await auto.candidatesFor(requested); } catch {} }
85
- order = [requested];
86
- }
87
- if (!order.length) order = [""];
88
- const canFallback = order.length > 1;
89
- const canForwardPeers = Boolean(peers) && hops < (maxHops ?? 3);
90
- mark("ordered");
91
-
92
- evt("request", { reqId, hops, ip: clientIp(req), stream: Boolean(req?.body?.stream), prompt: summarizePrompt(req?.body), rawModel: policy.rawModel, requested, lockModel: lockModel || null });
93
- if (aliasInfo) evt("alias", { reqId, alias: aliasInfo, rawModel: policy.rawModel, requested });
94
- if (Object.keys(shareKeys).length) evt("share-keys", { reqId, providers: Object.keys(shareKeys) });
95
- evt("ordered", { reqId, order, canFallback, canForwardPeers, useAuto, statuses: auto?.statuses?.() ?? null });
96
-
97
- if (plugins?.length && !lockModel) {
98
- const sel = await runHook(plugins, "model:select", { reqId, requested, useAuto, order: [...order], hops, stream: Boolean(req?.body?.stream) });
99
- if (sel.changed && Array.isArray(sel.value) && sel.value.length) {
100
- order = sel.value.filter(Boolean);
101
- if (!order.length) order = [requested];
102
- evt("plugin-hook", { reqId, hook: "model:select", applied: true, order: [...order] });
103
- }
104
- for (const e of sel.errors) evt("plugin-hook-error", { reqId, hook: "model:select", plugin: e.plugin, error: e.error });
105
- }
106
-
107
- // 客户端会话标识(opencode 插件 chat.headers 注入)→ 透传上游做粘性路由/缓存亲和;
108
- // 无头时由 upstream.js 按对话首两条消息哈希兜底(不再每请求随机)
109
- const clientSession = String(req?.headers?.["x-session-affinity"] || req?.headers?.["x-session-id"] || "").trim() || null;
110
- const handlerCtx = { reqId, model: null, body: req?.body, hops, peers, plugins, evt, logError, logCall, logs, workbuddyUid, sessionId: clientSession };
111
- if (clientSession) evt("client-session", { reqId, sessionId: clientSession.slice(0, 24) });
112
-
113
- await engine.run({
114
- reqId, startedAt, req, res, body: req?.body, policy,
115
- useAuto, lockModel, requested, hops,
116
- canFallback, canForwardPeers,
117
- perf0, stages, mark, evt, logCall, logError, done, handlerCtx,
118
- auto, upstream, peers, groups, bus, token, plugins, logs,
119
- order,
120
- });
121
- }
122
-
123
- return { execute, _policy: analyzePolicy, _engine: engine };
1
+ import { performance } from "node:perf_hooks";
2
+ import { analyzePolicy } from "./policy.js";
3
+ import { createEngine } from "./engine.js";
4
+ import { runHook } from "../plugins.js";
5
+ import { isFreeModel } from "../models.js";
6
+ import { clientIp, summarizePrompt } from "../routes/helpers.js";
7
+ import { formatTimeline } from "../timeline.js";
8
+ import { summarizeRequest, shouldTraceModel } from "../model-trace.js";
9
+
10
+ /**
11
+ * ChatPipeline 深模块门面 — 对外 execute(req) 单一 inlet
12
+ * 内部组合 Policy→Engine:解析 header/model → 产 order → 委托 engine 执行
13
+ * gateway 仅薄适配:readBody + request:received hook + 调 execute
14
+ */
15
+ export function createChatPipeline({ upstream, auto, logs, peers, groups, bus, token, plugins, maxHops } = {}) {
16
+ const engine = createEngine();
17
+
18
+ async function execute({ req, res } = {}) {
19
+ const startedAt = Date.now();
20
+ const reqId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
21
+ const perf0 = performance.now();
22
+ const stages = [];
23
+ const mark = (name) => stages.push([name, Math.round(performance.now() - perf0)]);
24
+
25
+ const policy = analyzePolicy({ headers: req?.headers || {}, body: req?.body || {} });
26
+ const { requested, useAuto, autoProvider, lockModel, hops, shareKeys, workbuddyUid, aliasInfo } = policy;
27
+ mark("parsed");
28
+ if (aliasInfo) { try { res?.setHeader?.("x-mslxdff-alias", aliasInfo); } catch {} }
29
+ // mslxdff/ 前缀或 alias 命中时,把 body.model 改写为还原后的模型(与原 gateway 语义一致)
30
+ const timeline = { direct: [], peers: [], retries: 0, result: null };
31
+ if (aliasInfo && req?.body && req.body.model !== requested) {
32
+ req.body = { ...req.body, model: requested };
33
+ }
34
+
35
+ // 事件/日志帮手先于 order 推导声明:evt 在 auto-scope 分支即被使用,
36
+ // 声明置后会让 useAuto && autoProvider 的请求在 TDZ 上崩(线上 -chat 回退路径)
37
+ const logCall = (model, status) => logs?.appendCall({ reqId, model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(req?.body?.stream), stages });
38
+ const logError = (model, status, message) => logs?.appendError({ reqId, model, auto: useAuto, status, message, stages });
39
+ const evt = (type, data) => {
40
+ const entry = { ts: Date.now(), reqId, type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt, stages: [...stages] };
41
+ const traceModel = entry.model || requested;
42
+ if (shouldTraceModel(type)) {
43
+ // Note: 模型日志只投影安全字段,不能把含 prompt 的 entry 原样下传 — 见 .agents/notes/implemented/feature/2026-09-25-model-trace-log.md
44
+ const safeTraceData = { ...entry };
45
+ if (safeTraceData.prompt !== undefined) delete safeTraceData.prompt;
46
+ logs?.appendModelTrace?.(traceModel, { type, reqId, model: traceModel, data: safeTraceData, request: type === "request" ? summarizeRequest(req?.body) : null, totalMs: entry.durationMs });
47
+ }
48
+ if (type === "peer-forward") timeline.peers.push({ peer: entry.peer, ok: entry.ok === true, status: entry.status, latencyMs: entry.latencyMs, message: entry.message || entry.error || "" });
49
+ if (type === "upstream-done" || type === "upstream-error") timeline.direct.push({ status: entry.status, reason: entry.message || entry.error || "" });
50
+ if (type === "peer-error") {
51
+ const known = timeline.peers.find((p) => p.peer === entry.peer);
52
+ if (known) { known.ok = false; known.status = entry.status ?? known.status; known.message = entry.message || entry.error || known.message; }
53
+ else timeline.peers.push({ peer: entry.peer, ok: false, status: entry.status, message: entry.message || entry.error || "" });
54
+ }
55
+ if (type === "empty-turn-retry") timeline.retries += 1;
56
+ if (type === "result" || (type === "client-response" && !timeline.result)) {
57
+ timeline.result = { status: entry.status, detail: entry.detail || null };
58
+ try { logs?.appendTimeline?.(formatTimeline({ reqId, model: entry.model || requested, ...timeline, totalMs: Date.now() - startedAt })); } catch {}
59
+ }
60
+ if (bus) bus.emit(entry);
61
+ logs?.appendEvent?.(entry);
62
+ };
63
+ const done = (info) => {
64
+ if (!plugins?.length) return;
65
+ runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(req?.body?.stream), durationMs: Date.now() - startedAt, ...info }).catch(() => {});
66
+ };
67
+
68
+ // order 推导 + plugin model:select 可改
69
+ // 语义:指定模型 = 死锁单模型(本机→组员同款,挂了就报挂,不兜其他 picks);只有 auto 才轮 picks
70
+ // x-mslxdff-auto-provider 头可把 auto 候选限定到单供应商(opencode=裸 id 免费池),-chat 默认带
71
+ let order;
72
+ if (lockModel) order = [requested];
73
+ else if (useAuto) {
74
+ let cands = auto ? await auto.candidates() : [""];
75
+ if (autoProvider) {
76
+ const before = cands.length;
77
+ cands = cands.filter((m) => (autoProvider === "opencode"
78
+ ? !String(m).includes("/") && isFreeModel(m)
79
+ : String(m).startsWith(`${autoProvider}/`)));
80
+ evt("auto-scope", { reqId, provider: autoProvider, before, after: cands.length });
81
+ }
82
+ order = cands;
83
+ } else {
84
+ if (auto && requested) { try { await auto.candidatesFor(requested); } catch {} }
85
+ order = [requested];
86
+ }
87
+ if (!order.length) order = [""];
88
+ const canFallback = order.length > 1;
89
+ const canForwardPeers = Boolean(peers) && hops < (maxHops ?? 3);
90
+ mark("ordered");
91
+
92
+ evt("request", { reqId, hops, ip: clientIp(req), stream: Boolean(req?.body?.stream), prompt: summarizePrompt(req?.body), rawModel: policy.rawModel, requested, lockModel: lockModel || null });
93
+ if (aliasInfo) evt("alias", { reqId, alias: aliasInfo, rawModel: policy.rawModel, requested });
94
+ if (Object.keys(shareKeys).length) evt("share-keys", { reqId, providers: Object.keys(shareKeys) });
95
+ evt("ordered", { reqId, order, canFallback, canForwardPeers, useAuto, statuses: auto?.statuses?.() ?? null });
96
+
97
+ if (plugins?.length && !lockModel) {
98
+ const sel = await runHook(plugins, "model:select", { reqId, requested, useAuto, order: [...order], hops, stream: Boolean(req?.body?.stream) });
99
+ if (sel.changed && Array.isArray(sel.value) && sel.value.length) {
100
+ order = sel.value.filter(Boolean);
101
+ if (!order.length) order = [requested];
102
+ evt("plugin-hook", { reqId, hook: "model:select", applied: true, order: [...order] });
103
+ }
104
+ for (const e of sel.errors) evt("plugin-hook-error", { reqId, hook: "model:select", plugin: e.plugin, error: e.error });
105
+ }
106
+
107
+ // 客户端会话标识(opencode 插件 chat.headers 注入)→ 透传上游做粘性路由/缓存亲和;
108
+ // 无头时由 upstream.js 按对话首两条消息哈希兜底(不再每请求随机)
109
+ const clientSession = String(req?.headers?.["x-session-affinity"] || req?.headers?.["x-session-id"] || "").trim() || null;
110
+ const handlerCtx = { reqId, model: null, body: req?.body, hops, peers, plugins, evt, logError, logCall, logs, workbuddyUid, sessionId: clientSession };
111
+ if (clientSession) evt("client-session", { reqId, sessionId: clientSession.slice(0, 24) });
112
+
113
+ await engine.run({
114
+ reqId, startedAt, req, res, body: req?.body, policy,
115
+ useAuto, lockModel, requested, hops,
116
+ canFallback, canForwardPeers,
117
+ perf0, stages, mark, evt, logCall, logError, done, handlerCtx,
118
+ auto, upstream, peers, groups, bus, token, plugins, logs,
119
+ order,
120
+ });
121
+ }
122
+
123
+ return { execute, _policy: analyzePolicy, _engine: engine };
124
124
  }
@@ -1,76 +1,76 @@
1
- import { normalizeModel } from "../reasoning.js";
2
- import { isAutoModel } from "../auto.js";
3
- import { parseHops } from "../routes/helpers.js";
4
- import { parseShareKeysHeader, SHARE_KEYS_HEADER } from "../providers/share-keys.js";
5
- import { normalizeFullId, getModelAlias } from "../providers/model-id.js";
6
-
7
- /**
8
- * PolicyStage 纯函数 — 解析 model 别名 / 白名单前置 / header 透传
9
- * 零网络副作用,输入 headers+body,输出 PolicyResult
10
- */
11
- export function analyzePolicy({ headers = {}, body = {} } = {}) {
12
- const hops = parseHops(headers["x-mslxdff-hops"] || headers["X-Mslxdff-Hops"] || "");
13
- const shareKeys = parseShareKeysHeader(headers[SHARE_KEYS_HEADER] || headers["x-mslxdff-share-keys"] || "");
14
- const workbuddyUid = (headers["x-mslxdff-workbuddy-uid"] || headers["x-workbuddy-uid"] || "").toString().trim();
15
- const lockModel = (headers["x-mslxdff-model-lock"] || headers["X-Mslxdff-Model-Lock"] || "").toString();
16
- // auto 范围限定:x-mslxdff-auto-provider: opencode → auto 候选只留该供应商(opencode=裸 id 免费池)
17
- const autoProvider = (headers["x-mslxdff-auto-provider"] || headers["X-Mslxdff-Auto-Provider"] || "").toString().trim().toLowerCase() || null;
18
- const rawModel = body.model || "";
19
-
20
- let normalizedRequested = normalizeModel(lockModel || rawModel || "");
21
- const aliasResolved = getModelAlias(normalizedRequested);
22
- if (aliasResolved) {
23
- normalizedRequested = aliasResolved;
24
- }
25
- let requested = normalizedRequested;
26
- let aliasInfo = null;
27
-
28
- if (requested.startsWith("mslxdff/")) {
29
- const rawPart = requested.slice("mslxdff/".length);
30
- aliasInfo = `${requested} -> ${rawPart} (mslxdff provider stripped)`;
31
- requested = rawPart;
32
- const alias2 = getModelAlias(requested);
33
- if (alias2) {
34
- aliasInfo = `${rawModel} -> ${alias2} (mslxdff + alias)`;
35
- requested = alias2;
36
- }
37
- } else if (aliasResolved) {
38
- // 非 mslxdff 的 dash 形态已在首轮 aliasResolved 处理
39
- aliasInfo = aliasResolved !== (lockModel || rawModel) ? `${rawModel} -> ${aliasResolved} (alias)` : null;
40
- if (!aliasInfo) aliasInfo = null;
41
- }
42
-
43
- // workbuddy <uid>:model 形式的 uid 钉死在 normalizeFullId 侧处理,这里透传原始 requested
44
- // 若 requested 含 workbuddy/ 前缀且含 :,则尝试提取 uid
45
- let extractedUid = workbuddyUid;
46
- if (!extractedUid && requested.startsWith("workbuddy/") && requested.includes(":")) {
47
- const after = requested.slice("workbuddy/".length);
48
- const uidPart = after.split(":")[0];
49
- if (uidPart) extractedUid = uidPart;
50
- }
51
-
52
- const useAuto = isAutoModel(requested);
53
-
54
- // 对 workbuddy 前缀的 model,做 normalizeFullId 归一(剥 uid 供上游)
55
- // 但保留 requested 为完整带前缀形态,供 serial-trial 做 ViaRoute 判定与组员路由
56
- let normalizedForUpstream = requested;
57
- try {
58
- const norm = normalizeFullId(requested);
59
- if (norm && norm.raw) normalizedForUpstream = norm.raw ? `${norm.provider ? norm.provider + "/" : ""}${norm.raw}` : requested;
60
- } catch {}
61
-
62
- return {
63
- rawModel,
64
- requested,
65
- normalizedRequested,
66
- normalizedForUpstream,
67
- aliasInfo,
68
- useAuto,
69
- autoProvider,
70
- shareKeys,
71
- workbuddyUid: extractedUid,
72
- lockModel,
73
- hops,
74
- bodyModel: body.model || null,
75
- };
76
- }
1
+ import { normalizeModel } from "../reasoning.js";
2
+ import { isAutoModel } from "../auto.js";
3
+ import { parseHops } from "../routes/helpers.js";
4
+ import { parseShareKeysHeader, SHARE_KEYS_HEADER } from "../providers/share-keys.js";
5
+ import { normalizeFullId, getModelAlias } from "../providers/model-id.js";
6
+
7
+ /**
8
+ * PolicyStage 纯函数 — 解析 model 别名 / 白名单前置 / header 透传
9
+ * 零网络副作用,输入 headers+body,输出 PolicyResult
10
+ */
11
+ export function analyzePolicy({ headers = {}, body = {} } = {}) {
12
+ const hops = parseHops(headers["x-mslxdff-hops"] || headers["X-Mslxdff-Hops"] || "");
13
+ const shareKeys = parseShareKeysHeader(headers[SHARE_KEYS_HEADER] || headers["x-mslxdff-share-keys"] || "");
14
+ const workbuddyUid = (headers["x-mslxdff-workbuddy-uid"] || headers["x-workbuddy-uid"] || "").toString().trim();
15
+ const lockModel = (headers["x-mslxdff-model-lock"] || headers["X-Mslxdff-Model-Lock"] || "").toString();
16
+ // auto 范围限定:x-mslxdff-auto-provider: opencode → auto 候选只留该供应商(opencode=裸 id 免费池)
17
+ const autoProvider = (headers["x-mslxdff-auto-provider"] || headers["X-Mslxdff-Auto-Provider"] || "").toString().trim().toLowerCase() || null;
18
+ const rawModel = body.model || "";
19
+
20
+ let normalizedRequested = normalizeModel(lockModel || rawModel || "");
21
+ const aliasResolved = getModelAlias(normalizedRequested);
22
+ if (aliasResolved) {
23
+ normalizedRequested = aliasResolved;
24
+ }
25
+ let requested = normalizedRequested;
26
+ let aliasInfo = null;
27
+
28
+ if (requested.startsWith("mslxdff/")) {
29
+ const rawPart = requested.slice("mslxdff/".length);
30
+ aliasInfo = `${requested} -> ${rawPart} (mslxdff provider stripped)`;
31
+ requested = rawPart;
32
+ const alias2 = getModelAlias(requested);
33
+ if (alias2) {
34
+ aliasInfo = `${rawModel} -> ${alias2} (mslxdff + alias)`;
35
+ requested = alias2;
36
+ }
37
+ } else if (aliasResolved) {
38
+ // 非 mslxdff 的 dash 形态已在首轮 aliasResolved 处理
39
+ aliasInfo = aliasResolved !== (lockModel || rawModel) ? `${rawModel} -> ${aliasResolved} (alias)` : null;
40
+ if (!aliasInfo) aliasInfo = null;
41
+ }
42
+
43
+ // workbuddy <uid>:model 形式的 uid 钉死在 normalizeFullId 侧处理,这里透传原始 requested
44
+ // 若 requested 含 workbuddy/ 前缀且含 :,则尝试提取 uid
45
+ let extractedUid = workbuddyUid;
46
+ if (!extractedUid && requested.startsWith("workbuddy/") && requested.includes(":")) {
47
+ const after = requested.slice("workbuddy/".length);
48
+ const uidPart = after.split(":")[0];
49
+ if (uidPart) extractedUid = uidPart;
50
+ }
51
+
52
+ const useAuto = isAutoModel(requested);
53
+
54
+ // 对 workbuddy 前缀的 model,做 normalizeFullId 归一(剥 uid 供上游)
55
+ // 但保留 requested 为完整带前缀形态,供 serial-trial 做 ViaRoute 判定与组员路由
56
+ let normalizedForUpstream = requested;
57
+ try {
58
+ const norm = normalizeFullId(requested);
59
+ if (norm && norm.raw) normalizedForUpstream = norm.raw ? `${norm.provider ? norm.provider + "/" : ""}${norm.raw}` : requested;
60
+ } catch {}
61
+
62
+ return {
63
+ rawModel,
64
+ requested,
65
+ normalizedRequested,
66
+ normalizedForUpstream,
67
+ aliasInfo,
68
+ useAuto,
69
+ autoProvider,
70
+ shareKeys,
71
+ workbuddyUid: extractedUid,
72
+ lockModel,
73
+ hops,
74
+ bodyModel: body.model || null,
75
+ };
76
+ }