mslxdff 0.1.126 → 0.1.128
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
|
@@ -34,6 +34,10 @@ export async function handleProviderModels(id, sub, args, rest) {
|
|
|
34
34
|
let provider;
|
|
35
35
|
if (id === "workbuddy") {
|
|
36
36
|
provider = createWorkbuddyProvider({ baseUrl, apiKeys: keys, auths, file: defaultStateFile() });
|
|
37
|
+
} else if (id === "cline" || id === "clinebot") {
|
|
38
|
+
// 与聚合目录同源:recommended-models 的 free(generic 的 /api/v1/models 是全量目录,含大量不可用条目)
|
|
39
|
+
const { createClineProvider } = await import("../../../providers/cline.js");
|
|
40
|
+
provider = createClineProvider({ id, baseUrl: baseUrl || undefined, apiKeys: keys, file: defaultStateFile() });
|
|
37
41
|
} else {
|
|
38
42
|
if (!baseUrl) {
|
|
39
43
|
console.error(`provider ${id}: missing baseUrl — set via: mslxdff -provider ${id} set-url <baseUrl>`);
|
|
@@ -3,6 +3,8 @@ import { loadProviderKeys, loadProviderBaseUrl, loadProviderModelsPath, loadProv
|
|
|
3
3
|
import { envInt, joinUrl, getUndici, createAgent, collectApiKeysGeneric, createChatRunner } from "../base.js";
|
|
4
4
|
import { compatFetch } from "../../compat.js";
|
|
5
5
|
import { joinModelId } from "../model-id.js";
|
|
6
|
+
import { logDir } from "../../logs.js";
|
|
7
|
+
import { join } from "node:path";
|
|
6
8
|
import { createAuthPool } from "./auth.js";
|
|
7
9
|
import { clineHeaders, isRefreshToken } from "./headers.js";
|
|
8
10
|
import { createChatService } from "./chat.js";
|
|
@@ -24,6 +26,7 @@ export function createClineProvider({
|
|
|
24
26
|
apiKey,
|
|
25
27
|
modelsPath,
|
|
26
28
|
chatPath,
|
|
29
|
+
snapshotPath,
|
|
27
30
|
connectTimeoutMs = Number(process.env.MSLXDFF_CLINE_TIMEOUT_MS) || 30_000,
|
|
28
31
|
cooldownMs = envInt("MSLXDFF_CLINE_COOLDOWN_MS", 30_000),
|
|
29
32
|
retry = { network: { attempts: 2, delayMs: 300 }, 429: { attempts: 1, delayMs: 100 }, 502: { attempts: 1, delayMs: 100 }, 503: { attempts: 1, delayMs: 100 }, 504: { attempts: 1, delayMs: 100 } },
|
|
@@ -105,7 +108,8 @@ export function createClineProvider({
|
|
|
105
108
|
return oldRunner.runChat(body, tmpRing, "shared provider keys");
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
|
|
111
|
+
// 快照文件供 daemon 启动自检 free 列表增删(默认 logDir 下,gitignored;测试可注入 tmp 路径)
|
|
112
|
+
const modelsSvc = createModelsService({ id, baseUrl: resolvedBase, modelsPath: resolvedModelsPath, fetchImpl, dispatcher, ring, loadKeys: (pid) => loadProviderKeys(pid, file ? { file } : {}), snapshotPath: snapshotPath || join(logDir(), "cline-free.json") });
|
|
109
113
|
|
|
110
114
|
async function listModels() {
|
|
111
115
|
const list = await modelsSvc.listModels();
|
|
@@ -113,10 +117,10 @@ export function createClineProvider({
|
|
|
113
117
|
return list;
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
const { preheat } = (() => {
|
|
117
|
-
try { return modelsSvc; } catch { return { preheat: async () => ({ ok: false }) }; }
|
|
120
|
+
const { preheat, checkFreeUpdates } = (() => {
|
|
121
|
+
try { return modelsSvc; } catch { return { preheat: async () => ({ ok: false }), checkFreeUpdates: async () => ({ ok: false }) }; }
|
|
118
122
|
})();
|
|
119
123
|
|
|
120
124
|
async function close() { if (agent?.close) try { await agent.close(); } catch {} }
|
|
121
|
-
return { id, chat, chatWithKeys, listModels, preheat, close, agent, keyRing: ring, baseUrl: resolvedBase, _authPool: authPool };
|
|
125
|
+
return { id, chat, chatWithKeys, listModels, preheat, checkFreeUpdates, close, agent, keyRing: ring, baseUrl: resolvedBase, _authPool: authPool };
|
|
122
126
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { joinUrl, getUndici } from "../base.js";
|
|
2
2
|
import { compatFetch } from "../../compat.js";
|
|
3
3
|
import { joinModelId } from "../model-id.js";
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
4
6
|
|
|
5
7
|
const { UndiciFetch } = getUndici();
|
|
6
8
|
|
|
@@ -8,7 +10,7 @@ function isClineBotHost(baseUrl) {
|
|
|
8
10
|
try { const u = new URL(baseUrl); return u.hostname === "api.cline.bot" || u.hostname.endsWith(".cline.bot"); } catch { return String(baseUrl).includes("cline.bot"); }
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispatcher, ring, loadKeys } = {}) {
|
|
13
|
+
export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispatcher, ring, loadKeys, snapshotPath } = {}) {
|
|
12
14
|
if (!fetchImpl) fetchImpl = UndiciFetch || compatFetch;
|
|
13
15
|
const resolvedBase = String(baseUrl).trim().replace(/\/+$/, "");
|
|
14
16
|
const resolvedPath = modelsPath || "/ai/cline/recommended-models";
|
|
@@ -16,11 +18,13 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
|
|
|
16
18
|
let cache = null;
|
|
17
19
|
let fetchedAt = 0;
|
|
18
20
|
|
|
19
|
-
//
|
|
21
|
+
// 离线兜底:与上游 recommended-models 的 free 对齐(2026-09-16 实测 5 个)。
|
|
20
22
|
// 上游挂了/401 时也不返回空数组,保证 -provider clinebot models 与 picks 仍有免费可用。
|
|
21
23
|
const FALLBACK_FREE = [
|
|
22
|
-
{ id: "
|
|
24
|
+
{ id: "cline-free/deepseek-v4.1-flash", name: "deepseek-v4.1-flash" },
|
|
25
|
+
{ id: "cline-free/muse-spark-1.3-contributor", name: "muse-spark-1.3-contributor" },
|
|
23
26
|
{ id: "z-ai/glm-5.3-flash", name: "glm-5.3-flash" },
|
|
27
|
+
{ id: "cline-free/solar-pro4", name: "solar-pro4" },
|
|
24
28
|
{ id: "poolside/laguna-s-2.1:free", name: "laguna-s-2.1:free" },
|
|
25
29
|
];
|
|
26
30
|
|
|
@@ -67,7 +71,41 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
|
|
|
67
71
|
} catch { return fallbackList(); } finally { clearTimeout(timer); }
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
// 每次 daemon 启动自检 free 列表(server-lifecycle 显式调 checkFreeUpdates,不经 dispatcher.preheat):
|
|
75
|
+
// 对比快照报增删并留痕(daemon.log),顺带把结果填缓存(省一次 listModels 请求)。
|
|
76
|
+
// 快照路径由 index.js 注入 logDir 下文件;未注入时仅跳过自检,不影响拉取。
|
|
77
|
+
function readSnapshotFree() {
|
|
78
|
+
try { const j = JSON.parse(readFileSync(snapshotPath, "utf8")); return Array.isArray(j?.free) ? j.free : null; } catch { return null; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function writeSnapshotFree(ids) {
|
|
82
|
+
try {
|
|
83
|
+
mkdirSync(dirname(snapshotPath), { recursive: true });
|
|
84
|
+
writeFileSync(snapshotPath, JSON.stringify({ ts: Date.now(), free: ids }, null, 2));
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Note: clinebot free 目录三处同源(CLI 直查/聚合目录/兜底)+ daemon 启动自检快照(diff 写 daemon.log,不自动改 picks)— 见 .agents/notes/implemented/bug-fix/2026-09-16-clinebot-free-catalog-unify.md
|
|
89
|
+
function detectFreeChanges(ids) {
|
|
90
|
+
const prev = readSnapshotFree();
|
|
91
|
+
if (!prev) {
|
|
92
|
+
writeSnapshotFree(ids);
|
|
93
|
+
console.log(`[${id}] free snapshot created (${ids.length} models)`);
|
|
94
|
+
return { first: true };
|
|
95
|
+
}
|
|
96
|
+
const added = ids.filter((x) => !prev.includes(x));
|
|
97
|
+
const removed = prev.filter((x) => !ids.includes(x));
|
|
98
|
+
if (!added.length && !removed.length) return { unchanged: true };
|
|
99
|
+
writeSnapshotFree(ids);
|
|
100
|
+
const parts = [];
|
|
101
|
+
if (added.length) parts.push(`+${added.join(" +")}`);
|
|
102
|
+
if (removed.length) parts.push(`-${removed.join(" -")}`);
|
|
103
|
+
console.log(`[${id}] free models updated (${ids.length} total): ${parts.join(" ")}`);
|
|
104
|
+
return { added, removed };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 启动自检入口:拉 recommended-models → 填缓存 → 对比快照报 free 增删。
|
|
108
|
+
async function checkFreeUpdates() {
|
|
71
109
|
const url = resolveModelsUrl();
|
|
72
110
|
const t0 = performance.now();
|
|
73
111
|
try {
|
|
@@ -77,12 +115,38 @@ export function createModelsService({ id, baseUrl, modelsPath, fetchImpl, dispat
|
|
|
77
115
|
const opts = { headers };
|
|
78
116
|
if (dispatcher) opts.dispatcher = dispatcher;
|
|
79
117
|
const res = await fetchImpl(url, opts);
|
|
80
|
-
|
|
81
|
-
|
|
118
|
+
let freeIds = null;
|
|
119
|
+
try {
|
|
120
|
+
const text = await res.text().catch(() => "");
|
|
121
|
+
if (res.ok && text && isClineBotHost(resolvedBase)) {
|
|
122
|
+
const json = JSON.parse(text);
|
|
123
|
+
if (Array.isArray(json.free)) {
|
|
124
|
+
const valid = json.free.filter((m) => m && typeof m.id === "string");
|
|
125
|
+
if (valid.length) {
|
|
126
|
+
cache = valid.map((m) => ({ ...m, id: joinModelId(id, m.id) }));
|
|
127
|
+
fetchedAt = Date.now();
|
|
128
|
+
freeIds = valid.map((m) => m.id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
133
|
+
let change = null;
|
|
134
|
+
if (freeIds && snapshotPath) { try { change = detectFreeChanges(freeIds); } catch {} }
|
|
135
|
+
return {
|
|
136
|
+
ok: res.ok,
|
|
137
|
+
status: res.status,
|
|
138
|
+
ms: Math.round(performance.now() - t0),
|
|
139
|
+
...(change?.added ? { freeAdded: change.added, freeRemoved: change.removed } : {}),
|
|
140
|
+
};
|
|
82
141
|
} catch (err) {
|
|
83
142
|
return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
|
|
84
143
|
}
|
|
85
144
|
}
|
|
86
145
|
|
|
87
|
-
|
|
146
|
+
// preheat 保留为别名:dispatcher 已不再调 clinebot,手动/测试/未来钩子仍可用,行为与自检一致
|
|
147
|
+
async function preheat() {
|
|
148
|
+
return checkFreeUpdates();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { listModels, preheat, checkFreeUpdates };
|
|
88
152
|
}
|
|
@@ -92,17 +92,18 @@ export function createProviderDispatcher(providers = [], opts = {}) {
|
|
|
92
92
|
return out;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// 只预热默认供应商(opencode):连接池与模型缓存预热是其主链路收益;
|
|
96
|
+
// 其他供应商按需在首次请求时自拉(10min 缓存)。不再逐家预热,避免 daemon 每次启动
|
|
97
|
+
// 对所有上游各发一次 GET;MSLXDFF_PREHEAT=0 的关闭由唯一被调的 opencode preheat 自行尊重。
|
|
98
|
+
// 见 .agents/notes/implemented/simplification/2026-09-16-preheat-opencode-only.md
|
|
95
99
|
async function preheat() {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
results.push({ ok: false, error: "preheat failed" });
|
|
103
|
-
}
|
|
100
|
+
const p = byId.get(DEFAULT_PROVIDER);
|
|
101
|
+
if (!p || typeof p.preheat !== "function") return { ok: false, skipped: true };
|
|
102
|
+
try {
|
|
103
|
+
return await p.preheat();
|
|
104
|
+
} catch {
|
|
105
|
+
return { ok: false, error: "preheat failed" };
|
|
104
106
|
}
|
|
105
|
-
return results.length ? results[0] : { ok: false, skipped: true };
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
async function close() {
|
|
@@ -123,6 +123,12 @@ export async function startServerLifecycle({ VERSION, token, created, upstream,
|
|
|
123
123
|
else if (r.ok) console.log(`[preheat] opencode models ok ${r.status} ${r.ms}ms`);
|
|
124
124
|
else console.log(`[preheat] opencode models failed ${r.error || r.status || ""} ${r.ms || 0}ms`);
|
|
125
125
|
}).catch(() => {});
|
|
126
|
+
// clinebot free 目录自检(独立于 opencode 连接预热:dispatcher.preheat 只做默认供应商)
|
|
127
|
+
// 见 .agents/notes/implemented/simplification/2026-09-16-preheat-opencode-only.md
|
|
128
|
+
try {
|
|
129
|
+
const cline = upstream?.byId?.get?.("clinebot") ?? upstream?.byId?.get?.("cline");
|
|
130
|
+
if (cline?.checkFreeUpdates) void cline.checkFreeUpdates().catch(() => {});
|
|
131
|
+
} catch {}
|
|
126
132
|
}, 100).unref?.();
|
|
127
133
|
|
|
128
134
|
// responses 模型判定改元数据驱动(models.dev provider.npm):就绪后注入,每小时重查使新模型自动识别
|