mslxdff 0.1.236 → 0.1.238

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 (105) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +264 -264
  4. package/src/autostart.js +62 -15
  5. package/src/bench/cline-bench.js +42 -42
  6. package/src/bench/probe.js +70 -70
  7. package/src/bench/report.js +162 -162
  8. package/src/bench/runner.js +77 -77
  9. package/src/bench/via-probe.js +124 -124
  10. package/src/bench/via-routes.js +87 -87
  11. package/src/bench/workbuddy-bench.js +54 -54
  12. package/src/chat/engine.js +160 -160
  13. package/src/chat/gateway.js +163 -163
  14. package/src/chat/orchestrator.js +234 -234
  15. package/src/chat/prompt.js +70 -70
  16. package/src/chat/repl.js +88 -88
  17. package/src/chat/terminal.js +135 -135
  18. package/src/chat/tools.js +306 -306
  19. package/src/chat-pipeline/index.js +155 -155
  20. package/src/chat-pipeline/policy.js +84 -84
  21. package/src/cli/commands/group.js +266 -266
  22. package/src/cli/commands/model/picks.js +50 -50
  23. package/src/cli/commands/provider/bench-via.js +247 -247
  24. package/src/cli/commands/provider/bench.js +141 -141
  25. package/src/cli/commands/provider/index.js +139 -139
  26. package/src/cli/commands/provider/keys-guard.js +20 -0
  27. package/src/cli/commands/provider/models.js +149 -149
  28. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  29. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  30. package/src/cli/commands/sync.js +310 -310
  31. package/src/cli/status.js +282 -282
  32. package/src/daemon.js +246 -246
  33. package/src/model-capabilities/enrich.js +86 -86
  34. package/src/model-capabilities/index.js +183 -183
  35. package/src/model-capabilities/parse.js +70 -70
  36. package/src/models.js +293 -293
  37. package/src/peers.js +389 -389
  38. package/src/providers/agentrouter.js +238 -238
  39. package/src/providers/base.js +164 -164
  40. package/src/providers/cline/auth.js +228 -228
  41. package/src/providers/cline/chat.js +307 -307
  42. package/src/providers/cline.js +2 -2
  43. package/src/providers/codearts/chat.js +207 -207
  44. package/src/providers/codearts/const.js +12 -5
  45. package/src/providers/codearts/models.js +23 -5
  46. package/src/providers/dispatcher.js +147 -147
  47. package/src/providers/globalqwenwork/cosy.js +123 -123
  48. package/src/providers/globalqwenwork/crypto.js +218 -218
  49. package/src/providers/globalqwenwork/http.js +20 -20
  50. package/src/providers/globalqwenwork/rsa.js +54 -54
  51. package/src/providers/globalqwenwork/sse.js +268 -268
  52. package/src/providers/globalqwenwork/stream.js +130 -130
  53. package/src/providers/globalqwenwork/upstream.js +122 -122
  54. package/src/providers/keyring.js +60 -60
  55. package/src/providers/qoder/chat.js +292 -292
  56. package/src/providers/qoder/index.js +311 -311
  57. package/src/providers/qoder/sse.js +233 -233
  58. package/src/providers/qwenwork/account-store.js +133 -133
  59. package/src/providers/qwenwork/constants.js +67 -67
  60. package/src/providers/qwenwork/cosy.js +120 -120
  61. package/src/providers/qwenwork/crypto.js +218 -218
  62. package/src/providers/qwenwork/http.js +20 -20
  63. package/src/providers/qwenwork/index.js +337 -337
  64. package/src/providers/qwenwork/payload.js +144 -144
  65. package/src/providers/qwenwork/rsa.js +54 -54
  66. package/src/providers/qwenwork/sse.js +268 -268
  67. package/src/providers/qwenwork/stream.js +130 -130
  68. package/src/providers/qwenwork/upstream.js +120 -120
  69. package/src/providers/qwenwork.js +1 -1
  70. package/src/providers/raccoon/const.js +219 -219
  71. package/src/providers/raccoon/credits.js +285 -285
  72. package/src/providers/registry.js +89 -89
  73. package/src/providers/workbuddy/chat.js +298 -298
  74. package/src/providers/workbuddy/reshape.js +152 -152
  75. package/src/providers/workbuddy.js +2 -2
  76. package/src/reasoning.js +32 -32
  77. package/src/routes/chat/broadband-handler.js +80 -80
  78. package/src/routes/chat/gateway.js +46 -46
  79. package/src/routes/chat/relay-pipeline.js +282 -282
  80. package/src/routes/chat/via-route-handler.js +158 -158
  81. package/src/routes/hedge.js +260 -260
  82. package/src/routes/models-route.js +167 -167
  83. package/src/routes/peers.js +373 -373
  84. package/src/routes/stream.js +364 -364
  85. package/src/runtime/bootstrap.js +75 -75
  86. package/src/runtime/keep-warm.js +135 -135
  87. package/src/runtime/provider-gate.js +75 -75
  88. package/src/runtime/providers-setup.js +187 -187
  89. package/src/runtime/raccoon-checkin.js +175 -175
  90. package/src/runtime/server-lifecycle.js +34 -10
  91. package/src/server.js +85 -85
  92. package/src/state/schemas/allowlist.js +92 -92
  93. package/src/sync-opencode.js +280 -280
  94. package/src/transport/index.js +244 -244
  95. package/src/transport/pool.js +56 -56
  96. package/src/transport/retry.js +24 -24
  97. package/src/transport/sse.js +93 -93
  98. package/src/upstream-probe/display.js +52 -52
  99. package/src/upstream-probe/probe.js +49 -49
  100. package/src/upstream-probe/rotate.js +110 -110
  101. package/src/upstream-probe/start.js +45 -45
  102. package/src/upstream.js +355 -355
  103. package/docs/cli_help_mini.md +0 -149
  104. package/src/providers/AGENTS.md +0 -49
  105. package/src/routes/AGENTS.md +0 -37
@@ -1,310 +1,310 @@
1
- import { join } from "node:path";
2
- import { loadToken, getPort, savePreferredModel, loadPreferredModel, loadModelPicks } from "../../state.js";
3
- import { getPreferredModel as getPref } from "../../auto.js";
4
- import { normalizeModel } from "../../reasoning.js";
5
- import { syncToWorkbuddy, workbuddyModelsPath } from "../../sync-workbuddy.js";
6
- import { syncToOpencode, opencodeConfigPath } from "../../sync-opencode.js";
7
- import { syncToCodex, codexConfigPath } from "../../sync-codex.js";
8
- import { syncToClaude, claudeSettingsPath } from "../../sync-claude.js";
9
- import { createModelsService } from "../../models.js";
10
- import { createUpstreamClient } from "../../upstream.js";
11
- import { logDir } from "../../logs.js";
12
-
13
- // 剪枝口径:picks 非空时,未在 picks 的旧模型视为失效,下次 setto 从第三方配置里摘除;
14
- // picks 为空(=不筛选)时返回 null,sync 侧一个不动。
15
- function pruneKeep() {
16
- const picks = loadModelPicks();
17
- return picks.length ? picks : null;
18
- }
19
-
20
- export async function handleSetto(args) {
21
- if (!(args.includes("-setto") || args.includes("--setto"))) return false;
22
- const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
23
- const target = args[idx + 1];
24
- if (!["workbuddy", "opencode", "chatgpt", "codex", "claude", "claude-desktop", "claudedesktop"].includes(target)) {
25
- console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId|--all] | mslxdff -setto chatgpt [modelId] | mslxdff -setto claude [modelId] [--behaves-as <id>] | mslxdff -setto claude-desktop [modelId ...] [--max N] [--max-effort <lvl>] [--1m <off|auto|all|列表>] [--no-alias] [--check] [--official] (claude: 不带 modelId = 写入 modelPicks 勾选集全集,--all 与之同义 / claude-desktop: 写 Claude Desktop 3P profile + 角色槽 alias,--1m 缺省 auto=有据才打 1M 标,详见 ADR-0056)");
26
- process.exit(1);
27
- }
28
- if (target === "claude-desktop" || target === "claudedesktop") {
29
- // Claude Desktop on 3P(ADR-0048):写 profile + 角色槽 alias。sync.js 只做转发——
30
- // 本文件已近 20KB 体积门,实现放 src/cli/commands/claude-desktop.js。
31
- const { handleClaudeDesktopSetto } = await import("./claude-desktop.js");
32
- return handleClaudeDesktopSetto(args, idx);
33
- }
34
- if (target === "chatgpt" || target === "codex") {
35
- // Codex/ChatGPT 三端共用 ~/.codex/config.toml:写 model + model_provider + [model_providers.mslxdff]
36
- const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
37
- let id;
38
- if (raw) {
39
- if (raw === "auto" || !raw) {
40
- console.error("modelId 不能为 auto 或空");
41
- process.exit(1);
42
- }
43
- const norm = normalizeModel(raw);
44
- if (!norm) {
45
- console.error("modelId 不能为空");
46
- process.exit(1);
47
- }
48
- savePreferredModel(norm);
49
- console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
50
- id = norm;
51
- } else {
52
- id = loadPreferredModel() || getPref();
53
- if (!id) {
54
- console.error("no preferred model set; use: mslxdff -setto chatgpt <modelId>");
55
- process.exit(1);
56
- }
57
- }
58
- try {
59
- const persisted = getPort();
60
- const envPort = Number(process.env.MSLXDFF_PORT);
61
- const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
62
- const file = codexConfigPath();
63
- const r = syncToCodex({ id, port, file });
64
- console.log(`synced to codex: ${r.action} "${r.id}" @ ${r.file}`);
65
- console.log(` url: http://127.0.0.1:${port}/v1/responses (Responses API)`);
66
- console.log(` 鉴权走 mslxdff -showtoken 命令(token 不落盘),直接 codex exec "hi" 验证`);
67
- } catch (err) {
68
- console.error(`failed to sync to codex: ${String(err?.message || err)}`);
69
- process.exit(1);
70
- }
71
- process.exit(0);
72
- }
73
- if (target === "claude") {
74
- // Claude Code 用户设置 ~/.claude/settings.json:走本机 /v1/messages 外壳(ADR-0047)
75
- // 参数校验一律先于任何写盘(含 savePreferredModel):校验不过不得碰 state.json 与 settings.json
76
- // --behaves-as <已知 claude-* id> 覆盖能力锚;`--behaves-as ""` 显式关掉(关掉后本机 Claude Code 可能对未知 id 拒跑)
77
- const baIdx = args.findIndex((x) => x === "--behaves-as");
78
- if (baIdx >= 0 && args[baIdx + 1] === undefined) {
79
- console.error("--behaves-as 需要取值:给一个已知 claude-* id,或显式传空串关闭(--behaves-as \"\")");
80
- process.exit(1);
81
- }
82
- const behavesAs = baIdx >= 0 ? String(args[baIdx + 1]) : undefined;
83
- const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") && args[idx + 2] !== "all" ? String(args[idx + 2]).trim() : null;
84
- const picksAll = loadModelPicks();
85
- let id;
86
- let batch = false;
87
- if (raw) {
88
- if (raw === "auto" || !normalizeModel(raw)) {
89
- console.error("modelId 不能为 auto 或空");
90
- process.exit(1);
91
- }
92
- const norm = normalizeModel(raw);
93
- if (!norm) {
94
- console.error("modelId 不能为空");
95
- process.exit(1);
96
- }
97
- if (picksAll.length && !picksAll.includes(norm)) {
98
- console.log(` ⚠ "${norm}" 不在勾选集 modelPicks 内 —— 可能是拼错或已失效;仍按你显式指定的写入`);
99
- }
100
- savePreferredModel(norm);
101
- console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
102
- id = norm;
103
- } else {
104
- // 不带参数(含 --all / 裸 all)= 写入全部勾选模型:modelPicker.options = modelPicks 全集,
105
- // 顶层 model 也只在勾选集内取。**不再兜底 preferredModel** —— 它可能是一个已从池子消失的 id
106
- // (真实踩坑:preferredModel=mimo-v2.5-free 被写进 Claude Code 后,每次请求只会得到 502)。
107
- batch = true;
108
- if (!picksAll.length) {
109
- console.error("modelPicks 勾选集为空 → 无可写入的模型;先跑 mslxdff -model pick 勾选,或直接 mslxdff -setto claude <modelId>");
110
- process.exit(1);
111
- }
112
- const pref = normalizeModel(loadPreferredModel() || getPref() || "");
113
- if (pref && picksAll.includes(pref)) {
114
- id = pref;
115
- } else {
116
- if (pref) console.log(` ⚠ 网关首选模型 "${pref}" 不在勾选集内(可能已失效)→ claude 默认改用勾选集首个 "${picksAll[0]}"`);
117
- id = normalizeModel(picksAll[0]);
118
- }
119
- }
120
- try {
121
- const { token } = await loadToken();
122
- const persisted = getPort();
123
- const envPort = Number(process.env.MSLXDFF_PORT);
124
- const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
125
- // options 由本命令全权管理:默认/`--all` 用勾选集整体替换,显式单模型则只留一条。
126
- const picks = batch ? picksAll : null;
127
- const file = claudeSettingsPath();
128
- const r = syncToClaude({ id, token, port, picks, file, behavesAs });
129
- console.log(`synced to claude: ${r.action} "${r.id}" @ ${r.file}${r.changed ? "" : "(已是目标状态,未改字节)"}`);
130
- if (r.backup) { console.log(` backup: ${r.backup}`); console.log(` 还原: Copy-Item "${r.backup}" "${r.file}"`); }
131
- else console.log(` backup: 未生成(本次无变化,或同目录已有更早的 settings.pre-mslxdff.json —— 那份是首次接管前的原文)`);
132
- if (r.tmpLeftover) console.error(` ⚠ 临时文件清理失败:${r.tmpLeftover}(内含明文 token,请手动删除)`);
133
- console.log(` url: http://127.0.0.1:${port} (不带 /v1 —— Claude Code 自己拼 /v1/messages)`);
134
- console.log(` modelPicker: ${r.rows} 行${batch ? "(勾选集全集)" : "(单模型)"}`);
135
- console.log(` 冒烟: curl -H "Authorization: Bearer $(mslxdff -showtoken)" -H "anthropic-version: 2023-06-01" -H "content-type: application/json" -d '{"model":"${r.id}","max_tokens":1024,"messages":[{"role":"user","content":"hi"}]}' http://127.0.0.1:${port}/v1/messages (max_tokens 别给太小:思考型模型会把额度全花在 reasoning 上而吐空正文轮)`);
136
- console.log(` ⚠ env 与 model 均为 Claude Code 启动时读取 → 重启 claude 生效;token 轮换后需重跑本命令`);
137
- console.log(` ℹ 客户端不认识的模型 id 一律按 200K 窗口假设(带 \`[1m]\` 后缀才按 1M),需要更大窗口另设 env.CLAUDE_CODE_MAX_CONTEXT_TOKENS`);
138
- } catch (err) {
139
- console.error(`failed to sync to claude: ${String(err?.message || err)}`);
140
- process.exit(1);
141
- }
142
- process.exit(0);
143
- }
144
- if (target === "opencode") {
145
- const wantsAll = args.includes("--all") || args.includes("-a") || args[idx + 2] === "all";
146
- if (wantsAll) {
147
- const picks = loadModelPicks();
148
- const list = picks.length ? picks : [loadPreferredModel() || getPref()].filter(Boolean);
149
- if (!list.length) {
150
- console.error("no picks and no preferred model; use: mslxdff -setto opencode <modelId>");
151
- process.exit(1);
152
- }
153
- try {
154
- const { token } = await loadToken();
155
- const persisted = getPort();
156
- const envPort = Number(process.env.MSLXDFF_PORT);
157
- const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
158
- const file = opencodeConfigPath();
159
- let inserted = 0, updated = 0, prunedTotal = 0;
160
- for (const rawId of list) {
161
- const norm = normalizeModel(rawId);
162
- if (!norm || norm === "auto") continue;
163
- // 首次循环也同步 preferred(保持 daemon 热重载语义)
164
- if (rawId === list[0]) savePreferredModel(norm);
165
- const r = await syncToOpencode({ id: norm, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
166
- if (r.action === "inserted") inserted++; else updated++;
167
- prunedTotal += r.pruned || 0;
168
- console.log(` ${r.action} "${r.id}" -> ${r.internal} @ ${file}`);
169
- if (r.capsSummaryText) console.log(` ${r.capsSummaryText}`);
170
- if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目`);
171
- }
172
- console.log(`synced to opencode: ${inserted} inserted, ${updated} updated, total ${list.length} @ ${file}`);
173
- if (prunedTotal) console.log(` pruned ${prunedTotal} 个失效模型(未在 picks,不再于 opencode 显示)`);
174
- console.log(` url: http://127.0.0.1:${port}/v1`);
175
- console.log(` models: ${list.map((x) => normalizeModel(x)).join(", ")}`);
176
- console.log(` opencode 选 mslxdff/<model> 直达本地,同名如 mslxdff/deepseek-v4-flash-free 或 mslxdff/bai-deepseek-v4-flash`);
177
- } catch (err) {
178
- console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
179
- process.exit(1);
180
- }
181
- process.exit(0);
182
- }
183
- const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
184
- let id;
185
- if (raw) {
186
- if (raw === "auto" || !raw) {
187
- console.error("modelId 不能为 auto 或空");
188
- process.exit(1);
189
- }
190
- const norm = normalizeModel(raw);
191
- if (!norm) {
192
- console.error("modelId 不能为空");
193
- process.exit(1);
194
- }
195
- savePreferredModel(norm);
196
- console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
197
- id = norm;
198
- } else {
199
- const pref = loadPreferredModel() || getPref();
200
- if (!pref) {
201
- console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
202
- process.exit(1);
203
- }
204
- const norm = normalizeModel(pref);
205
- if (!norm) {
206
- console.error("modelId 不能为空");
207
- process.exit(1);
208
- }
209
- id = norm;
210
- }
211
- // 可选:校验是否在 free 列表
212
- try {
213
- const cacheFile = join(logDir(), "models.json");
214
- const models = createModelsService({
215
- baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
216
- headers: createUpstreamClient({}).headers,
217
- refreshMs: 0,
218
- cacheFile,
219
- });
220
- const fresh = await Promise.race([
221
- models.get(),
222
- new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
223
- ]);
224
- if (fresh?.data?.length) {
225
- const ids = fresh.data.map((m) => m.id);
226
- // 对 slash 形态也做 dash 兼容检查
227
- const dashId = id.includes("/") ? id.replace(/\//g, "-") : id;
228
- if (!ids.includes(id) && !ids.includes(dashId)) {
229
- console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to opencode`);
230
- }
231
- }
232
- } catch {}
233
- try {
234
- const { token } = await loadToken();
235
- const persisted = getPort();
236
- const envPort = Number(process.env.MSLXDFF_PORT);
237
- const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
238
- const file = opencodeConfigPath();
239
- const r = await syncToOpencode({ id, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
240
- console.log(`synced to opencode: ${r.action} "${r.id}" @ ${file}`);
241
- if (r.capsSummaryText) console.log(` 能力: ${r.capsSummaryText}`);
242
- else console.log(` 能力: 未收录该模型的能力目录,条目仅含名称(不影响使用)`);
243
- if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目(此前仅含名称,已注入推理档位/读图/上下文)`);
244
- if (r.backfilled) console.log(` backfilled ${r.backfilled} 个 picks 模型(此前 pick 了但未同步过,现已补齐)`);
245
- if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 opencode 显示)`);
246
- console.log(` url: http://127.0.0.1:${port}/v1`);
247
- console.log(` opencode 选 mslxdff/${r.id} 直达本地 ${r.internal}${r.storageKey !== r.internal ? ` (dash→${r.internal} 自动映射)` : ""}`);
248
- } catch (err) {
249
- console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
250
- process.exit(1);
251
- }
252
- process.exit(0);
253
- }
254
- const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
255
- let id;
256
- if (raw) {
257
- if (raw === "auto" || !raw) {
258
- console.error("modelId 不能为 auto 或空");
259
- process.exit(1);
260
- }
261
- const norm = normalizeModel(raw);
262
- if (!norm) {
263
- console.error("modelId 不能为空");
264
- process.exit(1);
265
- }
266
- savePreferredModel(norm);
267
- console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
268
- id = norm;
269
- } else {
270
- id = loadPreferredModel() || getPref();
271
- if (!id) {
272
- console.error("no preferred model set; use: mslxdff -setto workbuddy <modelId>");
273
- process.exit(1);
274
- }
275
- }
276
- try {
277
- const cacheFile = join(logDir(), "models.json");
278
- const models = createModelsService({
279
- baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
280
- headers: createUpstreamClient({}).headers,
281
- refreshMs: 0,
282
- cacheFile,
283
- });
284
- const fresh = await Promise.race([
285
- models.get(),
286
- new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
287
- ]);
288
- if (fresh?.data?.length) {
289
- const ids = fresh.data.map((m) => m.id);
290
- if (!ids.includes(id)) {
291
- console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to WorkBuddy`);
292
- }
293
- }
294
- } catch {}
295
- try {
296
- const { token } = await loadToken();
297
- const persisted = getPort();
298
- const envPort = Number(process.env.MSLXDFF_PORT);
299
- const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
300
- const file = workbuddyModelsPath();
301
- const r = await syncToWorkbuddy({ id, token, port, file, keep: pruneKeep() });
302
- console.log(`synced to WorkBuddy: ${r.action} "${id}" @ ${file}`);
303
- if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 WorkBuddy 显示)`);
304
- console.log(` url: http://127.0.0.1:${port}/v1/chat/completions`);
305
- } catch (err) {
306
- console.error(`failed to sync to WorkBuddy: ${String(err?.message || err)}`);
307
- process.exit(1);
308
- }
309
- process.exit(0);
310
- }
1
+ import { join } from "node:path";
2
+ import { loadToken, getPort, savePreferredModel, loadPreferredModel, loadModelPicks } from "../../state.js";
3
+ import { getPreferredModel as getPref } from "../../auto.js";
4
+ import { normalizeModel } from "../../reasoning.js";
5
+ import { syncToWorkbuddy, workbuddyModelsPath } from "../../sync-workbuddy.js";
6
+ import { syncToOpencode, opencodeConfigPath } from "../../sync-opencode.js";
7
+ import { syncToCodex, codexConfigPath } from "../../sync-codex.js";
8
+ import { syncToClaude, claudeSettingsPath } from "../../sync-claude.js";
9
+ import { createModelsService } from "../../models.js";
10
+ import { createUpstreamClient } from "../../upstream.js";
11
+ import { logDir } from "../../logs.js";
12
+
13
+ // 剪枝口径:picks 非空时,未在 picks 的旧模型视为失效,下次 setto 从第三方配置里摘除;
14
+ // picks 为空(=不筛选)时返回 null,sync 侧一个不动。
15
+ function pruneKeep() {
16
+ const picks = loadModelPicks();
17
+ return picks.length ? picks : null;
18
+ }
19
+
20
+ export async function handleSetto(args) {
21
+ if (!(args.includes("-setto") || args.includes("--setto"))) return false;
22
+ const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
23
+ const target = args[idx + 1];
24
+ if (!["workbuddy", "opencode", "chatgpt", "codex", "claude", "claude-desktop", "claudedesktop"].includes(target)) {
25
+ console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId|--all] | mslxdff -setto chatgpt [modelId] | mslxdff -setto claude [modelId] [--behaves-as <id>] | mslxdff -setto claude-desktop [modelId ...] [--max N] [--max-effort <lvl>] [--1m <off|auto|all|列表>] [--no-alias] [--check] [--official] (claude: 不带 modelId = 写入 modelPicks 勾选集全集,--all 与之同义 / claude-desktop: 写 Claude Desktop 3P profile + 角色槽 alias,--1m 缺省 auto=有据才打 1M 标,详见 ADR-0056)");
26
+ process.exit(1);
27
+ }
28
+ if (target === "claude-desktop" || target === "claudedesktop") {
29
+ // Claude Desktop on 3P(ADR-0048):写 profile + 角色槽 alias。sync.js 只做转发——
30
+ // 本文件已近 20KB 体积门,实现放 src/cli/commands/claude-desktop.js。
31
+ const { handleClaudeDesktopSetto } = await import("./claude-desktop.js");
32
+ return handleClaudeDesktopSetto(args, idx);
33
+ }
34
+ if (target === "chatgpt" || target === "codex") {
35
+ // Codex/ChatGPT 三端共用 ~/.codex/config.toml:写 model + model_provider + [model_providers.mslxdff]
36
+ const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
37
+ let id;
38
+ if (raw) {
39
+ if (raw === "auto" || !raw) {
40
+ console.error("modelId 不能为 auto 或空");
41
+ process.exit(1);
42
+ }
43
+ const norm = normalizeModel(raw);
44
+ if (!norm) {
45
+ console.error("modelId 不能为空");
46
+ process.exit(1);
47
+ }
48
+ savePreferredModel(norm);
49
+ console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
50
+ id = norm;
51
+ } else {
52
+ id = loadPreferredModel() || getPref();
53
+ if (!id) {
54
+ console.error("no preferred model set; use: mslxdff -setto chatgpt <modelId>");
55
+ process.exit(1);
56
+ }
57
+ }
58
+ try {
59
+ const persisted = getPort();
60
+ const envPort = Number(process.env.MSLXDFF_PORT);
61
+ const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
62
+ const file = codexConfigPath();
63
+ const r = syncToCodex({ id, port, file });
64
+ console.log(`synced to codex: ${r.action} "${r.id}" @ ${r.file}`);
65
+ console.log(` url: http://127.0.0.1:${port}/v1/responses (Responses API)`);
66
+ console.log(` 鉴权走 mslxdff -showtoken 命令(token 不落盘),直接 codex exec "hi" 验证`);
67
+ } catch (err) {
68
+ console.error(`failed to sync to codex: ${String(err?.message || err)}`);
69
+ process.exit(1);
70
+ }
71
+ process.exit(0);
72
+ }
73
+ if (target === "claude") {
74
+ // Claude Code 用户设置 ~/.claude/settings.json:走本机 /v1/messages 外壳(ADR-0047)
75
+ // 参数校验一律先于任何写盘(含 savePreferredModel):校验不过不得碰 state.json 与 settings.json
76
+ // --behaves-as <已知 claude-* id> 覆盖能力锚;`--behaves-as ""` 显式关掉(关掉后本机 Claude Code 可能对未知 id 拒跑)
77
+ const baIdx = args.findIndex((x) => x === "--behaves-as");
78
+ if (baIdx >= 0 && args[baIdx + 1] === undefined) {
79
+ console.error("--behaves-as 需要取值:给一个已知 claude-* id,或显式传空串关闭(--behaves-as \"\")");
80
+ process.exit(1);
81
+ }
82
+ const behavesAs = baIdx >= 0 ? String(args[baIdx + 1]) : undefined;
83
+ const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") && args[idx + 2] !== "all" ? String(args[idx + 2]).trim() : null;
84
+ const picksAll = loadModelPicks();
85
+ let id;
86
+ let batch = false;
87
+ if (raw) {
88
+ if (raw === "auto" || !normalizeModel(raw)) {
89
+ console.error("modelId 不能为 auto 或空");
90
+ process.exit(1);
91
+ }
92
+ const norm = normalizeModel(raw);
93
+ if (!norm) {
94
+ console.error("modelId 不能为空");
95
+ process.exit(1);
96
+ }
97
+ if (picksAll.length && !picksAll.includes(norm)) {
98
+ console.log(` ⚠ "${norm}" 不在勾选集 modelPicks 内 —— 可能是拼错或已失效;仍按你显式指定的写入`);
99
+ }
100
+ savePreferredModel(norm);
101
+ console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
102
+ id = norm;
103
+ } else {
104
+ // 不带参数(含 --all / 裸 all)= 写入全部勾选模型:modelPicker.options = modelPicks 全集,
105
+ // 顶层 model 也只在勾选集内取。**不再兜底 preferredModel** —— 它可能是一个已从池子消失的 id
106
+ // (真实踩坑:preferredModel=mimo-v2.5-free 被写进 Claude Code 后,每次请求只会得到 502)。
107
+ batch = true;
108
+ if (!picksAll.length) {
109
+ console.error("modelPicks 勾选集为空 → 无可写入的模型;先跑 mslxdff -model pick 勾选,或直接 mslxdff -setto claude <modelId>");
110
+ process.exit(1);
111
+ }
112
+ const pref = normalizeModel(loadPreferredModel() || getPref() || "");
113
+ if (pref && picksAll.includes(pref)) {
114
+ id = pref;
115
+ } else {
116
+ if (pref) console.log(` ⚠ 网关首选模型 "${pref}" 不在勾选集内(可能已失效)→ claude 默认改用勾选集首个 "${picksAll[0]}"`);
117
+ id = normalizeModel(picksAll[0]);
118
+ }
119
+ }
120
+ try {
121
+ const { token } = await loadToken();
122
+ const persisted = getPort();
123
+ const envPort = Number(process.env.MSLXDFF_PORT);
124
+ const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
125
+ // options 由本命令全权管理:默认/`--all` 用勾选集整体替换,显式单模型则只留一条。
126
+ const picks = batch ? picksAll : null;
127
+ const file = claudeSettingsPath();
128
+ const r = syncToClaude({ id, token, port, picks, file, behavesAs });
129
+ console.log(`synced to claude: ${r.action} "${r.id}" @ ${r.file}${r.changed ? "" : "(已是目标状态,未改字节)"}`);
130
+ if (r.backup) { console.log(` backup: ${r.backup}`); console.log(` 还原: Copy-Item "${r.backup}" "${r.file}"`); }
131
+ else console.log(` backup: 未生成(本次无变化,或同目录已有更早的 settings.pre-mslxdff.json —— 那份是首次接管前的原文)`);
132
+ if (r.tmpLeftover) console.error(` ⚠ 临时文件清理失败:${r.tmpLeftover}(内含明文 token,请手动删除)`);
133
+ console.log(` url: http://127.0.0.1:${port} (不带 /v1 —— Claude Code 自己拼 /v1/messages)`);
134
+ console.log(` modelPicker: ${r.rows} 行${batch ? "(勾选集全集)" : "(单模型)"}`);
135
+ console.log(` 冒烟: curl -H "Authorization: Bearer $(mslxdff -showtoken)" -H "anthropic-version: 2023-06-01" -H "content-type: application/json" -d '{"model":"${r.id}","max_tokens":1024,"messages":[{"role":"user","content":"hi"}]}' http://127.0.0.1:${port}/v1/messages (max_tokens 别给太小:思考型模型会把额度全花在 reasoning 上而吐空正文轮)`);
136
+ console.log(` ⚠ env 与 model 均为 Claude Code 启动时读取 → 重启 claude 生效;token 轮换后需重跑本命令`);
137
+ console.log(` ℹ 客户端不认识的模型 id 一律按 200K 窗口假设(带 \`[1m]\` 后缀才按 1M),需要更大窗口另设 env.CLAUDE_CODE_MAX_CONTEXT_TOKENS`);
138
+ } catch (err) {
139
+ console.error(`failed to sync to claude: ${String(err?.message || err)}`);
140
+ process.exit(1);
141
+ }
142
+ process.exit(0);
143
+ }
144
+ if (target === "opencode") {
145
+ const wantsAll = args.includes("--all") || args.includes("-a") || args[idx + 2] === "all";
146
+ if (wantsAll) {
147
+ const picks = loadModelPicks();
148
+ const list = picks.length ? picks : [loadPreferredModel() || getPref()].filter(Boolean);
149
+ if (!list.length) {
150
+ console.error("no picks and no preferred model; use: mslxdff -setto opencode <modelId>");
151
+ process.exit(1);
152
+ }
153
+ try {
154
+ const { token } = await loadToken();
155
+ const persisted = getPort();
156
+ const envPort = Number(process.env.MSLXDFF_PORT);
157
+ const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
158
+ const file = opencodeConfigPath();
159
+ let inserted = 0, updated = 0, prunedTotal = 0;
160
+ for (const rawId of list) {
161
+ const norm = normalizeModel(rawId);
162
+ if (!norm || norm === "auto") continue;
163
+ // 首次循环也同步 preferred(保持 daemon 热重载语义)
164
+ if (rawId === list[0]) savePreferredModel(norm);
165
+ const r = await syncToOpencode({ id: norm, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
166
+ if (r.action === "inserted") inserted++; else updated++;
167
+ prunedTotal += r.pruned || 0;
168
+ console.log(` ${r.action} "${r.id}" -> ${r.internal} @ ${file}`);
169
+ if (r.capsSummaryText) console.log(` ${r.capsSummaryText}`);
170
+ if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目`);
171
+ }
172
+ console.log(`synced to opencode: ${inserted} inserted, ${updated} updated, total ${list.length} @ ${file}`);
173
+ if (prunedTotal) console.log(` pruned ${prunedTotal} 个失效模型(未在 picks,不再于 opencode 显示)`);
174
+ console.log(` url: http://127.0.0.1:${port}/v1`);
175
+ console.log(` models: ${list.map((x) => normalizeModel(x)).join(", ")}`);
176
+ console.log(` opencode 选 mslxdff/<model> 直达本地,同名如 mslxdff/deepseek-v4-flash-free 或 mslxdff/bai-deepseek-v4-flash`);
177
+ } catch (err) {
178
+ console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
179
+ process.exit(1);
180
+ }
181
+ process.exit(0);
182
+ }
183
+ const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
184
+ let id;
185
+ if (raw) {
186
+ if (raw === "auto" || !raw) {
187
+ console.error("modelId 不能为 auto 或空");
188
+ process.exit(1);
189
+ }
190
+ const norm = normalizeModel(raw);
191
+ if (!norm) {
192
+ console.error("modelId 不能为空");
193
+ process.exit(1);
194
+ }
195
+ savePreferredModel(norm);
196
+ console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
197
+ id = norm;
198
+ } else {
199
+ const pref = loadPreferredModel() || getPref();
200
+ if (!pref) {
201
+ console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
202
+ process.exit(1);
203
+ }
204
+ const norm = normalizeModel(pref);
205
+ if (!norm) {
206
+ console.error("modelId 不能为空");
207
+ process.exit(1);
208
+ }
209
+ id = norm;
210
+ }
211
+ // 可选:校验是否在 free 列表
212
+ try {
213
+ const cacheFile = join(logDir(), "models.json");
214
+ const models = createModelsService({
215
+ baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
216
+ headers: createUpstreamClient({}).headers,
217
+ refreshMs: 0,
218
+ cacheFile,
219
+ });
220
+ const fresh = await Promise.race([
221
+ models.get(),
222
+ new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
223
+ ]);
224
+ if (fresh?.data?.length) {
225
+ const ids = fresh.data.map((m) => m.id);
226
+ // 对 slash 形态也做 dash 兼容检查
227
+ const dashId = id.includes("/") ? id.replace(/\//g, "-") : id;
228
+ if (!ids.includes(id) && !ids.includes(dashId)) {
229
+ console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to opencode`);
230
+ }
231
+ }
232
+ } catch {}
233
+ try {
234
+ const { token } = await loadToken();
235
+ const persisted = getPort();
236
+ const envPort = Number(process.env.MSLXDFF_PORT);
237
+ const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
238
+ const file = opencodeConfigPath();
239
+ const r = await syncToOpencode({ id, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
240
+ console.log(`synced to opencode: ${r.action} "${r.id}" @ ${file}`);
241
+ if (r.capsSummaryText) console.log(` 能力: ${r.capsSummaryText}`);
242
+ else console.log(` 能力: 未收录该模型的能力目录,条目仅含名称(不影响使用)`);
243
+ if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目(此前仅含名称,已注入推理档位/读图/上下文)`);
244
+ if (r.backfilled) console.log(` backfilled ${r.backfilled} 个 picks 模型(此前 pick 了但未同步过,现已补齐)`);
245
+ if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 opencode 显示)`);
246
+ console.log(` url: http://127.0.0.1:${port}/v1`);
247
+ console.log(` opencode 选 mslxdff/${r.id} 直达本地 ${r.internal}${r.storageKey !== r.internal ? ` (dash→${r.internal} 自动映射)` : ""}`);
248
+ } catch (err) {
249
+ console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
250
+ process.exit(1);
251
+ }
252
+ process.exit(0);
253
+ }
254
+ const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
255
+ let id;
256
+ if (raw) {
257
+ if (raw === "auto" || !raw) {
258
+ console.error("modelId 不能为 auto 或空");
259
+ process.exit(1);
260
+ }
261
+ const norm = normalizeModel(raw);
262
+ if (!norm) {
263
+ console.error("modelId 不能为空");
264
+ process.exit(1);
265
+ }
266
+ savePreferredModel(norm);
267
+ console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
268
+ id = norm;
269
+ } else {
270
+ id = loadPreferredModel() || getPref();
271
+ if (!id) {
272
+ console.error("no preferred model set; use: mslxdff -setto workbuddy <modelId>");
273
+ process.exit(1);
274
+ }
275
+ }
276
+ try {
277
+ const cacheFile = join(logDir(), "models.json");
278
+ const models = createModelsService({
279
+ baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
280
+ headers: createUpstreamClient({}).headers,
281
+ refreshMs: 0,
282
+ cacheFile,
283
+ });
284
+ const fresh = await Promise.race([
285
+ models.get(),
286
+ new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
287
+ ]);
288
+ if (fresh?.data?.length) {
289
+ const ids = fresh.data.map((m) => m.id);
290
+ if (!ids.includes(id)) {
291
+ console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to WorkBuddy`);
292
+ }
293
+ }
294
+ } catch {}
295
+ try {
296
+ const { token } = await loadToken();
297
+ const persisted = getPort();
298
+ const envPort = Number(process.env.MSLXDFF_PORT);
299
+ const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
300
+ const file = workbuddyModelsPath();
301
+ const r = await syncToWorkbuddy({ id, token, port, file, keep: pruneKeep() });
302
+ console.log(`synced to WorkBuddy: ${r.action} "${id}" @ ${file}`);
303
+ if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 WorkBuddy 显示)`);
304
+ console.log(` url: http://127.0.0.1:${port}/v1/chat/completions`);
305
+ } catch (err) {
306
+ console.error(`failed to sync to WorkBuddy: ${String(err?.message || err)}`);
307
+ process.exit(1);
308
+ }
309
+ process.exit(0);
310
+ }