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.
- package/bin/mslxdff.js +4 -4
- package/docs/ARCHITECTURE.md +426 -0
- package/docs/FEATURE_TREE.md +164 -0
- package/docs/MOBILE.md +82 -0
- package/docs/adr/0001-reasoning-content-injection.md +14 -0
- package/docs/adr/0002-models-free-filter.md +12 -0
- package/docs/adr/0003-zero-state-no-auth.md +10 -0
- package/docs/adr/0004-bearer-token.md +18 -0
- package/docs/adr/0005-peer-mesh.md +53 -0
- package/docs/adr/0006-broadband-member.md +103 -0
- package/docs/adr/0007-multi-provider-prefix.md +25 -0
- package/docs/adr/0008-share-keys-to-peers.md +52 -0
- package/docs/adr/0009-chat-repl.md +37 -0
- package/docs/adr/0010-allowlist.md +36 -0
- package/docs/adr/0011-broadband-stream.md +30 -0
- package/docs/adr/0012-responses-endpoint-codex-sync.md +48 -0
- package/docs/adr/0013-node16-compat.md +41 -0
- package/docs/adr/0014-deepseek-provider.md +52 -0
- package/docs/adr/0015-upstream-probe-routing.md +50 -0
- package/docs/adr/0016-model-capabilities.md +27 -0
- package/docs/adr/0017-ai-sdk-upstream-engine.md +59 -0
- package/docs/adr/0018-zen-client-identity.md +52 -0
- package/docs/adr/0019-share-keys-always-lend.md +59 -0
- package/docs/adr/0020-zen-free-lane-agent-shape.md +52 -0
- package/docs/adr/0021-usage-report-jsonl.md +47 -0
- package/docs/adr/0022-models-capability-merge.md +61 -0
- package/docs/adr/0023-key-provider-default-direct.md +58 -0
- package/docs/adr/0024-node18-baseline.md +63 -0
- package/docs/adr/0025-workbuddy-authdir-follows-state.md +71 -0
- package/docs/adr/0026-cline-provider-id-unify.md +67 -0
- package/docs/adr/0027-codearts-provider.md +48 -0
- package/docs/adr/0028-traework-provider.md +34 -0
- package/docs/adr/0029-qoder-native-provider.md +60 -0
- package/docs/adr/0030-models-list-scoped-by-picks.md +53 -0
- package/docs/adr/0031-qoder-true-streaming.md +50 -0
- package/docs/adr/0032-generic-responses-channel.md +72 -0
- package/docs/adr/0033-cline-allowlist-auto-sync.md +75 -0
- package/docs/adr/0034-request-level-human-readable-observability.md +60 -0
- package/docs/adr/0035-sdk-channel-headers-timeout.md +49 -0
- package/docs/adr/0036-qoder-per-request-sticky-account.md +82 -0
- package/docs/adr/0037-qwenwork-independent-provider.md +82 -0
- package/docs/adr/0038-zcode-provider.md +140 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +30 -0
- package/docs/agents/triage-labels.md +15 -0
- package/docs/cli_help.md +1391 -0
- package/docs/cli_help_mini.md +133 -0
- package/docs/plans/bench-via-latency-2026-09-01.md +215 -0
- package/docs/plugins.md +187 -0
- package/package.json +1 -1
- package/src/auto.js +254 -254
- package/src/bench/cline-bench.js +42 -42
- package/src/bench/probe.js +70 -70
- package/src/bench/report.js +162 -162
- package/src/bench/runner.js +77 -77
- package/src/bench/via-probe.js +124 -124
- package/src/bench/via-routes.js +87 -87
- package/src/bench/workbuddy-bench.js +54 -54
- package/src/chat/engine.js +160 -160
- package/src/chat/gateway.js +163 -163
- package/src/chat/orchestrator.js +234 -234
- package/src/chat/prompt.js +70 -70
- package/src/chat/repl.js +88 -88
- package/src/chat/terminal.js +135 -135
- package/src/chat/tools.js +306 -306
- package/src/chat-pipeline/index.js +123 -123
- package/src/chat-pipeline/policy.js +76 -76
- package/src/chat-pipeline/serial-trial.js +210 -210
- package/src/cli/commands/group.js +249 -249
- package/src/cli/commands/model/list-providers.js +4 -3
- package/src/cli/commands/model/list-render.js +7 -9
- package/src/cli/commands/model/list-sort.js +52 -0
- package/src/cli/commands/model/list-tty.js +76 -0
- package/src/cli/commands/model/list.js +5 -62
- package/src/cli/commands/model/picks.js +50 -50
- package/src/cli/commands/provider/bench-via.js +247 -247
- package/src/cli/commands/provider/bench.js +141 -141
- package/src/cli/commands/provider/index.js +124 -116
- package/src/cli/commands/provider/models.js +139 -128
- package/src/cli/commands/provider/qwenwork-login.js +119 -0
- package/src/cli/commands/provider/zcode-login.js +77 -0
- package/src/cli/commands/provider/zcode-quota.js +55 -0
- package/src/cli/commands/sync.js +232 -232
- package/src/cli/provider-row.js +2 -2
- package/src/cli/status.js +279 -279
- package/src/daemon.js +96 -96
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/model-trace.js +1 -0
- package/src/models.js +225 -225
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/auth.js +228 -228
- package/src/providers/cline/chat.js +307 -307
- package/src/providers/cline.js +2 -2
- package/src/providers/keyring.js +60 -56
- package/src/providers/qoder/chat.js +183 -100
- package/src/providers/qoder/index.js +230 -184
- package/src/providers/qoder/sse.js +103 -46
- package/src/providers/qoder/sticky.js +1 -0
- package/src/providers/qoder/stream.js +32 -9
- package/src/providers/qwenwork/account-store.js +133 -0
- package/src/providers/qwenwork/constants.js +67 -0
- package/src/providers/qwenwork/cosy.js +120 -0
- package/src/providers/qwenwork/crypto.js +218 -0
- package/src/providers/qwenwork/http.js +20 -0
- package/src/providers/qwenwork/index.js +327 -0
- package/src/providers/qwenwork/payload.js +142 -0
- package/src/providers/qwenwork/rsa.js +54 -0
- package/src/providers/qwenwork/sse.js +268 -0
- package/src/providers/qwenwork/stream.js +130 -0
- package/src/providers/qwenwork/upstream.js +120 -0
- package/src/providers/qwenwork.js +1 -0
- package/src/providers/registry.js +66 -56
- package/src/providers/share-keys.js +2 -2
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/account-store.js +129 -0
- package/src/providers/zcode/auth.js +28 -0
- package/src/providers/zcode/chat.js +171 -0
- package/src/providers/zcode/const.js +54 -0
- package/src/providers/zcode/headers.js +55 -0
- package/src/providers/zcode/index.js +124 -0
- package/src/providers/zcode/models.js +65 -0
- package/src/providers/zcode/oauth.js +120 -0
- package/src/providers/zcode/quota.js +176 -0
- package/src/providers/zcode/sse.js +179 -0
- package/src/reasoning.js +32 -32
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +250 -246
- package/src/routes/chat/via-route-handler.js +144 -144
- package/src/routes/hedge.js +255 -255
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream.js +438 -399
- package/src/runtime/bootstrap.js +45 -45
- package/src/runtime/provider-gate.js +33 -30
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +64 -64
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/transport/index.js +244 -244
- package/src/transport/pool.js +56 -56
- package/src/transport/retry.js +24 -24
- package/src/transport/sse.js +93 -93
- package/src/upstream-probe/display.js +52 -52
- package/src/upstream-probe/probe.js +49 -49
- package/src/upstream-probe/rotate.js +110 -110
- package/src/upstream-probe/start.js +45 -45
- package/src/upstream.js +289 -289
package/src/cli/commands/sync.js
CHANGED
|
@@ -1,232 +1,232 @@
|
|
|
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 { createModelsService } from "../../models.js";
|
|
9
|
-
import { createUpstreamClient } from "../../upstream.js";
|
|
10
|
-
import { logDir } from "../../logs.js";
|
|
11
|
-
|
|
12
|
-
// 剪枝口径:picks 非空时,未在 picks 的旧模型视为失效,下次 setto 从第三方配置里摘除;
|
|
13
|
-
// picks 为空(=不筛选)时返回 null,sync 侧一个不动。
|
|
14
|
-
function pruneKeep() {
|
|
15
|
-
const picks = loadModelPicks();
|
|
16
|
-
return picks.length ? picks : null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function handleSetto(args) {
|
|
20
|
-
if (!(args.includes("-setto") || args.includes("--setto"))) return false;
|
|
21
|
-
const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
|
|
22
|
-
const target = args[idx + 1];
|
|
23
|
-
if (!["workbuddy", "opencode", "chatgpt", "codex"].includes(target)) {
|
|
24
|
-
console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId|--all] | mslxdff -setto chatgpt [modelId]");
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
if (target === "chatgpt" || target === "codex") {
|
|
28
|
-
// Codex/ChatGPT 三端共用 ~/.codex/config.toml:写 model + model_provider + [model_providers.mslxdff]
|
|
29
|
-
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
30
|
-
let id;
|
|
31
|
-
if (raw) {
|
|
32
|
-
if (raw === "auto" || !raw) {
|
|
33
|
-
console.error("modelId 不能为 auto 或空");
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
const norm = normalizeModel(raw);
|
|
37
|
-
if (!norm) {
|
|
38
|
-
console.error("modelId 不能为空");
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
savePreferredModel(norm);
|
|
42
|
-
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
43
|
-
id = norm;
|
|
44
|
-
} else {
|
|
45
|
-
id = loadPreferredModel() || getPref();
|
|
46
|
-
if (!id) {
|
|
47
|
-
console.error("no preferred model set; use: mslxdff -setto chatgpt <modelId>");
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const persisted = getPort();
|
|
53
|
-
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
54
|
-
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
55
|
-
const file = codexConfigPath();
|
|
56
|
-
const r = syncToCodex({ id, port, file });
|
|
57
|
-
console.log(`synced to codex: ${r.action} "${r.id}" @ ${r.file}`);
|
|
58
|
-
console.log(` url: http://127.0.0.1:${port}/v1/responses (Responses API)`);
|
|
59
|
-
console.log(` 鉴权走 mslxdff -showtoken 命令(token 不落盘),直接 codex exec "hi" 验证`);
|
|
60
|
-
} catch (err) {
|
|
61
|
-
console.error(`failed to sync to codex: ${String(err?.message || err)}`);
|
|
62
|
-
process.exit(1);
|
|
63
|
-
}
|
|
64
|
-
process.exit(0);
|
|
65
|
-
}
|
|
66
|
-
if (target === "opencode") {
|
|
67
|
-
const wantsAll = args.includes("--all") || args.includes("-a") || args[idx + 2] === "all";
|
|
68
|
-
if (wantsAll) {
|
|
69
|
-
const picks = loadModelPicks();
|
|
70
|
-
const list = picks.length ? picks : [loadPreferredModel() || getPref()].filter(Boolean);
|
|
71
|
-
if (!list.length) {
|
|
72
|
-
console.error("no picks and no preferred model; use: mslxdff -setto opencode <modelId>");
|
|
73
|
-
process.exit(1);
|
|
74
|
-
}
|
|
75
|
-
try {
|
|
76
|
-
const { token } = await loadToken();
|
|
77
|
-
const persisted = getPort();
|
|
78
|
-
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
79
|
-
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
80
|
-
const file = opencodeConfigPath();
|
|
81
|
-
let inserted = 0, updated = 0, prunedTotal = 0;
|
|
82
|
-
for (const rawId of list) {
|
|
83
|
-
const norm = normalizeModel(rawId);
|
|
84
|
-
if (!norm || norm === "auto") continue;
|
|
85
|
-
// 首次循环也同步 preferred(保持 daemon 热重载语义)
|
|
86
|
-
if (rawId === list[0]) savePreferredModel(norm);
|
|
87
|
-
const r = await syncToOpencode({ id: norm, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
|
|
88
|
-
if (r.action === "inserted") inserted++; else updated++;
|
|
89
|
-
prunedTotal += r.pruned || 0;
|
|
90
|
-
console.log(` ${r.action} "${r.id}" -> ${r.internal} @ ${file}`);
|
|
91
|
-
if (r.capsSummaryText) console.log(` ${r.capsSummaryText}`);
|
|
92
|
-
if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目`);
|
|
93
|
-
}
|
|
94
|
-
console.log(`synced to opencode: ${inserted} inserted, ${updated} updated, total ${list.length} @ ${file}`);
|
|
95
|
-
if (prunedTotal) console.log(` pruned ${prunedTotal} 个失效模型(未在 picks,不再于 opencode 显示)`);
|
|
96
|
-
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
97
|
-
console.log(` models: ${list.map((x) => normalizeModel(x)).join(", ")}`);
|
|
98
|
-
console.log(` opencode 选 mslxdff/<model> 直达本地,同名如 mslxdff/deepseek-v4-flash-free 或 mslxdff/bai-deepseek-v4-flash`);
|
|
99
|
-
} catch (err) {
|
|
100
|
-
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
process.exit(0);
|
|
104
|
-
}
|
|
105
|
-
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
106
|
-
let id;
|
|
107
|
-
if (raw) {
|
|
108
|
-
if (raw === "auto" || !raw) {
|
|
109
|
-
console.error("modelId 不能为 auto 或空");
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
|
-
const norm = normalizeModel(raw);
|
|
113
|
-
if (!norm) {
|
|
114
|
-
console.error("modelId 不能为空");
|
|
115
|
-
process.exit(1);
|
|
116
|
-
}
|
|
117
|
-
savePreferredModel(norm);
|
|
118
|
-
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
119
|
-
id = norm;
|
|
120
|
-
} else {
|
|
121
|
-
const pref = loadPreferredModel() || getPref();
|
|
122
|
-
if (!pref) {
|
|
123
|
-
console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
const norm = normalizeModel(pref);
|
|
127
|
-
if (!norm) {
|
|
128
|
-
console.error("modelId 不能为空");
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
id = norm;
|
|
132
|
-
}
|
|
133
|
-
// 可选:校验是否在 free 列表
|
|
134
|
-
try {
|
|
135
|
-
const cacheFile = join(logDir(), "models.json");
|
|
136
|
-
const models = createModelsService({
|
|
137
|
-
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
138
|
-
headers: createUpstreamClient({}).headers,
|
|
139
|
-
refreshMs: 0,
|
|
140
|
-
cacheFile,
|
|
141
|
-
});
|
|
142
|
-
const fresh = await Promise.race([
|
|
143
|
-
models.get(),
|
|
144
|
-
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
145
|
-
]);
|
|
146
|
-
if (fresh?.data?.length) {
|
|
147
|
-
const ids = fresh.data.map((m) => m.id);
|
|
148
|
-
// 对 slash 形态也做 dash 兼容检查
|
|
149
|
-
const dashId = id.includes("/") ? id.replace(/\//g, "-") : id;
|
|
150
|
-
if (!ids.includes(id) && !ids.includes(dashId)) {
|
|
151
|
-
console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to opencode`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
} catch {}
|
|
155
|
-
try {
|
|
156
|
-
const { token } = await loadToken();
|
|
157
|
-
const persisted = getPort();
|
|
158
|
-
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
159
|
-
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
160
|
-
const file = opencodeConfigPath();
|
|
161
|
-
const r = await syncToOpencode({ id, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
|
|
162
|
-
console.log(`synced to opencode: ${r.action} "${r.id}" @ ${file}`);
|
|
163
|
-
if (r.capsSummaryText) console.log(` 能力: ${r.capsSummaryText}`);
|
|
164
|
-
else console.log(` 能力: 未收录该模型的能力目录,条目仅含名称(不影响使用)`);
|
|
165
|
-
if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目(此前仅含名称,已注入推理档位/读图/上下文)`);
|
|
166
|
-
if (r.backfilled) console.log(` backfilled ${r.backfilled} 个 picks 模型(此前 pick 了但未同步过,现已补齐)`);
|
|
167
|
-
if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 opencode 显示)`);
|
|
168
|
-
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
169
|
-
console.log(` opencode 选 mslxdff/${r.id} 直达本地 ${r.internal}${r.storageKey !== r.internal ? ` (dash→${r.internal} 自动映射)` : ""}`);
|
|
170
|
-
} catch (err) {
|
|
171
|
-
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
172
|
-
process.exit(1);
|
|
173
|
-
}
|
|
174
|
-
process.exit(0);
|
|
175
|
-
}
|
|
176
|
-
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
177
|
-
let id;
|
|
178
|
-
if (raw) {
|
|
179
|
-
if (raw === "auto" || !raw) {
|
|
180
|
-
console.error("modelId 不能为 auto 或空");
|
|
181
|
-
process.exit(1);
|
|
182
|
-
}
|
|
183
|
-
const norm = normalizeModel(raw);
|
|
184
|
-
if (!norm) {
|
|
185
|
-
console.error("modelId 不能为空");
|
|
186
|
-
process.exit(1);
|
|
187
|
-
}
|
|
188
|
-
savePreferredModel(norm);
|
|
189
|
-
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
190
|
-
id = norm;
|
|
191
|
-
} else {
|
|
192
|
-
id = loadPreferredModel() || getPref();
|
|
193
|
-
if (!id) {
|
|
194
|
-
console.error("no preferred model set; use: mslxdff -setto workbuddy <modelId>");
|
|
195
|
-
process.exit(1);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
try {
|
|
199
|
-
const cacheFile = join(logDir(), "models.json");
|
|
200
|
-
const models = createModelsService({
|
|
201
|
-
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
202
|
-
headers: createUpstreamClient({}).headers,
|
|
203
|
-
refreshMs: 0,
|
|
204
|
-
cacheFile,
|
|
205
|
-
});
|
|
206
|
-
const fresh = await Promise.race([
|
|
207
|
-
models.get(),
|
|
208
|
-
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
209
|
-
]);
|
|
210
|
-
if (fresh?.data?.length) {
|
|
211
|
-
const ids = fresh.data.map((m) => m.id);
|
|
212
|
-
if (!ids.includes(id)) {
|
|
213
|
-
console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to WorkBuddy`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
} catch {}
|
|
217
|
-
try {
|
|
218
|
-
const { token } = await loadToken();
|
|
219
|
-
const persisted = getPort();
|
|
220
|
-
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
221
|
-
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
222
|
-
const file = workbuddyModelsPath();
|
|
223
|
-
const r = await syncToWorkbuddy({ id, token, port, file, keep: pruneKeep() });
|
|
224
|
-
console.log(`synced to WorkBuddy: ${r.action} "${id}" @ ${file}`);
|
|
225
|
-
if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 WorkBuddy 显示)`);
|
|
226
|
-
console.log(` url: http://127.0.0.1:${port}/v1/chat/completions`);
|
|
227
|
-
} catch (err) {
|
|
228
|
-
console.error(`failed to sync to WorkBuddy: ${String(err?.message || err)}`);
|
|
229
|
-
process.exit(1);
|
|
230
|
-
}
|
|
231
|
-
process.exit(0);
|
|
232
|
-
}
|
|
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 { createModelsService } from "../../models.js";
|
|
9
|
+
import { createUpstreamClient } from "../../upstream.js";
|
|
10
|
+
import { logDir } from "../../logs.js";
|
|
11
|
+
|
|
12
|
+
// 剪枝口径:picks 非空时,未在 picks 的旧模型视为失效,下次 setto 从第三方配置里摘除;
|
|
13
|
+
// picks 为空(=不筛选)时返回 null,sync 侧一个不动。
|
|
14
|
+
function pruneKeep() {
|
|
15
|
+
const picks = loadModelPicks();
|
|
16
|
+
return picks.length ? picks : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function handleSetto(args) {
|
|
20
|
+
if (!(args.includes("-setto") || args.includes("--setto"))) return false;
|
|
21
|
+
const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
|
|
22
|
+
const target = args[idx + 1];
|
|
23
|
+
if (!["workbuddy", "opencode", "chatgpt", "codex"].includes(target)) {
|
|
24
|
+
console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId|--all] | mslxdff -setto chatgpt [modelId]");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
if (target === "chatgpt" || target === "codex") {
|
|
28
|
+
// Codex/ChatGPT 三端共用 ~/.codex/config.toml:写 model + model_provider + [model_providers.mslxdff]
|
|
29
|
+
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
30
|
+
let id;
|
|
31
|
+
if (raw) {
|
|
32
|
+
if (raw === "auto" || !raw) {
|
|
33
|
+
console.error("modelId 不能为 auto 或空");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const norm = normalizeModel(raw);
|
|
37
|
+
if (!norm) {
|
|
38
|
+
console.error("modelId 不能为空");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
savePreferredModel(norm);
|
|
42
|
+
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
43
|
+
id = norm;
|
|
44
|
+
} else {
|
|
45
|
+
id = loadPreferredModel() || getPref();
|
|
46
|
+
if (!id) {
|
|
47
|
+
console.error("no preferred model set; use: mslxdff -setto chatgpt <modelId>");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const persisted = getPort();
|
|
53
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
54
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
55
|
+
const file = codexConfigPath();
|
|
56
|
+
const r = syncToCodex({ id, port, file });
|
|
57
|
+
console.log(`synced to codex: ${r.action} "${r.id}" @ ${r.file}`);
|
|
58
|
+
console.log(` url: http://127.0.0.1:${port}/v1/responses (Responses API)`);
|
|
59
|
+
console.log(` 鉴权走 mslxdff -showtoken 命令(token 不落盘),直接 codex exec "hi" 验证`);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(`failed to sync to codex: ${String(err?.message || err)}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
if (target === "opencode") {
|
|
67
|
+
const wantsAll = args.includes("--all") || args.includes("-a") || args[idx + 2] === "all";
|
|
68
|
+
if (wantsAll) {
|
|
69
|
+
const picks = loadModelPicks();
|
|
70
|
+
const list = picks.length ? picks : [loadPreferredModel() || getPref()].filter(Boolean);
|
|
71
|
+
if (!list.length) {
|
|
72
|
+
console.error("no picks and no preferred model; use: mslxdff -setto opencode <modelId>");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const { token } = await loadToken();
|
|
77
|
+
const persisted = getPort();
|
|
78
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
79
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
80
|
+
const file = opencodeConfigPath();
|
|
81
|
+
let inserted = 0, updated = 0, prunedTotal = 0;
|
|
82
|
+
for (const rawId of list) {
|
|
83
|
+
const norm = normalizeModel(rawId);
|
|
84
|
+
if (!norm || norm === "auto") continue;
|
|
85
|
+
// 首次循环也同步 preferred(保持 daemon 热重载语义)
|
|
86
|
+
if (rawId === list[0]) savePreferredModel(norm);
|
|
87
|
+
const r = await syncToOpencode({ id: norm, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
|
|
88
|
+
if (r.action === "inserted") inserted++; else updated++;
|
|
89
|
+
prunedTotal += r.pruned || 0;
|
|
90
|
+
console.log(` ${r.action} "${r.id}" -> ${r.internal} @ ${file}`);
|
|
91
|
+
if (r.capsSummaryText) console.log(` ${r.capsSummaryText}`);
|
|
92
|
+
if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目`);
|
|
93
|
+
}
|
|
94
|
+
console.log(`synced to opencode: ${inserted} inserted, ${updated} updated, total ${list.length} @ ${file}`);
|
|
95
|
+
if (prunedTotal) console.log(` pruned ${prunedTotal} 个失效模型(未在 picks,不再于 opencode 显示)`);
|
|
96
|
+
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
97
|
+
console.log(` models: ${list.map((x) => normalizeModel(x)).join(", ")}`);
|
|
98
|
+
console.log(` opencode 选 mslxdff/<model> 直达本地,同名如 mslxdff/deepseek-v4-flash-free 或 mslxdff/bai-deepseek-v4-flash`);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
106
|
+
let id;
|
|
107
|
+
if (raw) {
|
|
108
|
+
if (raw === "auto" || !raw) {
|
|
109
|
+
console.error("modelId 不能为 auto 或空");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const norm = normalizeModel(raw);
|
|
113
|
+
if (!norm) {
|
|
114
|
+
console.error("modelId 不能为空");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
savePreferredModel(norm);
|
|
118
|
+
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
119
|
+
id = norm;
|
|
120
|
+
} else {
|
|
121
|
+
const pref = loadPreferredModel() || getPref();
|
|
122
|
+
if (!pref) {
|
|
123
|
+
console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const norm = normalizeModel(pref);
|
|
127
|
+
if (!norm) {
|
|
128
|
+
console.error("modelId 不能为空");
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
id = norm;
|
|
132
|
+
}
|
|
133
|
+
// 可选:校验是否在 free 列表
|
|
134
|
+
try {
|
|
135
|
+
const cacheFile = join(logDir(), "models.json");
|
|
136
|
+
const models = createModelsService({
|
|
137
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
138
|
+
headers: createUpstreamClient({}).headers,
|
|
139
|
+
refreshMs: 0,
|
|
140
|
+
cacheFile,
|
|
141
|
+
});
|
|
142
|
+
const fresh = await Promise.race([
|
|
143
|
+
models.get(),
|
|
144
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
145
|
+
]);
|
|
146
|
+
if (fresh?.data?.length) {
|
|
147
|
+
const ids = fresh.data.map((m) => m.id);
|
|
148
|
+
// 对 slash 形态也做 dash 兼容检查
|
|
149
|
+
const dashId = id.includes("/") ? id.replace(/\//g, "-") : id;
|
|
150
|
+
if (!ids.includes(id) && !ids.includes(dashId)) {
|
|
151
|
+
console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to opencode`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch {}
|
|
155
|
+
try {
|
|
156
|
+
const { token } = await loadToken();
|
|
157
|
+
const persisted = getPort();
|
|
158
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
159
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
160
|
+
const file = opencodeConfigPath();
|
|
161
|
+
const r = await syncToOpencode({ id, token, port, file, keep: pruneKeep(), ensureAll: pruneKeep() });
|
|
162
|
+
console.log(`synced to opencode: ${r.action} "${r.id}" @ ${file}`);
|
|
163
|
+
if (r.capsSummaryText) console.log(` 能力: ${r.capsSummaryText}`);
|
|
164
|
+
else console.log(` 能力: 未收录该模型的能力目录,条目仅含名称(不影响使用)`);
|
|
165
|
+
if (r.upgraded) console.log(` 能力补齐 ${r.upgraded} 个旧条目(此前仅含名称,已注入推理档位/读图/上下文)`);
|
|
166
|
+
if (r.backfilled) console.log(` backfilled ${r.backfilled} 个 picks 模型(此前 pick 了但未同步过,现已补齐)`);
|
|
167
|
+
if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 opencode 显示)`);
|
|
168
|
+
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
169
|
+
console.log(` opencode 选 mslxdff/${r.id} 直达本地 ${r.internal}${r.storageKey !== r.internal ? ` (dash→${r.internal} 自动映射)` : ""}`);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
177
|
+
let id;
|
|
178
|
+
if (raw) {
|
|
179
|
+
if (raw === "auto" || !raw) {
|
|
180
|
+
console.error("modelId 不能为 auto 或空");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const norm = normalizeModel(raw);
|
|
184
|
+
if (!norm) {
|
|
185
|
+
console.error("modelId 不能为空");
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
savePreferredModel(norm);
|
|
189
|
+
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
190
|
+
id = norm;
|
|
191
|
+
} else {
|
|
192
|
+
id = loadPreferredModel() || getPref();
|
|
193
|
+
if (!id) {
|
|
194
|
+
console.error("no preferred model set; use: mslxdff -setto workbuddy <modelId>");
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const cacheFile = join(logDir(), "models.json");
|
|
200
|
+
const models = createModelsService({
|
|
201
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
202
|
+
headers: createUpstreamClient({}).headers,
|
|
203
|
+
refreshMs: 0,
|
|
204
|
+
cacheFile,
|
|
205
|
+
});
|
|
206
|
+
const fresh = await Promise.race([
|
|
207
|
+
models.get(),
|
|
208
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
209
|
+
]);
|
|
210
|
+
if (fresh?.data?.length) {
|
|
211
|
+
const ids = fresh.data.map((m) => m.id);
|
|
212
|
+
if (!ids.includes(id)) {
|
|
213
|
+
console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to WorkBuddy`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch {}
|
|
217
|
+
try {
|
|
218
|
+
const { token } = await loadToken();
|
|
219
|
+
const persisted = getPort();
|
|
220
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
221
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
222
|
+
const file = workbuddyModelsPath();
|
|
223
|
+
const r = await syncToWorkbuddy({ id, token, port, file, keep: pruneKeep() });
|
|
224
|
+
console.log(`synced to WorkBuddy: ${r.action} "${id}" @ ${file}`);
|
|
225
|
+
if (r.pruned) console.log(` pruned ${r.pruned} 个失效模型(未在 picks,不再于 WorkBuddy 显示)`);
|
|
226
|
+
console.log(` url: http://127.0.0.1:${port}/v1/chat/completions`);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
console.error(`failed to sync to WorkBuddy: ${String(err?.message || err)}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
package/src/cli/provider-row.js
CHANGED
|
@@ -40,12 +40,12 @@ export function buildProviderRows({ stateFile, env = process.env } = {}) {
|
|
|
40
40
|
let allowed = [];
|
|
41
41
|
let allowAny = false;
|
|
42
42
|
try { keys = loadProviderKeys(gid, stateFile ? { file: stateFile } : undefined); } catch {}
|
|
43
|
-
try { baseUrl = loadProviderBaseUrl(gid, stateFile ? { file: stateFile } : undefined) || cfg?.baseUrl || (gid === "openrouter" ? "https://openrouter.ai/api/v1" : gid === "workbuddy" ? "https://copilot.tencent.com" : gid === "traework" ? "https://trae-api-cn.mchost.guru" : gid === "qoder" ? "qoder://native" : ""); } catch { baseUrl = cfg?.baseUrl || ""; }
|
|
43
|
+
try { baseUrl = loadProviderBaseUrl(gid, stateFile ? { file: stateFile } : undefined) || cfg?.baseUrl || (gid === "openrouter" ? "https://openrouter.ai/api/v1" : gid === "workbuddy" ? "https://copilot.tencent.com" : gid === "traework" ? "https://trae-api-cn.mchost.guru" : gid === "qoder" ? "qoder://native" : gid === "qwenwork" ? "qwenwork://native" : ""); } catch { baseUrl = cfg?.baseUrl || ""; }
|
|
44
44
|
try { allowed = loadProviderAllowedModels(gid, stateFile ? { file: stateFile } : undefined); } catch {}
|
|
45
45
|
try { allowAny = loadProviderAllowAnyModels(gid, stateFile ? { file: stateFile } : undefined); } catch {}
|
|
46
46
|
// openrouter 特殊:opencode 例外默认 allowAny true,其余默认 false
|
|
47
47
|
if (gid === "opencode") allowAny = true;
|
|
48
|
-
let enabled = Boolean(baseUrl && keys.length) || (gid === "openrouter" && keys.length > 0);
|
|
48
|
+
let enabled = Boolean(baseUrl && keys.length) || (gid === "openrouter" && keys.length > 0) || ((gid === "qoder" || gid === "qwenwork" || gid === "traework") && keys.length > 0);
|
|
49
49
|
const auths = gid === "workbuddy" ? (() => { try { return loadProviderAuths(gid, stateFile ? { file: stateFile } : undefined) || []; } catch { return []; } })() : [];
|
|
50
50
|
let note = "";
|
|
51
51
|
const isWorkbuddyStub = gid === "workbuddy" && (keys.includes("k-new") || String(baseUrl).includes("127.0.0.1") || (keys.length === 1 && keys[0].length < 20));
|