mslxdff 0.1.86 → 0.1.87
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/package.json +1 -1
- package/src/cli/commands/sync.js +46 -14
- package/src/routes/chat/gateway.js +12 -18
- package/src/sync-opencode.js +72 -28
package/package.json
CHANGED
package/src/cli/commands/sync.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import { loadToken, getPort, savePreferredModel, loadPreferredModel } from "../../state.js";
|
|
2
|
+
import { loadToken, getPort, savePreferredModel, loadPreferredModel, loadModelPicks } from "../../state.js";
|
|
3
3
|
import { getPreferredModel as getPref } from "../../auto.js";
|
|
4
4
|
import { normalizeModel } from "../../reasoning.js";
|
|
5
5
|
import { syncToWorkbuddy, workbuddyModelsPath } from "../../sync-workbuddy.js";
|
|
6
|
-
import { syncToOpencode, opencodeConfigPath
|
|
6
|
+
import { syncToOpencode, opencodeConfigPath } from "../../sync-opencode.js";
|
|
7
7
|
import { createModelsService } from "../../models.js";
|
|
8
8
|
import { createUpstreamClient } from "../../upstream.js";
|
|
9
9
|
import { logDir } from "../../logs.js";
|
|
@@ -13,13 +13,46 @@ export async function handleSetto(args) {
|
|
|
13
13
|
const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
|
|
14
14
|
const target = args[idx + 1];
|
|
15
15
|
if (!["workbuddy", "opencode"].includes(target)) {
|
|
16
|
-
console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId]");
|
|
16
|
+
console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId|--all]");
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
19
|
if (target === "opencode") {
|
|
20
|
+
const wantsAll = args.includes("--all") || args.includes("-a") || args[idx + 2] === "all";
|
|
21
|
+
if (wantsAll) {
|
|
22
|
+
const picks = loadModelPicks();
|
|
23
|
+
const list = picks.length ? picks : [loadPreferredModel() || getPref()].filter(Boolean);
|
|
24
|
+
if (!list.length) {
|
|
25
|
+
console.error("no picks and no preferred model; use: mslxdff -setto opencode <modelId>");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const { token } = await loadToken();
|
|
30
|
+
const persisted = getPort();
|
|
31
|
+
const envPort = Number(process.env.MSLXDFF_PORT);
|
|
32
|
+
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
33
|
+
const file = opencodeConfigPath();
|
|
34
|
+
let inserted = 0, updated = 0;
|
|
35
|
+
for (const rawId of list) {
|
|
36
|
+
const norm = normalizeModel(rawId);
|
|
37
|
+
if (!norm || norm === "auto") continue;
|
|
38
|
+
// 首次循环也同步 preferred(保持 daemon 热重载语义)
|
|
39
|
+
if (rawId === list[0]) savePreferredModel(norm);
|
|
40
|
+
const r = await syncToOpencode({ id: norm, token, port, file });
|
|
41
|
+
if (r.action === "inserted") inserted++; else updated++;
|
|
42
|
+
console.log(` ${r.action} "${r.id}" -> ${r.internal} @ ${file}`);
|
|
43
|
+
}
|
|
44
|
+
console.log(`synced to opencode: ${inserted} inserted, ${updated} updated, total ${list.length} @ ${file}`);
|
|
45
|
+
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
46
|
+
console.log(` models: ${list.map((x) => normalizeModel(x)).join(", ")}`);
|
|
47
|
+
console.log(` opencode 选 mslxdff/<model> 直达本地,同名如 mslxdff/deepseek-v4-flash-free 或 mslxdff/bai-deepseek-v4-flash`);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
20
54
|
const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
|
|
21
55
|
let id;
|
|
22
|
-
let internal;
|
|
23
56
|
if (raw) {
|
|
24
57
|
if (raw === "auto" || !raw) {
|
|
25
58
|
console.error("modelId 不能为 auto 或空");
|
|
@@ -32,8 +65,7 @@ export async function handleSetto(args) {
|
|
|
32
65
|
}
|
|
33
66
|
savePreferredModel(norm);
|
|
34
67
|
console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
|
|
35
|
-
|
|
36
|
-
id = toExternalAlias(internal);
|
|
68
|
+
id = norm;
|
|
37
69
|
} else {
|
|
38
70
|
const pref = loadPreferredModel() || getPref();
|
|
39
71
|
if (!pref) {
|
|
@@ -45,9 +77,9 @@ export async function handleSetto(args) {
|
|
|
45
77
|
console.error("modelId 不能为空");
|
|
46
78
|
process.exit(1);
|
|
47
79
|
}
|
|
48
|
-
|
|
49
|
-
id = toExternalAlias(internal);
|
|
80
|
+
id = norm;
|
|
50
81
|
}
|
|
82
|
+
// 可选:校验是否在 free 列表
|
|
51
83
|
try {
|
|
52
84
|
const cacheFile = join(logDir(), "models.json");
|
|
53
85
|
const models = createModelsService({
|
|
@@ -62,8 +94,10 @@ export async function handleSetto(args) {
|
|
|
62
94
|
]);
|
|
63
95
|
if (fresh?.data?.length) {
|
|
64
96
|
const ids = fresh.data.map((m) => m.id);
|
|
65
|
-
|
|
66
|
-
|
|
97
|
+
// 对 slash 形态也做 dash 兼容检查
|
|
98
|
+
const dashId = id.includes("/") ? id.replace(/\//g, "-") : id;
|
|
99
|
+
if (!ids.includes(id) && !ids.includes(dashId)) {
|
|
100
|
+
console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to opencode`);
|
|
67
101
|
}
|
|
68
102
|
}
|
|
69
103
|
} catch {}
|
|
@@ -74,11 +108,9 @@ export async function handleSetto(args) {
|
|
|
74
108
|
const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
|
|
75
109
|
const file = opencodeConfigPath();
|
|
76
110
|
const r = await syncToOpencode({ id, token, port, file });
|
|
77
|
-
|
|
78
|
-
console.log(`synced to opencode: ${r.action} "${r.id}"${aliasLabel} @ ${file}`);
|
|
111
|
+
console.log(`synced to opencode: ${r.action} "${r.id}" @ ${file}`);
|
|
79
112
|
console.log(` url: http://127.0.0.1:${port}/v1`);
|
|
80
|
-
|
|
81
|
-
else console.log(` alias: ${r.internal} (原名直用,opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
|
|
113
|
+
console.log(` opencode 选 mslxdff/${r.id} 直达本地 ${r.internal}${r.storageKey !== r.internal ? ` (dash→${r.internal} 自动映射)` : ""}`);
|
|
82
114
|
} catch (err) {
|
|
83
115
|
console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
|
|
84
116
|
process.exit(1);
|
|
@@ -1,7 +1,6 @@
|
|
|
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";
|
|
5
4
|
import { clientIp, json, readBody, parseHops, summarizePrompt, errMsg } from "../helpers.js";
|
|
6
5
|
import { hedgeDelayMs, shouldHedge } from "../hedge.js";
|
|
7
6
|
import { runHook } from "../../plugins.js";
|
|
@@ -53,25 +52,20 @@ export function createChatGateway({ upstream, auto, logs, peers, maxHops, groups
|
|
|
53
52
|
if (aliasResolved) { normalizedRequested = aliasResolved; body = { ...body, model: aliasResolved }; }
|
|
54
53
|
let requested = normalizedRequested;
|
|
55
54
|
let aliasInfo = null;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
requested = `${providerPart}/${internal}`;
|
|
68
|
-
if (providerPart === "mslxdff") { requested = internal; aliasInfo = `${rawModel} -> ${internal} (mslxdff alias stripped)`; }
|
|
69
|
-
}
|
|
70
|
-
} else if (providerPart === "mslxdff") {
|
|
71
|
-
aliasInfo = `${requested} -> ${rawPart} (mslxdff provider stripped, 原名兼容)`;
|
|
72
|
-
requested = rawPart;
|
|
55
|
+
// opencode 侧 provider.mslxdff 模型会以 mslxdff/<id> 形式到达,剥掉前缀即得真实模型
|
|
56
|
+
if (requested.startsWith("mslxdff/")) {
|
|
57
|
+
const rawPart = requested.slice("mslxdff/".length);
|
|
58
|
+
aliasInfo = `${requested} -> ${rawPart} (mslxdff provider stripped)`;
|
|
59
|
+
requested = rawPart;
|
|
60
|
+
// mslxdff/bai-deepseek... 这类 dash 形态二次走 alias 表还原为 bai/...
|
|
61
|
+
const alias2 = getModelAlias(requested);
|
|
62
|
+
if (alias2) {
|
|
63
|
+
aliasInfo = `${rawModel} -> ${alias2} (mslxdff + alias)`;
|
|
64
|
+
requested = alias2;
|
|
65
|
+
body = { ...body, model: alias2 };
|
|
73
66
|
}
|
|
74
67
|
}
|
|
68
|
+
// 非 mslxdff 前缀的 dash 形态(如 bai-deepseek)已在首轮 aliasResolved 处理
|
|
75
69
|
const useAuto = isAutoModel(requested);
|
|
76
70
|
mark("parsed");
|
|
77
71
|
if (aliasInfo) { try { res.setHeader("x-mslxdff-alias", aliasInfo); } catch {} }
|
package/src/sync-opencode.js
CHANGED
|
@@ -8,6 +8,7 @@ export function opencodeConfigPath() {
|
|
|
8
8
|
return join(os.homedir(), ".config", "opencode", "opencode.json");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// legacy mslxdff- 前缀(保留做兼容剥离,新写入不再使用)
|
|
11
12
|
export function toExternalAlias(id) {
|
|
12
13
|
const s = String(id || "").trim();
|
|
13
14
|
if (!s) return "";
|
|
@@ -20,9 +21,28 @@ export function toInternalId(aliasOrRaw) {
|
|
|
20
21
|
return s.startsWith("mslxdff-") ? s.slice("mslxdff-".length) : s;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
// 新存储键:/ → -(与 WorkBuddy 一致),裸 id 原样
|
|
25
|
+
export function toStorageKey(canonical) {
|
|
26
|
+
const s = String(canonical || "").trim();
|
|
27
|
+
if (!s) return "";
|
|
28
|
+
// 先剥 legacy 前缀
|
|
29
|
+
const internal = toInternalId(s);
|
|
30
|
+
return internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 从存储键还原为内部 canonical(需查 alias 表,调用方用 getModelAlias)
|
|
34
|
+
export function storageKeyToCanonical(storageKey) {
|
|
35
|
+
const s = String(storageKey || "").trim();
|
|
36
|
+
if (!s) return "";
|
|
37
|
+
const internal = toInternalId(s);
|
|
38
|
+
return internal;
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
export function buildOpencodeProvider({ id, token, port }) {
|
|
24
42
|
const p = Number(port) || 8989;
|
|
25
|
-
const
|
|
43
|
+
const internal = toInternalId(String(id || "").trim());
|
|
44
|
+
const storageKey = internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
45
|
+
const key = storageKey || toExternalAlias(id); // fallback 兼容
|
|
26
46
|
return {
|
|
27
47
|
name: "mslxdff",
|
|
28
48
|
npm: "@ai-sdk/openai-compatible",
|
|
@@ -31,7 +51,7 @@ export function buildOpencodeProvider({ id, token, port }) {
|
|
|
31
51
|
baseURL: `http://127.0.0.1:${p}/v1`,
|
|
32
52
|
},
|
|
33
53
|
models: {
|
|
34
|
-
[
|
|
54
|
+
[key]: { name: key },
|
|
35
55
|
},
|
|
36
56
|
};
|
|
37
57
|
}
|
|
@@ -43,12 +63,11 @@ export function isOpencodeLocalUrl(url) {
|
|
|
43
63
|
|
|
44
64
|
export async function syncToOpencode({ id, token, port, file } = {}) {
|
|
45
65
|
const targetFile = file || opencodeConfigPath();
|
|
46
|
-
// id 可能是 alias 或原名,统一以 internal 去重、以 external 入库(原名兼容)
|
|
47
66
|
const normalizedRaw = String(id || "").trim();
|
|
48
67
|
if (!normalizedRaw) throw new Error("model id required");
|
|
49
68
|
const internal = toInternalId(normalizedRaw);
|
|
50
69
|
if (!internal) throw new Error("model id required");
|
|
51
|
-
const
|
|
70
|
+
const storageKey = internal.includes("/") ? internal.replace(/\//g, "-") : internal;
|
|
52
71
|
const cleanToken = String(token || "");
|
|
53
72
|
const p = Number(port) || 8989;
|
|
54
73
|
|
|
@@ -81,32 +100,49 @@ export async function syncToOpencode({ id, token, port, file } = {}) {
|
|
|
81
100
|
: null;
|
|
82
101
|
|
|
83
102
|
let action;
|
|
84
|
-
let effectiveId =
|
|
103
|
+
let effectiveId = storageKey;
|
|
85
104
|
if (oldProvider) {
|
|
86
105
|
const oldModels = oldProvider.models && typeof oldProvider.models === "object" && !Array.isArray(oldProvider.models)
|
|
87
106
|
? oldProvider.models
|
|
88
107
|
: {};
|
|
89
|
-
//
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
// 归一所有旧 key 到 storageKey 维度,判断是否已存在(兼容 mslxdff- 前缀与 / 形态)
|
|
109
|
+
const normalizeToStorage = (k) => {
|
|
110
|
+
const inner = toInternalId(String(k));
|
|
111
|
+
return inner.includes("/") ? inner.replace(/\//g, "-") : inner;
|
|
112
|
+
};
|
|
113
|
+
let existingKey = null;
|
|
114
|
+
for (const k of Object.keys(oldModels)) {
|
|
115
|
+
if (normalizeToStorage(k) === storageKey) { existingKey = k; break; }
|
|
116
|
+
}
|
|
117
|
+
// 也兼容直接 internal(slash)形态
|
|
118
|
+
if (!existingKey && Object.prototype.hasOwnProperty.call(oldModels, internal)) existingKey = internal;
|
|
119
|
+
if (!existingKey && Object.prototype.hasOwnProperty.call(oldModels, storageKey)) existingKey = storageKey;
|
|
120
|
+
|
|
93
121
|
const nextModels = { ...oldModels };
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
122
|
+
if (existingKey) {
|
|
123
|
+
// 已存在:迁移到 storageKey(新规范),清理旧的 legacy 键
|
|
124
|
+
const oldEntry = oldModels[existingKey];
|
|
125
|
+
const merged = oldEntry && typeof oldEntry === "object" && !Array.isArray(oldEntry) ? oldEntry : {};
|
|
126
|
+
// 若 existingKey !== storageKey,需要把旧键删掉,统一到 storageKey
|
|
127
|
+
if (existingKey !== storageKey) {
|
|
128
|
+
// 收集所有同逻辑的旧键一起清理
|
|
129
|
+
for (const k of Object.keys(oldModels)) {
|
|
130
|
+
if (normalizeToStorage(k) === storageKey) delete nextModels[k];
|
|
131
|
+
}
|
|
132
|
+
// 也清理 legacy mslxdff- 前缀变体
|
|
133
|
+
const legacyAlias = toExternalAlias(storageKey);
|
|
134
|
+
const legacyInternal = toExternalAlias(internal);
|
|
135
|
+
if (nextModels[legacyAlias]) delete nextModels[legacyAlias];
|
|
136
|
+
if (legacyInternal !== legacyAlias && nextModels[legacyInternal]) delete nextModels[legacyInternal];
|
|
102
137
|
}
|
|
138
|
+
nextModels[storageKey] = { ...merged, name: storageKey };
|
|
139
|
+
effectiveId = storageKey;
|
|
103
140
|
action = "updated";
|
|
104
141
|
} else {
|
|
105
|
-
nextModels[
|
|
106
|
-
effectiveId =
|
|
142
|
+
nextModels[storageKey] = { name: storageKey };
|
|
143
|
+
effectiveId = storageKey;
|
|
107
144
|
action = "inserted";
|
|
108
145
|
}
|
|
109
|
-
// 合并 provider:保留 name/npm,覆盖 options.baseURL/apiKey,合并 models
|
|
110
146
|
const nextProvider = {
|
|
111
147
|
...oldProvider,
|
|
112
148
|
name: oldProvider.name || "mslxdff",
|
|
@@ -118,18 +154,26 @@ export async function syncToOpencode({ id, token, port, file } = {}) {
|
|
|
118
154
|
},
|
|
119
155
|
models: nextModels,
|
|
120
156
|
};
|
|
121
|
-
// 若插入的是 alias 但原名已存在,上面已处理为不新增;否则正常
|
|
122
157
|
data.provider.mslxdff = nextProvider;
|
|
123
|
-
// 若是新插入且 external 不等于 internal,且 internal 已存在时,需避免双键,上面已处理
|
|
124
|
-
// 若是新插入 external 且 internal 不存在,正常插入
|
|
125
|
-
if (!exists) {
|
|
126
|
-
data.provider.mslxdff.models = nextModels;
|
|
127
|
-
}
|
|
128
158
|
} else {
|
|
129
|
-
data.provider.mslxdff = buildOpencodeProvider({ id:
|
|
159
|
+
data.provider.mslxdff = buildOpencodeProvider({ id: storageKey, token: cleanToken, port: p });
|
|
160
|
+
// buildOpencodeProvider 已用 storageKey,这里再确保
|
|
161
|
+
if (!data.provider.mslxdff.models[storageKey]) {
|
|
162
|
+
data.provider.mslxdff.models = { [storageKey]: { name: storageKey } };
|
|
163
|
+
}
|
|
130
164
|
action = "inserted";
|
|
131
165
|
}
|
|
132
166
|
|
|
167
|
+
// 若内部含 /,注册 dash→slash 别名,供网关 mslxdff 侧自动映射(与 WorkBuddy 同表)
|
|
168
|
+
if (internal.includes("/") && storageKey !== internal) {
|
|
169
|
+
try {
|
|
170
|
+
const { loadModelAliases, registerModelAlias, persistModelAliases } = await import("./providers/model-id.js");
|
|
171
|
+
loadModelAliases();
|
|
172
|
+
registerModelAlias(storageKey, internal);
|
|
173
|
+
persistModelAliases();
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
176
|
+
|
|
133
177
|
// 原子写
|
|
134
178
|
mkdirSync(dirname(targetFile), { recursive: true });
|
|
135
179
|
const tmp = `${targetFile}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
@@ -148,5 +192,5 @@ export async function syncToOpencode({ id, token, port, file } = {}) {
|
|
|
148
192
|
}
|
|
149
193
|
} catch {}
|
|
150
194
|
|
|
151
|
-
return { action, file: targetFile, id: effectiveId, alias:
|
|
195
|
+
return { action, file: targetFile, id: effectiveId, alias: storageKey, internal, corrupted, storageKey };
|
|
152
196
|
}
|