mslxdff 0.1.236 → 0.1.237

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 (102) 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/models.js +149 -149
  27. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  28. package/src/cli/commands/provider/raccoon-quota.js +156 -156
  29. package/src/cli/commands/sync.js +310 -310
  30. package/src/cli/status.js +282 -282
  31. package/src/daemon.js +246 -246
  32. package/src/model-capabilities/enrich.js +86 -86
  33. package/src/model-capabilities/index.js +183 -183
  34. package/src/model-capabilities/parse.js +70 -70
  35. package/src/models.js +293 -293
  36. package/src/peers.js +389 -389
  37. package/src/providers/agentrouter.js +238 -238
  38. package/src/providers/base.js +164 -164
  39. package/src/providers/cline/auth.js +228 -228
  40. package/src/providers/cline/chat.js +307 -307
  41. package/src/providers/cline.js +2 -2
  42. package/src/providers/codearts/chat.js +207 -207
  43. package/src/providers/dispatcher.js +147 -147
  44. package/src/providers/globalqwenwork/cosy.js +123 -123
  45. package/src/providers/globalqwenwork/crypto.js +218 -218
  46. package/src/providers/globalqwenwork/http.js +20 -20
  47. package/src/providers/globalqwenwork/rsa.js +54 -54
  48. package/src/providers/globalqwenwork/sse.js +268 -268
  49. package/src/providers/globalqwenwork/stream.js +130 -130
  50. package/src/providers/globalqwenwork/upstream.js +122 -122
  51. package/src/providers/keyring.js +60 -60
  52. package/src/providers/qoder/chat.js +292 -292
  53. package/src/providers/qoder/index.js +311 -311
  54. package/src/providers/qoder/sse.js +233 -233
  55. package/src/providers/qwenwork/account-store.js +133 -133
  56. package/src/providers/qwenwork/constants.js +67 -67
  57. package/src/providers/qwenwork/cosy.js +120 -120
  58. package/src/providers/qwenwork/crypto.js +218 -218
  59. package/src/providers/qwenwork/http.js +20 -20
  60. package/src/providers/qwenwork/index.js +337 -337
  61. package/src/providers/qwenwork/payload.js +144 -144
  62. package/src/providers/qwenwork/rsa.js +54 -54
  63. package/src/providers/qwenwork/sse.js +268 -268
  64. package/src/providers/qwenwork/stream.js +130 -130
  65. package/src/providers/qwenwork/upstream.js +120 -120
  66. package/src/providers/qwenwork.js +1 -1
  67. package/src/providers/raccoon/const.js +219 -219
  68. package/src/providers/raccoon/credits.js +285 -285
  69. package/src/providers/registry.js +89 -89
  70. package/src/providers/workbuddy/chat.js +298 -298
  71. package/src/providers/workbuddy/reshape.js +152 -152
  72. package/src/providers/workbuddy.js +2 -2
  73. package/src/reasoning.js +32 -32
  74. package/src/routes/chat/broadband-handler.js +80 -80
  75. package/src/routes/chat/gateway.js +46 -46
  76. package/src/routes/chat/relay-pipeline.js +282 -282
  77. package/src/routes/chat/via-route-handler.js +158 -158
  78. package/src/routes/hedge.js +260 -260
  79. package/src/routes/models-route.js +167 -167
  80. package/src/routes/peers.js +373 -373
  81. package/src/routes/stream.js +364 -364
  82. package/src/runtime/bootstrap.js +75 -75
  83. package/src/runtime/keep-warm.js +135 -135
  84. package/src/runtime/provider-gate.js +75 -75
  85. package/src/runtime/providers-setup.js +187 -187
  86. package/src/runtime/raccoon-checkin.js +175 -175
  87. package/src/runtime/server-lifecycle.js +34 -10
  88. package/src/server.js +85 -85
  89. package/src/state/schemas/allowlist.js +92 -92
  90. package/src/sync-opencode.js +280 -280
  91. package/src/transport/index.js +244 -244
  92. package/src/transport/pool.js +56 -56
  93. package/src/transport/retry.js +24 -24
  94. package/src/transport/sse.js +93 -93
  95. package/src/upstream-probe/display.js +52 -52
  96. package/src/upstream-probe/probe.js +49 -49
  97. package/src/upstream-probe/rotate.js +110 -110
  98. package/src/upstream-probe/start.js +45 -45
  99. package/src/upstream.js +355 -355
  100. package/docs/cli_help_mini.md +0 -149
  101. package/src/providers/AGENTS.md +0 -49
  102. 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
+ }