mslxdff 0.1.170 → 0.1.172
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 +3 -3
- package/docs/cli_help_mini.md +143 -0
- package/package.json +1 -1
- package/src/anthropic/count-tokens.js +39 -0
- package/src/anthropic/translate.js +20 -41
- 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 +131 -131
- package/src/chat-pipeline/policy.js +76 -76
- package/src/cli/commands/group.js +249 -249
- 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 +133 -133
- package/src/cli/commands/provider/models.js +145 -145
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/sync.js +310 -310
- package/src/cli/status.js +279 -279
- package/src/daemon.js +112 -112
- package/src/model-capabilities/context-lookup.js +65 -0
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/merge.js +50 -35
- package/src/model-capabilities/parse.js +70 -70
- package/src/models.js +225 -225
- package/src/providers/AGENTS.md +49 -0
- 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/codearts/concurrency.js +34 -0
- package/src/providers/codearts/index.js +11 -3
- package/src/providers/globalqwenwork/cosy.js +123 -123
- package/src/providers/globalqwenwork/crypto.js +218 -218
- package/src/providers/globalqwenwork/http.js +20 -20
- package/src/providers/globalqwenwork/payload.js +4 -2
- package/src/providers/globalqwenwork/rsa.js +54 -54
- package/src/providers/globalqwenwork/sse.js +268 -268
- package/src/providers/globalqwenwork/stream.js +130 -130
- package/src/providers/globalqwenwork/upstream.js +122 -122
- package/src/providers/keyring.js +60 -60
- package/src/providers/qoder/chat.js +203 -200
- package/src/providers/qoder/index.js +249 -249
- package/src/providers/qoder/sse.js +145 -145
- package/src/providers/qwenwork/account-store.js +133 -133
- package/src/providers/qwenwork/constants.js +67 -67
- package/src/providers/qwenwork/cosy.js +120 -120
- package/src/providers/qwenwork/crypto.js +218 -218
- package/src/providers/qwenwork/http.js +20 -20
- package/src/providers/qwenwork/index.js +327 -327
- package/src/providers/qwenwork/payload.js +144 -142
- package/src/providers/qwenwork/rsa.js +54 -54
- package/src/providers/qwenwork/sse.js +268 -268
- package/src/providers/qwenwork/stream.js +130 -130
- package/src/providers/qwenwork/upstream.js +120 -120
- package/src/providers/qwenwork.js +1 -1
- package/src/providers/raccoon/chat.js +10 -5
- package/src/providers/raccoon/const.js +4 -4
- package/src/providers/registry.js +79 -79
- package/src/providers/traework/models.js +4 -3
- package/src/providers/workbuddy/chat.js +248 -248
- package/src/providers/workbuddy/payload.js +16 -7
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/providers/zcode/chat.js +7 -0
- package/src/reasoning.js +32 -32
- package/src/responses/translate.js +4 -0
- package/src/routes/AGENTS.md +37 -0
- package/src/routes/chat/exhausted-handler.js +5 -0
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +294 -291
- package/src/routes/chat/via-route-handler.js +146 -146
- package/src/routes/hedge.js +255 -255
- package/src/routes/messages-route.js +1 -1
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +273 -273
- package/src/routes/stream-scan.js +47 -21
- package/src/routes/stream.js +385 -385
- package/src/runtime/bootstrap.js +50 -50
- package/src/runtime/provider-gate.js +36 -36
- package/src/runtime/providers-setup.js +165 -165
- package/src/server.js +69 -69
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- package/src/talk-full.js +1 -0
- package/src/talk-log.js +3 -2
- package/src/thinking-effort.js +67 -0
- 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-engine/sdk/convert.js +6 -0
- 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-responses.js +5 -0
- package/src/upstream.js +289 -289
package/src/sync-opencode.js
CHANGED
|
@@ -1,280 +1,280 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import { enrichOpencodeEntry, capsSummary as capsSummaryText } from "./model-capabilities/enrich.js";
|
|
5
|
-
|
|
6
|
-
export function opencodeConfigPath() {
|
|
7
|
-
const env = process.env.OPENCODE_CONFIG || process.env.OPENCODE_CONFIG_PATH;
|
|
8
|
-
if (typeof env === "string" && env.trim()) return env.trim();
|
|
9
|
-
return join(os.homedir(), ".config", "opencode", "opencode.json");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// legacy mslxdff- 前缀(保留做兼容剥离,新写入不再使用)
|
|
13
|
-
export function toExternalAlias(id) {
|
|
14
|
-
const s = String(id || "").trim();
|
|
15
|
-
if (!s) return "";
|
|
16
|
-
return s.startsWith("mslxdff-") ? s : `mslxdff-${s}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function toInternalId(aliasOrRaw) {
|
|
20
|
-
const s = String(aliasOrRaw || "").trim();
|
|
21
|
-
if (!s) return "";
|
|
22
|
-
return s.startsWith("mslxdff-") ? s.slice("mslxdff-".length) : s;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// 新存储键:/ → -(与 WorkBuddy 一致),裸 id 原样
|
|
26
|
-
export function toStorageKey(canonical) {
|
|
27
|
-
const s = String(canonical || "").trim();
|
|
28
|
-
if (!s) return "";
|
|
29
|
-
// 先剥 legacy 前缀
|
|
30
|
-
const internal = toInternalId(s);
|
|
31
|
-
return internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 剪枝比较键:legacy 前缀剥掉 + / → -(picks 里 slash 形态与存储 dash 形态互认)
|
|
35
|
-
export function normalizeOpencodeKey(k) {
|
|
36
|
-
const inner = toInternalId(String(k || ""));
|
|
37
|
-
return inner.includes("/") ? inner.replace(/\//g, "-") : inner;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 从存储键还原为内部 canonical(需查 alias 表,调用方用 getModelAlias)
|
|
41
|
-
export function storageKeyToCanonical(storageKey) {
|
|
42
|
-
const s = String(storageKey || "").trim();
|
|
43
|
-
if (!s) return "";
|
|
44
|
-
const internal = toInternalId(s);
|
|
45
|
-
return internal;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function buildOpencodeProvider({ id, token, port }) {
|
|
49
|
-
const p = Number(port) || 8989;
|
|
50
|
-
const internal = toInternalId(String(id || "").trim());
|
|
51
|
-
const storageKey = internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
52
|
-
const key = storageKey || toExternalAlias(id); // fallback 兼容
|
|
53
|
-
return {
|
|
54
|
-
name: "mslxdff",
|
|
55
|
-
npm: "@ai-sdk/openai-compatible",
|
|
56
|
-
options: {
|
|
57
|
-
apiKey: String(token || ""),
|
|
58
|
-
baseURL: `http://127.0.0.1:${p}/v1`,
|
|
59
|
-
},
|
|
60
|
-
models: {
|
|
61
|
-
[key]: { name: key },
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function isOpencodeLocalUrl(url) {
|
|
67
|
-
const u = String(url || "");
|
|
68
|
-
return u.includes("127.0.0.1") && u.includes("/v1");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 剪枝:删掉不在 keep(picks 口径)里的旧模型键。keep 为空/非数组时不动(向后兼容)。
|
|
72
|
-
export function pruneOpencodeModels(models, keep, currentKey) {
|
|
73
|
-
if (!Array.isArray(keep) || !keep.length || !models || typeof models !== "object") return 0;
|
|
74
|
-
const keepSet = new Set([normalizeOpencodeKey(currentKey), ...keep.map(normalizeOpencodeKey)].filter(Boolean));
|
|
75
|
-
let pruned = 0;
|
|
76
|
-
for (const k of Object.keys(models)) {
|
|
77
|
-
if (!keepSet.has(normalizeOpencodeKey(k))) { delete models[k]; pruned++; }
|
|
78
|
-
}
|
|
79
|
-
return pruned;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// 补齐:把 ensureAll(picks 口径)里缺失的键写入 models(slash → dash + alias 注册)。
|
|
83
|
-
// 已存在但为旧格式(缺 limit 能力字段)的条目也自动补注能力(upgraded 计数)。
|
|
84
|
-
// 返回 { nextModels, backfilled, upgraded };ensureAll 非数组或为空时原样返回。
|
|
85
|
-
// 注意:需在剪枝之前调用——先补 picks 缺失,再剪 picks 外旧键,结果集恰为 picks ∪ currentKey。
|
|
86
|
-
// capsSvc 透传给能力注入(undefined=全局单例;补齐的条目同样带能力)。
|
|
87
|
-
export async function ensureAllOpencodeModels(models, ensureAll, capsSvc) {
|
|
88
|
-
if (!Array.isArray(ensureAll) || !ensureAll.length || !models || typeof models !== "object") {
|
|
89
|
-
return { nextModels: models, backfilled: 0, upgraded: 0 };
|
|
90
|
-
}
|
|
91
|
-
const existing = new Set(Object.keys(models).map(normalizeOpencodeKey));
|
|
92
|
-
let backfilled = 0;
|
|
93
|
-
let upgraded = 0;
|
|
94
|
-
let aliasDirty = false;
|
|
95
|
-
for (const raw of ensureAll) {
|
|
96
|
-
const internal = toInternalId(String(raw || "").trim());
|
|
97
|
-
if (!internal || internal === "auto") continue;
|
|
98
|
-
const storageKey = internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
99
|
-
if (!storageKey) continue;
|
|
100
|
-
if (existing.has(storageKey)) {
|
|
101
|
-
// 已存在:旧格式条目(仅 name、无能力字段)或缺档位(variants,ctrl+t 推理档切换)自动重注入;
|
|
102
|
-
// enrich 幂等(字段覆盖一致),重复注入无害
|
|
103
|
-
const cur = models[storageKey];
|
|
104
|
-
if (cur && typeof cur === "object" && !Array.isArray(cur) && (cur.limit === undefined || cur.variants === undefined)) {
|
|
105
|
-
const en = await enrichOpencodeEntry(cur, internal, capsSvc);
|
|
106
|
-
models[storageKey] = en.entry;
|
|
107
|
-
if (en.caps) upgraded++;
|
|
108
|
-
}
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
const enriched = await enrichOpencodeEntry({ name: storageKey }, internal, capsSvc);
|
|
112
|
-
models[storageKey] = enriched.entry;
|
|
113
|
-
existing.add(storageKey);
|
|
114
|
-
backfilled++;
|
|
115
|
-
if (internal.includes("/") && storageKey !== internal) {
|
|
116
|
-
try {
|
|
117
|
-
const { loadModelAliases, registerModelAlias } = await import("./providers/model-id.js");
|
|
118
|
-
loadModelAliases();
|
|
119
|
-
registerModelAlias(storageKey, internal);
|
|
120
|
-
aliasDirty = true;
|
|
121
|
-
} catch {}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (aliasDirty) {
|
|
125
|
-
try {
|
|
126
|
-
const { persistModelAliases } = await import("./providers/model-id.js");
|
|
127
|
-
persistModelAliases();
|
|
128
|
-
} catch {}
|
|
129
|
-
}
|
|
130
|
-
return { nextModels: models, backfilled, upgraded };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export async function syncToOpencode({ id, token, port, file, keep, ensureAll, capabilities } = {}) {
|
|
134
|
-
const targetFile = file || opencodeConfigPath();
|
|
135
|
-
const normalizedRaw = String(id || "").trim();
|
|
136
|
-
if (!normalizedRaw) throw new Error("model id required");
|
|
137
|
-
const internal = toInternalId(normalizedRaw);
|
|
138
|
-
if (!internal) throw new Error("model id required");
|
|
139
|
-
const storageKey = internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
140
|
-
const cleanToken = String(token || "");
|
|
141
|
-
const p = Number(port) || 8989;
|
|
142
|
-
|
|
143
|
-
let data = null;
|
|
144
|
-
let corrupted = false;
|
|
145
|
-
let rawText = null;
|
|
146
|
-
try {
|
|
147
|
-
rawText = readFileSync(targetFile, "utf8");
|
|
148
|
-
const parsed = JSON.parse(rawText);
|
|
149
|
-
data = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
150
|
-
} catch (err) {
|
|
151
|
-
if (rawText !== null) {
|
|
152
|
-
corrupted = true;
|
|
153
|
-
try {
|
|
154
|
-
mkdirSync(dirname(targetFile), { recursive: true });
|
|
155
|
-
writeFileSync(targetFile + ".bak", rawText ?? "", "utf8");
|
|
156
|
-
} catch {}
|
|
157
|
-
data = {};
|
|
158
|
-
} else {
|
|
159
|
-
data = {};
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (!data.provider || typeof data.provider !== "object" || Array.isArray(data.provider)) {
|
|
164
|
-
data.provider = {};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const oldProvider = data.provider.mslxdff && typeof data.provider.mslxdff === "object" && !Array.isArray(data.provider.mslxdff)
|
|
168
|
-
? data.provider.mslxdff
|
|
169
|
-
: null;
|
|
170
|
-
|
|
171
|
-
let action;
|
|
172
|
-
let effectiveId = storageKey;
|
|
173
|
-
let pruned = 0;
|
|
174
|
-
let backfilled = 0;
|
|
175
|
-
let upgraded = 0;
|
|
176
|
-
let currentCaps = null;
|
|
177
|
-
if (oldProvider) {
|
|
178
|
-
const oldModels = oldProvider.models && typeof oldProvider.models === "object" && !Array.isArray(oldProvider.models)
|
|
179
|
-
? oldProvider.models
|
|
180
|
-
: {};
|
|
181
|
-
// 归一所有旧 key 到 storageKey 维度,判断是否已存在(兼容 mslxdff- 前缀与 / 形态)
|
|
182
|
-
const normalizeToStorage = normalizeOpencodeKey;
|
|
183
|
-
let existingKey = null;
|
|
184
|
-
for (const k of Object.keys(oldModels)) {
|
|
185
|
-
if (normalizeToStorage(k) === storageKey) { existingKey = k; break; }
|
|
186
|
-
}
|
|
187
|
-
// 也兼容直接 internal(slash)形态
|
|
188
|
-
if (!existingKey && Object.prototype.hasOwnProperty.call(oldModels, internal)) existingKey = internal;
|
|
189
|
-
if (!existingKey && Object.prototype.hasOwnProperty.call(oldModels, storageKey)) existingKey = storageKey;
|
|
190
|
-
|
|
191
|
-
const nextModels = { ...oldModels };
|
|
192
|
-
if (existingKey) {
|
|
193
|
-
// 已存在:迁移到 storageKey(新规范),清理旧的 legacy 键
|
|
194
|
-
const oldEntry = oldModels[existingKey];
|
|
195
|
-
const merged = oldEntry && typeof oldEntry === "object" && !Array.isArray(oldEntry) ? oldEntry : {};
|
|
196
|
-
// 若 existingKey !== storageKey,需要把旧键删掉,统一到 storageKey
|
|
197
|
-
if (existingKey !== storageKey) {
|
|
198
|
-
// 收集所有同逻辑的旧键一起清理
|
|
199
|
-
for (const k of Object.keys(oldModels)) {
|
|
200
|
-
if (normalizeToStorage(k) === storageKey) delete nextModels[k];
|
|
201
|
-
}
|
|
202
|
-
// 也清理 legacy mslxdff- 前缀变体
|
|
203
|
-
const legacyAlias = toExternalAlias(storageKey);
|
|
204
|
-
const legacyInternal = toExternalAlias(internal);
|
|
205
|
-
if (nextModels[legacyAlias]) delete nextModels[legacyAlias];
|
|
206
|
-
if (legacyInternal !== legacyAlias && nextModels[legacyInternal]) delete nextModels[legacyInternal];
|
|
207
|
-
}
|
|
208
|
-
const enriched = await enrichOpencodeEntry({ ...merged, name: storageKey }, internal, capabilities);
|
|
209
|
-
nextModels[storageKey] = enriched.entry;
|
|
210
|
-
currentCaps = enriched.caps;
|
|
211
|
-
effectiveId = storageKey;
|
|
212
|
-
action = "updated";
|
|
213
|
-
} else {
|
|
214
|
-
const enriched = await enrichOpencodeEntry({ name: storageKey }, internal, capabilities);
|
|
215
|
-
nextModels[storageKey] = enriched.entry;
|
|
216
|
-
currentCaps = enriched.caps;
|
|
217
|
-
effectiveId = storageKey;
|
|
218
|
-
action = "inserted";
|
|
219
|
-
}
|
|
220
|
-
const ensured = await ensureAllOpencodeModels(nextModels, ensureAll, capabilities);
|
|
221
|
-
backfilled = ensured.backfilled;
|
|
222
|
-
upgraded = ensured.upgraded || 0;
|
|
223
|
-
pruned = pruneOpencodeModels(nextModels, keep, storageKey);
|
|
224
|
-
const nextProvider = {
|
|
225
|
-
...oldProvider,
|
|
226
|
-
name: oldProvider.name || "mslxdff",
|
|
227
|
-
npm: oldProvider.npm || "@ai-sdk/openai-compatible",
|
|
228
|
-
options: {
|
|
229
|
-
...(oldProvider.options && typeof oldProvider.options === "object" ? oldProvider.options : {}),
|
|
230
|
-
baseURL: `http://127.0.0.1:${p}/v1`,
|
|
231
|
-
apiKey: cleanToken,
|
|
232
|
-
},
|
|
233
|
-
models: nextModels,
|
|
234
|
-
};
|
|
235
|
-
data.provider.mslxdff = nextProvider;
|
|
236
|
-
} else {
|
|
237
|
-
data.provider.mslxdff = buildOpencodeProvider({ id: storageKey, token: cleanToken, port: p });
|
|
238
|
-
// buildOpencodeProvider 已用 storageKey,这里再确保
|
|
239
|
-
if (!data.provider.mslxdff.models[storageKey]) {
|
|
240
|
-
data.provider.mslxdff.models = { [storageKey]: { name: storageKey } };
|
|
241
|
-
}
|
|
242
|
-
const enrichedNew = await enrichOpencodeEntry(data.provider.mslxdff.models[storageKey], internal, capabilities);
|
|
243
|
-
data.provider.mslxdff.models[storageKey] = enrichedNew.entry;
|
|
244
|
-
currentCaps = enrichedNew.caps;
|
|
245
|
-
const ensured = await ensureAllOpencodeModels(data.provider.mslxdff.models, ensureAll, capabilities);
|
|
246
|
-
backfilled = ensured.backfilled;
|
|
247
|
-
upgraded = ensured.upgraded || 0;
|
|
248
|
-
action = "inserted";
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// 若内部含 /,注册 dash→slash 别名,供网关 mslxdff 侧自动映射(与 WorkBuddy 同表)
|
|
252
|
-
if (internal.includes("/") && storageKey !== internal) {
|
|
253
|
-
try {
|
|
254
|
-
const { loadModelAliases, registerModelAlias, persistModelAliases } = await import("./providers/model-id.js");
|
|
255
|
-
loadModelAliases();
|
|
256
|
-
registerModelAlias(storageKey, internal);
|
|
257
|
-
persistModelAliases();
|
|
258
|
-
} catch {}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// 原子写
|
|
262
|
-
mkdirSync(dirname(targetFile), { recursive: true });
|
|
263
|
-
const tmp = `${targetFile}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
264
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
265
|
-
try {
|
|
266
|
-
renameSync(tmp, targetFile);
|
|
267
|
-
} catch {
|
|
268
|
-
try {
|
|
269
|
-
writeFileSync(targetFile, readFileSync(tmp, "utf8"), "utf8");
|
|
270
|
-
} catch {}
|
|
271
|
-
}
|
|
272
|
-
try {
|
|
273
|
-
if (existsSync(tmp)) {
|
|
274
|
-
const { unlinkSync } = await import("node:fs");
|
|
275
|
-
unlinkSync(tmp);
|
|
276
|
-
}
|
|
277
|
-
} catch {}
|
|
278
|
-
|
|
279
|
-
return { action, file: targetFile, id: effectiveId, alias: storageKey, internal, corrupted, storageKey, pruned, backfilled, upgraded, caps: currentCaps, capsSummaryText: capsSummaryText(currentCaps) };
|
|
280
|
-
}
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { enrichOpencodeEntry, capsSummary as capsSummaryText } from "./model-capabilities/enrich.js";
|
|
5
|
+
|
|
6
|
+
export function opencodeConfigPath() {
|
|
7
|
+
const env = process.env.OPENCODE_CONFIG || process.env.OPENCODE_CONFIG_PATH;
|
|
8
|
+
if (typeof env === "string" && env.trim()) return env.trim();
|
|
9
|
+
return join(os.homedir(), ".config", "opencode", "opencode.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// legacy mslxdff- 前缀(保留做兼容剥离,新写入不再使用)
|
|
13
|
+
export function toExternalAlias(id) {
|
|
14
|
+
const s = String(id || "").trim();
|
|
15
|
+
if (!s) return "";
|
|
16
|
+
return s.startsWith("mslxdff-") ? s : `mslxdff-${s}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toInternalId(aliasOrRaw) {
|
|
20
|
+
const s = String(aliasOrRaw || "").trim();
|
|
21
|
+
if (!s) return "";
|
|
22
|
+
return s.startsWith("mslxdff-") ? s.slice("mslxdff-".length) : s;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 新存储键:/ → -(与 WorkBuddy 一致),裸 id 原样
|
|
26
|
+
export function toStorageKey(canonical) {
|
|
27
|
+
const s = String(canonical || "").trim();
|
|
28
|
+
if (!s) return "";
|
|
29
|
+
// 先剥 legacy 前缀
|
|
30
|
+
const internal = toInternalId(s);
|
|
31
|
+
return internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 剪枝比较键:legacy 前缀剥掉 + / → -(picks 里 slash 形态与存储 dash 形态互认)
|
|
35
|
+
export function normalizeOpencodeKey(k) {
|
|
36
|
+
const inner = toInternalId(String(k || ""));
|
|
37
|
+
return inner.includes("/") ? inner.replace(/\//g, "-") : inner;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 从存储键还原为内部 canonical(需查 alias 表,调用方用 getModelAlias)
|
|
41
|
+
export function storageKeyToCanonical(storageKey) {
|
|
42
|
+
const s = String(storageKey || "").trim();
|
|
43
|
+
if (!s) return "";
|
|
44
|
+
const internal = toInternalId(s);
|
|
45
|
+
return internal;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildOpencodeProvider({ id, token, port }) {
|
|
49
|
+
const p = Number(port) || 8989;
|
|
50
|
+
const internal = toInternalId(String(id || "").trim());
|
|
51
|
+
const storageKey = internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
52
|
+
const key = storageKey || toExternalAlias(id); // fallback 兼容
|
|
53
|
+
return {
|
|
54
|
+
name: "mslxdff",
|
|
55
|
+
npm: "@ai-sdk/openai-compatible",
|
|
56
|
+
options: {
|
|
57
|
+
apiKey: String(token || ""),
|
|
58
|
+
baseURL: `http://127.0.0.1:${p}/v1`,
|
|
59
|
+
},
|
|
60
|
+
models: {
|
|
61
|
+
[key]: { name: key },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isOpencodeLocalUrl(url) {
|
|
67
|
+
const u = String(url || "");
|
|
68
|
+
return u.includes("127.0.0.1") && u.includes("/v1");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 剪枝:删掉不在 keep(picks 口径)里的旧模型键。keep 为空/非数组时不动(向后兼容)。
|
|
72
|
+
export function pruneOpencodeModels(models, keep, currentKey) {
|
|
73
|
+
if (!Array.isArray(keep) || !keep.length || !models || typeof models !== "object") return 0;
|
|
74
|
+
const keepSet = new Set([normalizeOpencodeKey(currentKey), ...keep.map(normalizeOpencodeKey)].filter(Boolean));
|
|
75
|
+
let pruned = 0;
|
|
76
|
+
for (const k of Object.keys(models)) {
|
|
77
|
+
if (!keepSet.has(normalizeOpencodeKey(k))) { delete models[k]; pruned++; }
|
|
78
|
+
}
|
|
79
|
+
return pruned;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 补齐:把 ensureAll(picks 口径)里缺失的键写入 models(slash → dash + alias 注册)。
|
|
83
|
+
// 已存在但为旧格式(缺 limit 能力字段)的条目也自动补注能力(upgraded 计数)。
|
|
84
|
+
// 返回 { nextModels, backfilled, upgraded };ensureAll 非数组或为空时原样返回。
|
|
85
|
+
// 注意:需在剪枝之前调用——先补 picks 缺失,再剪 picks 外旧键,结果集恰为 picks ∪ currentKey。
|
|
86
|
+
// capsSvc 透传给能力注入(undefined=全局单例;补齐的条目同样带能力)。
|
|
87
|
+
export async function ensureAllOpencodeModels(models, ensureAll, capsSvc) {
|
|
88
|
+
if (!Array.isArray(ensureAll) || !ensureAll.length || !models || typeof models !== "object") {
|
|
89
|
+
return { nextModels: models, backfilled: 0, upgraded: 0 };
|
|
90
|
+
}
|
|
91
|
+
const existing = new Set(Object.keys(models).map(normalizeOpencodeKey));
|
|
92
|
+
let backfilled = 0;
|
|
93
|
+
let upgraded = 0;
|
|
94
|
+
let aliasDirty = false;
|
|
95
|
+
for (const raw of ensureAll) {
|
|
96
|
+
const internal = toInternalId(String(raw || "").trim());
|
|
97
|
+
if (!internal || internal === "auto") continue;
|
|
98
|
+
const storageKey = internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
99
|
+
if (!storageKey) continue;
|
|
100
|
+
if (existing.has(storageKey)) {
|
|
101
|
+
// 已存在:旧格式条目(仅 name、无能力字段)或缺档位(variants,ctrl+t 推理档切换)自动重注入;
|
|
102
|
+
// enrich 幂等(字段覆盖一致),重复注入无害
|
|
103
|
+
const cur = models[storageKey];
|
|
104
|
+
if (cur && typeof cur === "object" && !Array.isArray(cur) && (cur.limit === undefined || cur.variants === undefined)) {
|
|
105
|
+
const en = await enrichOpencodeEntry(cur, internal, capsSvc);
|
|
106
|
+
models[storageKey] = en.entry;
|
|
107
|
+
if (en.caps) upgraded++;
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const enriched = await enrichOpencodeEntry({ name: storageKey }, internal, capsSvc);
|
|
112
|
+
models[storageKey] = enriched.entry;
|
|
113
|
+
existing.add(storageKey);
|
|
114
|
+
backfilled++;
|
|
115
|
+
if (internal.includes("/") && storageKey !== internal) {
|
|
116
|
+
try {
|
|
117
|
+
const { loadModelAliases, registerModelAlias } = await import("./providers/model-id.js");
|
|
118
|
+
loadModelAliases();
|
|
119
|
+
registerModelAlias(storageKey, internal);
|
|
120
|
+
aliasDirty = true;
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (aliasDirty) {
|
|
125
|
+
try {
|
|
126
|
+
const { persistModelAliases } = await import("./providers/model-id.js");
|
|
127
|
+
persistModelAliases();
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
return { nextModels: models, backfilled, upgraded };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function syncToOpencode({ id, token, port, file, keep, ensureAll, capabilities } = {}) {
|
|
134
|
+
const targetFile = file || opencodeConfigPath();
|
|
135
|
+
const normalizedRaw = String(id || "").trim();
|
|
136
|
+
if (!normalizedRaw) throw new Error("model id required");
|
|
137
|
+
const internal = toInternalId(normalizedRaw);
|
|
138
|
+
if (!internal) throw new Error("model id required");
|
|
139
|
+
const storageKey = internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
140
|
+
const cleanToken = String(token || "");
|
|
141
|
+
const p = Number(port) || 8989;
|
|
142
|
+
|
|
143
|
+
let data = null;
|
|
144
|
+
let corrupted = false;
|
|
145
|
+
let rawText = null;
|
|
146
|
+
try {
|
|
147
|
+
rawText = readFileSync(targetFile, "utf8");
|
|
148
|
+
const parsed = JSON.parse(rawText);
|
|
149
|
+
data = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (rawText !== null) {
|
|
152
|
+
corrupted = true;
|
|
153
|
+
try {
|
|
154
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
155
|
+
writeFileSync(targetFile + ".bak", rawText ?? "", "utf8");
|
|
156
|
+
} catch {}
|
|
157
|
+
data = {};
|
|
158
|
+
} else {
|
|
159
|
+
data = {};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!data.provider || typeof data.provider !== "object" || Array.isArray(data.provider)) {
|
|
164
|
+
data.provider = {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const oldProvider = data.provider.mslxdff && typeof data.provider.mslxdff === "object" && !Array.isArray(data.provider.mslxdff)
|
|
168
|
+
? data.provider.mslxdff
|
|
169
|
+
: null;
|
|
170
|
+
|
|
171
|
+
let action;
|
|
172
|
+
let effectiveId = storageKey;
|
|
173
|
+
let pruned = 0;
|
|
174
|
+
let backfilled = 0;
|
|
175
|
+
let upgraded = 0;
|
|
176
|
+
let currentCaps = null;
|
|
177
|
+
if (oldProvider) {
|
|
178
|
+
const oldModels = oldProvider.models && typeof oldProvider.models === "object" && !Array.isArray(oldProvider.models)
|
|
179
|
+
? oldProvider.models
|
|
180
|
+
: {};
|
|
181
|
+
// 归一所有旧 key 到 storageKey 维度,判断是否已存在(兼容 mslxdff- 前缀与 / 形态)
|
|
182
|
+
const normalizeToStorage = normalizeOpencodeKey;
|
|
183
|
+
let existingKey = null;
|
|
184
|
+
for (const k of Object.keys(oldModels)) {
|
|
185
|
+
if (normalizeToStorage(k) === storageKey) { existingKey = k; break; }
|
|
186
|
+
}
|
|
187
|
+
// 也兼容直接 internal(slash)形态
|
|
188
|
+
if (!existingKey && Object.prototype.hasOwnProperty.call(oldModels, internal)) existingKey = internal;
|
|
189
|
+
if (!existingKey && Object.prototype.hasOwnProperty.call(oldModels, storageKey)) existingKey = storageKey;
|
|
190
|
+
|
|
191
|
+
const nextModels = { ...oldModels };
|
|
192
|
+
if (existingKey) {
|
|
193
|
+
// 已存在:迁移到 storageKey(新规范),清理旧的 legacy 键
|
|
194
|
+
const oldEntry = oldModels[existingKey];
|
|
195
|
+
const merged = oldEntry && typeof oldEntry === "object" && !Array.isArray(oldEntry) ? oldEntry : {};
|
|
196
|
+
// 若 existingKey !== storageKey,需要把旧键删掉,统一到 storageKey
|
|
197
|
+
if (existingKey !== storageKey) {
|
|
198
|
+
// 收集所有同逻辑的旧键一起清理
|
|
199
|
+
for (const k of Object.keys(oldModels)) {
|
|
200
|
+
if (normalizeToStorage(k) === storageKey) delete nextModels[k];
|
|
201
|
+
}
|
|
202
|
+
// 也清理 legacy mslxdff- 前缀变体
|
|
203
|
+
const legacyAlias = toExternalAlias(storageKey);
|
|
204
|
+
const legacyInternal = toExternalAlias(internal);
|
|
205
|
+
if (nextModels[legacyAlias]) delete nextModels[legacyAlias];
|
|
206
|
+
if (legacyInternal !== legacyAlias && nextModels[legacyInternal]) delete nextModels[legacyInternal];
|
|
207
|
+
}
|
|
208
|
+
const enriched = await enrichOpencodeEntry({ ...merged, name: storageKey }, internal, capabilities);
|
|
209
|
+
nextModels[storageKey] = enriched.entry;
|
|
210
|
+
currentCaps = enriched.caps;
|
|
211
|
+
effectiveId = storageKey;
|
|
212
|
+
action = "updated";
|
|
213
|
+
} else {
|
|
214
|
+
const enriched = await enrichOpencodeEntry({ name: storageKey }, internal, capabilities);
|
|
215
|
+
nextModels[storageKey] = enriched.entry;
|
|
216
|
+
currentCaps = enriched.caps;
|
|
217
|
+
effectiveId = storageKey;
|
|
218
|
+
action = "inserted";
|
|
219
|
+
}
|
|
220
|
+
const ensured = await ensureAllOpencodeModels(nextModels, ensureAll, capabilities);
|
|
221
|
+
backfilled = ensured.backfilled;
|
|
222
|
+
upgraded = ensured.upgraded || 0;
|
|
223
|
+
pruned = pruneOpencodeModels(nextModels, keep, storageKey);
|
|
224
|
+
const nextProvider = {
|
|
225
|
+
...oldProvider,
|
|
226
|
+
name: oldProvider.name || "mslxdff",
|
|
227
|
+
npm: oldProvider.npm || "@ai-sdk/openai-compatible",
|
|
228
|
+
options: {
|
|
229
|
+
...(oldProvider.options && typeof oldProvider.options === "object" ? oldProvider.options : {}),
|
|
230
|
+
baseURL: `http://127.0.0.1:${p}/v1`,
|
|
231
|
+
apiKey: cleanToken,
|
|
232
|
+
},
|
|
233
|
+
models: nextModels,
|
|
234
|
+
};
|
|
235
|
+
data.provider.mslxdff = nextProvider;
|
|
236
|
+
} else {
|
|
237
|
+
data.provider.mslxdff = buildOpencodeProvider({ id: storageKey, token: cleanToken, port: p });
|
|
238
|
+
// buildOpencodeProvider 已用 storageKey,这里再确保
|
|
239
|
+
if (!data.provider.mslxdff.models[storageKey]) {
|
|
240
|
+
data.provider.mslxdff.models = { [storageKey]: { name: storageKey } };
|
|
241
|
+
}
|
|
242
|
+
const enrichedNew = await enrichOpencodeEntry(data.provider.mslxdff.models[storageKey], internal, capabilities);
|
|
243
|
+
data.provider.mslxdff.models[storageKey] = enrichedNew.entry;
|
|
244
|
+
currentCaps = enrichedNew.caps;
|
|
245
|
+
const ensured = await ensureAllOpencodeModels(data.provider.mslxdff.models, ensureAll, capabilities);
|
|
246
|
+
backfilled = ensured.backfilled;
|
|
247
|
+
upgraded = ensured.upgraded || 0;
|
|
248
|
+
action = "inserted";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 若内部含 /,注册 dash→slash 别名,供网关 mslxdff 侧自动映射(与 WorkBuddy 同表)
|
|
252
|
+
if (internal.includes("/") && storageKey !== internal) {
|
|
253
|
+
try {
|
|
254
|
+
const { loadModelAliases, registerModelAlias, persistModelAliases } = await import("./providers/model-id.js");
|
|
255
|
+
loadModelAliases();
|
|
256
|
+
registerModelAlias(storageKey, internal);
|
|
257
|
+
persistModelAliases();
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 原子写
|
|
262
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
263
|
+
const tmp = `${targetFile}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
264
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
265
|
+
try {
|
|
266
|
+
renameSync(tmp, targetFile);
|
|
267
|
+
} catch {
|
|
268
|
+
try {
|
|
269
|
+
writeFileSync(targetFile, readFileSync(tmp, "utf8"), "utf8");
|
|
270
|
+
} catch {}
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
if (existsSync(tmp)) {
|
|
274
|
+
const { unlinkSync } = await import("node:fs");
|
|
275
|
+
unlinkSync(tmp);
|
|
276
|
+
}
|
|
277
|
+
} catch {}
|
|
278
|
+
|
|
279
|
+
return { action, file: targetFile, id: effectiveId, alias: storageKey, internal, corrupted, storageKey, pruned, backfilled, upgraded, caps: currentCaps, capsSummaryText: capsSummaryText(currentCaps) };
|
|
280
|
+
}
|
package/src/talk-full.js
CHANGED
|
@@ -120,6 +120,7 @@ export function recordAgentLoop({ reqId, model, via, hops, body, out, sessionKey
|
|
|
120
120
|
responseChars: Number.isFinite(Number(talk.n)) ? Number(talk.n) : content.length, // 桶的 n 显式写进读数
|
|
121
121
|
reasoningChars: reasoning.length, // 思考与正文分列,不互相吞并
|
|
122
122
|
truncated: Boolean(talk.capped), // 撞顶必须看得见
|
|
123
|
+
droppedFrames: Number(talk.dropped) || 0, // 不可逆丢弃帧显式计数(行缓冲切点内多字节字符/超限半行):0=完整(openspec fix-talk-capture-frame-loss review P1-1)
|
|
123
124
|
cap: talkCapChars(),
|
|
124
125
|
relayMs: elapsedOf(relayMs, out?.totalMs), // relay 内部计时恒写:与 elapsedMs(尝试墙钟)分列,「上游多久 vs 写给客户端多久」可各自判读
|
|
125
126
|
},
|
package/src/talk-log.js
CHANGED
|
@@ -95,7 +95,7 @@ function pushBlock(out, label, text, cap) {
|
|
|
95
95
|
const TRUNC_NOTE = "[!] 本轮已撞捕获上限(truncated=1):思考/正文后半段未落盘,内容可能不完整 —— 别当成模型的完整回答";
|
|
96
96
|
|
|
97
97
|
/** 一条对话 = 一个可读块:头部一行元信息(ts= 是环形淘汰的唯一依据),正文三段。elapsed= 取本次尝试墙钟(design D3),finish=/relayMs= 为 ADR-0046 新读数,api= 为入站协议门短码(认不出的路 = other;未拿到判定值则整段省略,绝不伪造)。 */
|
|
98
|
-
export function formatTalkEntry({ reqId, api, model, via, hops, status, stream, elapsedMs, question, thinking, answer, tools, usage, upstream, account, pick, ts, talkCapped = false, talkChars, talkCap, finishReason, relayMs, ttftMs } = {}) {
|
|
98
|
+
export function formatTalkEntry({ reqId, api, model, via, hops, status, stream, elapsedMs, question, thinking, answer, tools, usage, upstream, account, pick, ts, talkCapped = false, talkChars, talkCap, talkDropped, finishReason, relayMs, ttftMs } = {}) {
|
|
99
99
|
const fval = finishReason || "-"; // 缺值占位(grill Q5):「没给 finish」不得被读成 stop;字段恒在场
|
|
100
100
|
const now = Number.isFinite(ts) && ts > 0 ? ts : Date.now();
|
|
101
101
|
const trunc = talkCapped === true;
|
|
@@ -113,6 +113,7 @@ export function formatTalkEntry({ reqId, api, model, via, hops, status, stream,
|
|
|
113
113
|
upstream ? `upstream=${upstream}` : "", account ? `account=${account}` : "", pick ? `pick=${pick}` : "",
|
|
114
114
|
// 截断读数只在真截断时出现(未截断不得新增噪音字段):cap= 上限、chars= 已捕获字符数
|
|
115
115
|
trunc ? `truncated=1 cap=${Number.isFinite(talkCap) && talkCap > 0 ? talkCap : "-"} chars=${Number.isFinite(talkChars) ? talkChars : "-"}` : "",
|
|
116
|
+
Number(talkDropped) > 0 ? `dropped=${Math.round(Number(talkDropped))}` : "", // 不可逆丢弃帧计数:只在真丢时出现(与 truncated 同「未丢不添噪音」口径,review P1-1)
|
|
116
117
|
].filter(Boolean).join(" ");
|
|
117
118
|
const out = [head];
|
|
118
119
|
if (trunc) out.push(TRUNC_NOTE);
|
|
@@ -222,7 +223,7 @@ export function recordRelayTalk({ reqId, api, model, via, hops, body, out, echo
|
|
|
222
223
|
elapsedMs: Number.isFinite(Number(attemptMs)) ? Number(attemptMs) : out?.totalMs, // 新口径:本次尝试墙钟优先(design D3)
|
|
223
224
|
relayMs: out?.totalMs, ttftMs, question: lastUserQuestion(body || {}),
|
|
224
225
|
thinking, answer, tools, usage: out?.detail?.usage, ...echo, ts,
|
|
225
|
-
talkCapped: talk?.capped === true, talkChars: talk?.n, talkCap: talk?.cap,
|
|
226
|
+
talkCapped: talk?.capped === true, talkChars: talk?.n, talkCap: talk?.cap, talkDropped: Number(talk?.dropped) || 0,
|
|
226
227
|
});
|
|
227
228
|
appendTalkEntry({ file: talkLogFile(model), block, atMs: ts });
|
|
228
229
|
return true;
|