mslxdff 0.1.56 → 0.1.57
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 +75 -3
- package/package.json +1 -1
- package/src/routes/chat/index.js +36 -1
- package/src/sync-opencode.js +152 -0
package/bin/mslxdff.js
CHANGED
|
@@ -12,6 +12,7 @@ import { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroups
|
|
|
12
12
|
import { getPreferredModel } from "../src/auto.js";
|
|
13
13
|
import { normalizeModel } from "../src/reasoning.js";
|
|
14
14
|
import { syncToWorkbuddy, workbuddyModelsPath } from "../src/sync-workbuddy.js";
|
|
15
|
+
import { syncToOpencode, opencodeConfigPath, toExternalAlias, toInternalId } from "../src/sync-opencode.js";
|
|
15
16
|
import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid, readPidVersion, isPidAlive } from "../src/daemon.js";
|
|
16
17
|
import { createAutoSelector } from "../src/auto.js";
|
|
17
18
|
import { createPeersService } from "../src/peers.js";
|
|
@@ -313,14 +314,85 @@ if (args.includes("-model") || args.includes("-models")) {
|
|
|
313
314
|
process.exit(0);
|
|
314
315
|
}
|
|
315
316
|
|
|
316
|
-
// -setto workbuddy [modelId]:
|
|
317
|
+
// -setto workbuddy|opencode [modelId]: sync to WorkBuddy / opencode
|
|
317
318
|
if (args.includes("-setto") || args.includes("--setto")) {
|
|
318
319
|
const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
|
|
319
320
|
const target = args[idx + 1];
|
|
320
|
-
if (
|
|
321
|
-
console.error("usage: mslxdff -setto workbuddy [modelId]");
|
|
321
|
+
if (!["workbuddy", "opencode"].includes(target)) {
|
|
322
|
+
console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId]");
|
|
322
323
|
process.exit(1);
|
|
323
324
|
}
|
|
325
|
+
if (target === "opencode") {
|
|
326
|
+
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
327
|
+
let id;
|
|
328
|
+
let internal;
|
|
329
|
+
if (raw) {
|
|
330
|
+
if (raw === "auto" || !raw) {
|
|
331
|
+
console.error("modelId 不能为 auto 或空");
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
const norm = normalizeModel(raw);
|
|
335
|
+
if (!norm) {
|
|
336
|
+
console.error("modelId 不能为空");
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
savePreferredModel(norm);
|
|
340
|
+
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
341
|
+
internal = toInternalId(norm);
|
|
342
|
+
id = toExternalAlias(internal);
|
|
343
|
+
} else {
|
|
344
|
+
const pref = loadPreferredModel() || getPreferredModel();
|
|
345
|
+
if (!pref) {
|
|
346
|
+
console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
const norm = normalizeModel(pref);
|
|
350
|
+
if (!norm) {
|
|
351
|
+
console.error("modelId 不能为空");
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
internal = toInternalId(norm);
|
|
355
|
+
id = toExternalAlias(internal);
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
const cacheFile = join(logDir(), "models.json");
|
|
359
|
+
const models = createModelsService({
|
|
360
|
+
baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
|
|
361
|
+
headers: createUpstreamClient({}).headers,
|
|
362
|
+
refreshMs: 0,
|
|
363
|
+
cacheFile,
|
|
364
|
+
});
|
|
365
|
+
const fresh = await Promise.race([
|
|
366
|
+
models.get(),
|
|
367
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
|
|
368
|
+
]);
|
|
369
|
+
if (fresh?.data?.length) {
|
|
370
|
+
const ids = fresh.data.map((m) => m.id);
|
|
371
|
+
if (!ids.includes(internal) && !ids.includes(id)) {
|
|
372
|
+
console.log(`warn: "${internal}" not in current free list (${ids.length} models), still syncing to opencode (alias ${id})`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
// refresh failed, still proceed
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const { token } = await loadToken();
|
|
380
|
+
const persisted = getPort();
|
|
381
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
382
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
383
|
+
const file = opencodeConfigPath();
|
|
384
|
+
const r = await syncToOpencode({ id, token, port, file });
|
|
385
|
+
const aliasLabel = r.alias && r.alias !== r.id ? ` (alias ${r.alias} 对应内部 ${r.internal})` : (r.id !== r.internal ? ` (alias for "${r.internal}", 原名仍兼容)` : ` (原名兼容)`);
|
|
386
|
+
console.log(`synced to opencode: ${r.action} "${r.id}"${aliasLabel} @ ${file}`);
|
|
387
|
+
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
388
|
+
if (r.id !== r.internal) console.log(` alias: ${r.id} -> ${r.internal} (opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
|
|
389
|
+
else console.log(` alias: ${r.internal} (原名直用,opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
process.exit(0);
|
|
395
|
+
}
|
|
324
396
|
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
325
397
|
let id;
|
|
326
398
|
if (raw) {
|
package/package.json
CHANGED
package/src/routes/chat/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { performance } from "node:perf_hooks";
|
|
2
2
|
import { injectReasoningContent, normalizeModel } from "../../reasoning.js";
|
|
3
3
|
import { isAutoModel } from "../../auto.js";
|
|
4
|
+
import { toInternalId as aliasToInternal } from "../../sync-opencode.js";
|
|
4
5
|
import { clientIp, json, readBody, parseHops, summarizePrompt, errMsg } from "../helpers.js";
|
|
5
6
|
import { hedgeDelayMs, shouldHedge } from "../hedge.js";
|
|
6
7
|
import { runHook } from "../../plugins.js";
|
|
@@ -39,9 +40,42 @@ export async function chatHandler({ req, res, upstream, auto, logs, peers, maxHo
|
|
|
39
40
|
const workbuddyUid = (req.headers["x-mslxdff-workbuddy-uid"] || req.headers["x-workbuddy-uid"] || "").toString().trim();
|
|
40
41
|
const lockModel = req.headers["x-mslxdff-model-lock"] || "";
|
|
41
42
|
const rawModel = body.model || "";
|
|
42
|
-
const
|
|
43
|
+
const normalizedRequested = normalizeModel(lockModel || rawModel || "");
|
|
44
|
+
// alias 还原:mslxdff-deepseek -> deepseek(原名仍兼容,双向支持 mslxdff/mslxdff-deepseek 与裸 mslxdff-deepseek/裸 deepseek)
|
|
45
|
+
let requested = normalizedRequested;
|
|
46
|
+
let aliasInfo = null;
|
|
47
|
+
if (requested.startsWith("mslxdff-")) {
|
|
48
|
+
const internal = aliasToInternal(requested);
|
|
49
|
+
if (internal) {
|
|
50
|
+
aliasInfo = `${requested} -> ${internal}`;
|
|
51
|
+
requested = internal;
|
|
52
|
+
}
|
|
53
|
+
} else if (requested.includes("/")) {
|
|
54
|
+
const slashIdx = requested.indexOf("/");
|
|
55
|
+
const rawPart = requested.slice(slashIdx + 1);
|
|
56
|
+
const providerPart = requested.slice(0, slashIdx);
|
|
57
|
+
if (rawPart.startsWith("mslxdff-")) {
|
|
58
|
+
const internal = aliasToInternal(rawPart);
|
|
59
|
+
if (internal) {
|
|
60
|
+
aliasInfo = `${requested} -> ${providerPart}/${internal} (alias stripped)`;
|
|
61
|
+
requested = `${providerPart}/${internal}`;
|
|
62
|
+
if (providerPart === "mslxdff") {
|
|
63
|
+
requested = internal;
|
|
64
|
+
aliasInfo = `${rawModel} -> ${internal} (mslxdff alias stripped)`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else if (providerPart === "mslxdff") {
|
|
68
|
+
// mslxdff/deepseek 原名直用 -> deepseek(原名兼容,provider 前缀剥离)
|
|
69
|
+
aliasInfo = `${requested} -> ${rawPart} (mslxdff provider stripped, 原名兼容)`;
|
|
70
|
+
requested = rawPart;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
43
73
|
const useAuto = isAutoModel(requested);
|
|
44
74
|
mark("parsed");
|
|
75
|
+
if (aliasInfo) {
|
|
76
|
+
// 供日志与 header 透传
|
|
77
|
+
try { res.setHeader("x-mslxdff-alias", aliasInfo); } catch {}
|
|
78
|
+
}
|
|
45
79
|
|
|
46
80
|
let order;
|
|
47
81
|
if (lockModel) {
|
|
@@ -70,6 +104,7 @@ export async function chatHandler({ req, res, upstream, auto, logs, peers, maxHo
|
|
|
70
104
|
runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(body.stream), durationMs: Date.now() - startedAt, ...info }).catch(() => {});
|
|
71
105
|
};
|
|
72
106
|
evt("request", { reqId, hops, ip: clientIp(req), stream: Boolean(body.stream), prompt: summarizePrompt(body), rawModel, requested, lockModel: lockModel || null });
|
|
107
|
+
if (aliasInfo) evt("alias", { reqId, alias: aliasInfo, rawModel, requested });
|
|
73
108
|
if (Object.keys(shareKeys).length) evt("share-keys", { reqId, providers: Object.keys(shareKeys) });
|
|
74
109
|
evt("ordered", { reqId, order, canFallback, canForwardPeers, useAuto, statuses: auto?.statuses?.() ?? null });
|
|
75
110
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
|
|
5
|
+
export function opencodeConfigPath() {
|
|
6
|
+
const env = process.env.OPENCODE_CONFIG || process.env.OPENCODE_CONFIG_PATH;
|
|
7
|
+
if (typeof env === "string" && env.trim()) return env.trim();
|
|
8
|
+
return join(os.homedir(), ".config", "opencode", "opencode.json");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function toExternalAlias(id) {
|
|
12
|
+
const s = String(id || "").trim();
|
|
13
|
+
if (!s) return "";
|
|
14
|
+
return s.startsWith("mslxdff-") ? s : `mslxdff-${s}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toInternalId(aliasOrRaw) {
|
|
18
|
+
const s = String(aliasOrRaw || "").trim();
|
|
19
|
+
if (!s) return "";
|
|
20
|
+
return s.startsWith("mslxdff-") ? s.slice("mslxdff-".length) : s;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildOpencodeProvider({ id, token, port }) {
|
|
24
|
+
const p = Number(port) || 8989;
|
|
25
|
+
const alias = toExternalAlias(id);
|
|
26
|
+
return {
|
|
27
|
+
name: "mslxdff",
|
|
28
|
+
npm: "@ai-sdk/openai-compatible",
|
|
29
|
+
options: {
|
|
30
|
+
apiKey: String(token || ""),
|
|
31
|
+
baseURL: `http://127.0.0.1:${p}/v1`,
|
|
32
|
+
},
|
|
33
|
+
models: {
|
|
34
|
+
[alias]: { name: alias },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isOpencodeLocalUrl(url) {
|
|
40
|
+
const u = String(url || "");
|
|
41
|
+
return u.includes("127.0.0.1") && u.includes("/v1");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function syncToOpencode({ id, token, port, file } = {}) {
|
|
45
|
+
const targetFile = file || opencodeConfigPath();
|
|
46
|
+
// id 可能是 alias 或原名,统一以 internal 去重、以 external 入库(原名兼容)
|
|
47
|
+
const normalizedRaw = String(id || "").trim();
|
|
48
|
+
if (!normalizedRaw) throw new Error("model id required");
|
|
49
|
+
const internal = toInternalId(normalizedRaw);
|
|
50
|
+
if (!internal) throw new Error("model id required");
|
|
51
|
+
const external = toExternalAlias(internal);
|
|
52
|
+
const cleanToken = String(token || "");
|
|
53
|
+
const p = Number(port) || 8989;
|
|
54
|
+
|
|
55
|
+
let data = null;
|
|
56
|
+
let corrupted = false;
|
|
57
|
+
let rawText = null;
|
|
58
|
+
try {
|
|
59
|
+
rawText = readFileSync(targetFile, "utf8");
|
|
60
|
+
const parsed = JSON.parse(rawText);
|
|
61
|
+
data = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (rawText !== null) {
|
|
64
|
+
corrupted = true;
|
|
65
|
+
try {
|
|
66
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
67
|
+
writeFileSync(targetFile + ".bak", rawText ?? "", "utf8");
|
|
68
|
+
} catch {}
|
|
69
|
+
data = {};
|
|
70
|
+
} else {
|
|
71
|
+
data = {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!data.provider || typeof data.provider !== "object" || Array.isArray(data.provider)) {
|
|
76
|
+
data.provider = {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const oldProvider = data.provider.mslxdff && typeof data.provider.mslxdff === "object" && !Array.isArray(data.provider.mslxdff)
|
|
80
|
+
? data.provider.mslxdff
|
|
81
|
+
: null;
|
|
82
|
+
|
|
83
|
+
let action;
|
|
84
|
+
let effectiveId = external;
|
|
85
|
+
if (oldProvider) {
|
|
86
|
+
const oldModels = oldProvider.models && typeof oldProvider.models === "object" && !Array.isArray(oldProvider.models)
|
|
87
|
+
? oldProvider.models
|
|
88
|
+
: {};
|
|
89
|
+
// 去重:internal 或 external 任一已存在即视为已存在(原名兼容,以 internal 为基准)
|
|
90
|
+
const hasExternal = Object.prototype.hasOwnProperty.call(oldModels, external);
|
|
91
|
+
const hasInternal = Object.prototype.hasOwnProperty.call(oldModels, internal);
|
|
92
|
+
const exists = hasExternal || hasInternal;
|
|
93
|
+
const nextModels = { ...oldModels };
|
|
94
|
+
if (exists) {
|
|
95
|
+
if (hasExternal) {
|
|
96
|
+
nextModels[external] = { name: external, ...(oldModels[external] && typeof oldModels[external] === "object" ? oldModels[external] : {}), name: external };
|
|
97
|
+
effectiveId = external;
|
|
98
|
+
} else if (hasInternal) {
|
|
99
|
+
// 仅原名存在:保留原名不强制迁移为 alias,视为 updated(原名兼容)
|
|
100
|
+
nextModels[internal] = { name: internal, ...(oldModels[internal] && typeof oldModels[internal] === "object" ? oldModels[internal] : {}), name: internal };
|
|
101
|
+
effectiveId = internal;
|
|
102
|
+
}
|
|
103
|
+
action = "updated";
|
|
104
|
+
} else {
|
|
105
|
+
nextModels[external] = { name: external };
|
|
106
|
+
effectiveId = external;
|
|
107
|
+
action = "inserted";
|
|
108
|
+
}
|
|
109
|
+
// 合并 provider:保留 name/npm,覆盖 options.baseURL/apiKey,合并 models
|
|
110
|
+
const nextProvider = {
|
|
111
|
+
...oldProvider,
|
|
112
|
+
name: oldProvider.name || "mslxdff",
|
|
113
|
+
npm: oldProvider.npm || "@ai-sdk/openai-compatible",
|
|
114
|
+
options: {
|
|
115
|
+
...(oldProvider.options && typeof oldProvider.options === "object" ? oldProvider.options : {}),
|
|
116
|
+
baseURL: `http://127.0.0.1:${p}/v1`,
|
|
117
|
+
apiKey: cleanToken,
|
|
118
|
+
},
|
|
119
|
+
models: nextModels,
|
|
120
|
+
};
|
|
121
|
+
// 若插入的是 alias 但原名已存在,上面已处理为不新增;否则正常
|
|
122
|
+
data.provider.mslxdff = nextProvider;
|
|
123
|
+
// 若是新插入且 external 不等于 internal,且 internal 已存在时,需避免双键,上面已处理
|
|
124
|
+
// 若是新插入 external 且 internal 不存在,正常插入
|
|
125
|
+
if (!exists) {
|
|
126
|
+
data.provider.mslxdff.models = nextModels;
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
data.provider.mslxdff = buildOpencodeProvider({ id: external, token: cleanToken, port: p });
|
|
130
|
+
action = "inserted";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 原子写
|
|
134
|
+
mkdirSync(dirname(targetFile), { recursive: true });
|
|
135
|
+
const tmp = `${targetFile}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
136
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
137
|
+
try {
|
|
138
|
+
renameSync(tmp, targetFile);
|
|
139
|
+
} catch {
|
|
140
|
+
try {
|
|
141
|
+
writeFileSync(targetFile, readFileSync(tmp, "utf8"), "utf8");
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
if (existsSync(tmp)) {
|
|
146
|
+
const { unlinkSync } = await import("node:fs");
|
|
147
|
+
unlinkSync(tmp);
|
|
148
|
+
}
|
|
149
|
+
} catch {}
|
|
150
|
+
|
|
151
|
+
return { action, file: targetFile, id: effectiveId, alias: external, internal, corrupted };
|
|
152
|
+
}
|