mslxdff 0.1.86 → 0.1.88

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.86",
3
+ "version": "0.1.88",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, toExternalAlias, toInternalId } from "../../sync-opencode.js";
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
- internal = toInternalId(norm);
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
- internal = toInternalId(norm);
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
- if (!ids.includes(internal) && !ids.includes(id)) {
66
- console.log(`warn: "${internal}" not in current free list (${ids.length} models), still syncing to opencode (alias ${id})`);
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
- const aliasLabel = r.alias && r.alias !== r.id ? ` (alias ${r.alias} 对应内部 ${r.internal})` : (r.id !== r.internal ? ` (alias for "${r.internal}", 原名仍兼容)` : ` (原名兼容)`);
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
- if (r.id !== r.internal) console.log(` alias: ${r.id} -> ${r.internal} (opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
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
- if (requested.startsWith("mslxdff-")) {
57
- const internal = aliasToInternal(requested);
58
- if (internal) { aliasInfo = `${requested} -> ${internal}`; requested = internal; }
59
- } else if (requested.includes("/")) {
60
- const slashIdx = requested.indexOf("/");
61
- const rawPart = requested.slice(slashIdx + 1);
62
- const providerPart = requested.slice(0, slashIdx);
63
- if (rawPart.startsWith("mslxdff-")) {
64
- const internal = aliasToInternal(rawPart);
65
- if (internal) {
66
- aliasInfo = `${requested} -> ${providerPart}/${internal} (alias stripped)`;
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 {} }
@@ -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 alias = toExternalAlias(id);
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
- [alias]: { name: alias },
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 external = toExternalAlias(internal);
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 = external;
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
- // 去重: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;
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 (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;
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[external] = { name: external };
106
- effectiveId = external;
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: external, token: cleanToken, port: p });
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: external, internal, corrupted };
195
+ return { action, file: targetFile, id: effectiveId, alias: storageKey, internal, corrupted, storageKey };
152
196
  }