mslxdff 0.1.236 → 0.1.237
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/package.json +1 -1
- package/src/auto.js +264 -264
- package/src/autostart.js +62 -15
- 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 +155 -155
- package/src/chat-pipeline/policy.js +84 -84
- package/src/cli/commands/group.js +266 -266
- 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 +139 -139
- package/src/cli/commands/provider/models.js +149 -149
- package/src/cli/commands/provider/qwenwork-login.js +119 -119
- package/src/cli/commands/provider/raccoon-quota.js +156 -156
- package/src/cli/commands/sync.js +310 -310
- package/src/cli/status.js +282 -282
- package/src/daemon.js +246 -246
- package/src/model-capabilities/enrich.js +86 -86
- package/src/model-capabilities/index.js +183 -183
- package/src/model-capabilities/parse.js +70 -70
- package/src/models.js +293 -293
- package/src/peers.js +389 -389
- package/src/providers/agentrouter.js +238 -238
- package/src/providers/base.js +164 -164
- 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/chat.js +207 -207
- package/src/providers/dispatcher.js +147 -147
- 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/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 +292 -292
- package/src/providers/qoder/index.js +311 -311
- package/src/providers/qoder/sse.js +233 -233
- 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 +337 -337
- package/src/providers/qwenwork/payload.js +144 -144
- 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/const.js +219 -219
- package/src/providers/raccoon/credits.js +285 -285
- package/src/providers/registry.js +89 -89
- package/src/providers/workbuddy/chat.js +298 -298
- package/src/providers/workbuddy/reshape.js +152 -152
- package/src/providers/workbuddy.js +2 -2
- package/src/reasoning.js +32 -32
- package/src/routes/chat/broadband-handler.js +80 -80
- package/src/routes/chat/gateway.js +46 -46
- package/src/routes/chat/relay-pipeline.js +282 -282
- package/src/routes/chat/via-route-handler.js +158 -158
- package/src/routes/hedge.js +260 -260
- package/src/routes/models-route.js +167 -167
- package/src/routes/peers.js +373 -373
- package/src/routes/stream.js +364 -364
- package/src/runtime/bootstrap.js +75 -75
- package/src/runtime/keep-warm.js +135 -135
- package/src/runtime/provider-gate.js +75 -75
- package/src/runtime/providers-setup.js +187 -187
- package/src/runtime/raccoon-checkin.js +175 -175
- package/src/runtime/server-lifecycle.js +34 -10
- package/src/server.js +85 -85
- package/src/state/schemas/allowlist.js +92 -92
- package/src/sync-opencode.js +280 -280
- 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-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.js +355 -355
- package/docs/cli_help_mini.md +0 -149
- package/src/providers/AGENTS.md +0 -49
- package/src/routes/AGENTS.md +0 -37
package/src/autostart.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node
|
|
|
3
3
|
import { homedir, userInfo } from "node:os";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { daemonDir, readPid } from "./daemon.js";
|
|
6
|
+
import { daemonDir, readPid, isPidAlive } from "./daemon.js";
|
|
7
7
|
import { writeExitMarker } from "./runtime/lifecycle-forensics.js";
|
|
8
8
|
|
|
9
9
|
const TASK_NAME = "mslxdff";
|
|
@@ -139,7 +139,47 @@ WantedBy=default.target
|
|
|
139
139
|
`;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 从 pid 文件原文算出「要不要等某个裸 daemon 退出」;null = 不等。
|
|
145
|
+
* 自杀护栏①:pid 文件写的就是**本进程**时(daemon 自己 `writePid(process.pid)`),
|
|
146
|
+
* 等它退出、再补一发 SIGKILL,就是等自己死 + 补自己一刀 —— 0.1.236 真机的死法之一。
|
|
147
|
+
*/
|
|
148
|
+
export function pickBareDaemonPid(raw, self = process.pid, isAlive = isPidAlive) {
|
|
149
|
+
const n = Number(String(raw ?? "").trim().split("\n")[0]);
|
|
150
|
+
if (!Number.isInteger(n) || n <= 0) return null;
|
|
151
|
+
if (n === self) return null;
|
|
152
|
+
return isAlive(n) ? n : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 把「端口持有者集合」拆成「可以杀的」+「自己也是持有者吗」—— 自杀护栏②的判据。
|
|
157
|
+
* 为什么需要:`ss -lptn 'sport = :<port>'` 对本用户自己的进程会显示 pid,集合里必然有 self。
|
|
158
|
+
* 旧代码第一圈 `continue` 跳过自己、第二圈**无差别 SIGKILL** ⇒ daemon 启动 4~5 秒后杀掉自己
|
|
159
|
+
* (0.1.236 真机:errors.log 记 unexplained-kill、daemon.alive 停在 uptimeMs:0、
|
|
160
|
+
* systemd unit 永远停在 disabled,下次裸跑继续循环)。
|
|
161
|
+
* 返回的 selfWasHolder 供调用方决定要不要跑 `fuser -k <port>/tcp`:它按端口无差别杀,绕不过 self。
|
|
162
|
+
*/
|
|
163
|
+
export function partitionKillTargets(pids, self = process.pid) {
|
|
164
|
+
const set = new Set();
|
|
165
|
+
for (const p of pids || []) {
|
|
166
|
+
const n = Number(p);
|
|
167
|
+
if (Number.isInteger(n) && n > 0) set.add(n);
|
|
168
|
+
}
|
|
169
|
+
const selfWasHolder = set.has(self);
|
|
170
|
+
set.delete(self);
|
|
171
|
+
return { targets: [...set], selfWasHolder };
|
|
172
|
+
}
|
|
142
173
|
async function linuxEnable() {
|
|
174
|
+
// systemd --user 不可用时**什么都不清理**:先杀掉还能用的裸 daemon、再发现 systemd 起不来,
|
|
175
|
+
// 等于用一次失败把服务弄死。只在 Linux 探测(macOS 也走本函数但没有 systemd,行为保持原样)。
|
|
176
|
+
if (process.platform === "linux") {
|
|
177
|
+
const probe = await execAsync("systemctl", ["--user", "show-environment"]);
|
|
178
|
+
if (probe.err) {
|
|
179
|
+
const why = (probe.stderr || probe.stdout || probe.err.message || "").slice(0, 200);
|
|
180
|
+
return { ok: false, error: `systemd --user 不可用(${why || "systemctl 失败"}):未做任何清理,裸 daemon 继续运行` };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
143
183
|
const unit = systemdUnitPath();
|
|
144
184
|
try {
|
|
145
185
|
mkdirSync(dirname(unit), { recursive: true });
|
|
@@ -158,35 +198,43 @@ async function linuxEnable() {
|
|
|
158
198
|
lingerRes = lr.err ? (lr.stderr || lr.stdout || lr.err.message || "").slice(0, 200) : "ok";
|
|
159
199
|
}
|
|
160
200
|
} catch {}
|
|
201
|
+
// 先 daemon-reload + enable,再清理裸 daemon:顺序反过来,一旦清理途中进程没了(旧版就是「杀自己」),
|
|
202
|
+
// unit 会永远停在 disabled,下一次裸跑又走进来 —— 这正是 0.1.236 的自我循环。
|
|
203
|
+
let r = await execAsync("systemctl", ["--user", "daemon-reload"]);
|
|
204
|
+
if (r.err) return { ok: false, error: (r.stderr || r.stdout).slice(0, 500) };
|
|
205
|
+
r = await execAsync("systemctl", ["--user", "enable", SERVICE_NAME]);
|
|
206
|
+
if (r.err) return { ok: false, error: (r.stderr || r.stdout).slice(0, 500) };
|
|
161
207
|
// stop bare detached daemon that may hold the port, let systemd take over (best-effort, wait for port free)
|
|
162
208
|
try {
|
|
163
209
|
// 路径相对**本文件**解析:本文件在 src/ 下,daemon.js 与 server.js 都在 src/ 内。
|
|
164
210
|
// 旧写的 `../daemon.js` / `../server.js` 从 src/ 出发指向仓库根 —— 两处都不存在,
|
|
165
211
|
// 整个「停裸 daemon → 等端口释放 → 让 systemd 接管」的分支被后面的 catch {} 静默吞掉、从未执行过。
|
|
166
|
-
const {
|
|
212
|
+
const { stopDaemon } = await import("./daemon.js");
|
|
167
213
|
const { resolvePort } = await import("./server.js");
|
|
168
214
|
const port = resolvePort();
|
|
169
|
-
const pidFile = join(
|
|
215
|
+
const pidFile = join(daemonDir(), "daemon.pid"); // 权威路径,尊重 MSLXDFF_DAEMON_DIR(旧代码硬编码 ~/.config ⇒ 清错 pid 文件)
|
|
170
216
|
const { existsSync: exists2, readFileSync: read2 } = await import("node:fs");
|
|
171
217
|
let pidToWait = null;
|
|
172
218
|
try {
|
|
173
219
|
if (exists2(pidFile)) {
|
|
174
|
-
const raw = read2(pidFile, "utf8")
|
|
175
|
-
|
|
176
|
-
|
|
220
|
+
const raw = read2(pidFile, "utf8");
|
|
221
|
+
// 自杀护栏①:pid 文件写的就是本进程时(daemon 自己 writePid)既不能等、更不能杀
|
|
222
|
+
pidToWait = pickBareDaemonPid(raw);
|
|
177
223
|
}
|
|
178
224
|
} catch {}
|
|
179
225
|
try { stopDaemon(); } catch {}
|
|
180
226
|
// ensure bare daemon really exits (SIGTERM -> SIGKILL)
|
|
181
227
|
for (let i = 0; i < 15; i++) {
|
|
182
|
-
if (pidToWait && isPidAlive(pidToWait)) {
|
|
183
|
-
if (i === 7) try { process.kill(pidToWait, "SIGKILL"); } catch {}
|
|
228
|
+
if (pidToWait && pidToWait !== process.pid && isPidAlive(pidToWait)) {
|
|
229
|
+
if (i === 7 && pidToWait !== process.pid) try { process.kill(pidToWait, "SIGKILL"); } catch {}
|
|
184
230
|
await new Promise((r2) => setTimeout(r2, 200));
|
|
185
231
|
} else break;
|
|
186
232
|
}
|
|
187
233
|
// robust: scan ss for any holder of :port (covers stale pidFile, fuser not installed)
|
|
188
234
|
const killHolders = async () => {
|
|
189
235
|
let killed = 0;
|
|
236
|
+
// 自己是不是端口持有者(决定要不要跑 fuser -k);探测失败时按「是」处理,宁可不杀
|
|
237
|
+
let selfWasHolder = false;
|
|
190
238
|
try {
|
|
191
239
|
const r = await execAsync("ss", ["-lptn", `sport = :${port}`]);
|
|
192
240
|
const out = r.stdout || "";
|
|
@@ -206,18 +254,21 @@ async function linuxEnable() {
|
|
|
206
254
|
while ((m2 = re2.exec(line))) pids.add(Number(m2[1]));
|
|
207
255
|
}
|
|
208
256
|
}
|
|
257
|
+
// 自杀护栏②:ss 会把自己报成端口持有者 ⇒ 剔除 self,SIGKILL/fuser 一律不打自己
|
|
258
|
+
const part = partitionKillTargets(pids, process.pid);
|
|
259
|
+
selfWasHolder = part.selfWasHolder;
|
|
209
260
|
for (const p of pids) {
|
|
210
261
|
if (p === process.pid) continue;
|
|
211
262
|
if (p === readPid()) { try { writeExitMarker(daemonDir(), { reason: "autostart-cleanup", prevPid: p, prevVersion: null, byPid: process.pid }); } catch {} }
|
|
212
263
|
try { process.kill(p, "SIGTERM"); killed++; } catch {}
|
|
213
264
|
}
|
|
214
265
|
if (killed) await new Promise((r2) => setTimeout(r2, 400));
|
|
215
|
-
for (const p of
|
|
266
|
+
for (const p of part.targets) {
|
|
216
267
|
try { if (isPidAlive(p)) process.kill(p, "SIGKILL"); } catch {}
|
|
217
268
|
}
|
|
218
|
-
} catch {}
|
|
269
|
+
} catch { selfWasHolder = true; } // ss 探测失败 ⇒ 谁占着端口不清楚,fuser 一律不开火
|
|
219
270
|
// fuser as extra best-effort (may not exist)
|
|
220
|
-
try { await execAsync("fuser", ["-k", `${port}/tcp`]); } catch {}
|
|
271
|
+
if (!selfWasHolder) { try { await execAsync("fuser", ["-k", `${port}/tcp`]); } catch {} }
|
|
221
272
|
return killed;
|
|
222
273
|
};
|
|
223
274
|
await killHolders();
|
|
@@ -231,10 +282,6 @@ async function linuxEnable() {
|
|
|
231
282
|
await new Promise((r2) => setTimeout(r2, 250));
|
|
232
283
|
}
|
|
233
284
|
} catch {}
|
|
234
|
-
let r = await execAsync("systemctl", ["--user", "daemon-reload"]);
|
|
235
|
-
if (r.err) return { ok: false, error: (r.stderr || r.stdout).slice(0, 500) };
|
|
236
|
-
r = await execAsync("systemctl", ["--user", "enable", SERVICE_NAME]);
|
|
237
|
-
if (r.err) return { ok: false, error: (r.stderr || r.stdout).slice(0, 500) };
|
|
238
285
|
r = await execAsync("systemctl", ["--user", "start", SERVICE_NAME]);
|
|
239
286
|
// Verify actually active; if not, surface journal hint
|
|
240
287
|
const chk = await execAsync("systemctl", ["--user", "is-active", SERVICE_NAME]);
|
package/src/bench/cline-bench.js
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { clineHeaders } from "../providers/cline/headers.js";
|
|
2
|
-
import { computeMetrics } from "../metrics.js";
|
|
3
|
-
import { createTransport } from "../transport/index.js";
|
|
4
|
-
|
|
5
|
-
function sseContent(obj) {
|
|
6
|
-
const c = obj?.choices?.[0]?.delta?.content || obj?.choices?.[0]?.message?.content || "";
|
|
7
|
-
return typeof c === "string" ? c : "";
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function clineBenchOne({ baseUrl, model, accessToken, prompt, maxTokens, timeoutMs, fetchImpl }) {
|
|
11
|
-
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
12
|
-
let ttfbMs = null;
|
|
13
|
-
let totalMs = null;
|
|
14
|
-
let content = "";
|
|
15
|
-
try {
|
|
16
|
-
const sid = `sess_bench_${Date.now()}`;
|
|
17
|
-
const res = await tr.request({
|
|
18
|
-
url: `${baseUrl}/api/v1/chat/completions`,
|
|
19
|
-
method: "POST",
|
|
20
|
-
headers: { ...clineHeaders(sid, accessToken), Accept: "text/event-stream" },
|
|
21
|
-
body: { model, messages: [{ role: "user", content: prompt }], stream: true, max_tokens: maxTokens, session_id: sid, reasoning_effort: "high" },
|
|
22
|
-
stream: true,
|
|
23
|
-
});
|
|
24
|
-
if (!res.ok) {
|
|
25
|
-
let txt = "";
|
|
26
|
-
try { txt = await res.text(); } catch {}
|
|
27
|
-
const label = res.status === 401 ? "鉴权失败" : res.status === 429 ? "限流" : res.status >= 500 ? `上游错误 ${res.status}` : `HTTP ${res.status}`;
|
|
28
|
-
return { id: model, ok: false, status: res.status, label, error: txt.slice(0, 300), ttfbMs, totalMs: res.totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
29
|
-
}
|
|
30
|
-
for await (const ev of res.stream()) {
|
|
31
|
-
try { content += sseContent(JSON.parse(ev)); } catch {}
|
|
32
|
-
}
|
|
33
|
-
ttfbMs = res.ttfbMs;
|
|
34
|
-
totalMs = res.totalMs;
|
|
35
|
-
const chars = content.length;
|
|
36
|
-
const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens: null, completionTokens: null, chars });
|
|
37
|
-
return { id: model, ok: true, status: 200, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: null, chars };
|
|
38
|
-
} catch (e) {
|
|
39
|
-
const msg = e?.message || String(e);
|
|
40
|
-
return { id: model, ok: false, label: /timeout|abort/i.test(msg) ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
import { clineHeaders } from "../providers/cline/headers.js";
|
|
2
|
+
import { computeMetrics } from "../metrics.js";
|
|
3
|
+
import { createTransport } from "../transport/index.js";
|
|
4
|
+
|
|
5
|
+
function sseContent(obj) {
|
|
6
|
+
const c = obj?.choices?.[0]?.delta?.content || obj?.choices?.[0]?.message?.content || "";
|
|
7
|
+
return typeof c === "string" ? c : "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function clineBenchOne({ baseUrl, model, accessToken, prompt, maxTokens, timeoutMs, fetchImpl }) {
|
|
11
|
+
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
12
|
+
let ttfbMs = null;
|
|
13
|
+
let totalMs = null;
|
|
14
|
+
let content = "";
|
|
15
|
+
try {
|
|
16
|
+
const sid = `sess_bench_${Date.now()}`;
|
|
17
|
+
const res = await tr.request({
|
|
18
|
+
url: `${baseUrl}/api/v1/chat/completions`,
|
|
19
|
+
method: "POST",
|
|
20
|
+
headers: { ...clineHeaders(sid, accessToken), Accept: "text/event-stream" },
|
|
21
|
+
body: { model, messages: [{ role: "user", content: prompt }], stream: true, max_tokens: maxTokens, session_id: sid, reasoning_effort: "high" },
|
|
22
|
+
stream: true,
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
let txt = "";
|
|
26
|
+
try { txt = await res.text(); } catch {}
|
|
27
|
+
const label = res.status === 401 ? "鉴权失败" : res.status === 429 ? "限流" : res.status >= 500 ? `上游错误 ${res.status}` : `HTTP ${res.status}`;
|
|
28
|
+
return { id: model, ok: false, status: res.status, label, error: txt.slice(0, 300), ttfbMs, totalMs: res.totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
29
|
+
}
|
|
30
|
+
for await (const ev of res.stream()) {
|
|
31
|
+
try { content += sseContent(JSON.parse(ev)); } catch {}
|
|
32
|
+
}
|
|
33
|
+
ttfbMs = res.ttfbMs;
|
|
34
|
+
totalMs = res.totalMs;
|
|
35
|
+
const chars = content.length;
|
|
36
|
+
const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens: null, completionTokens: null, chars });
|
|
37
|
+
return { id: model, ok: true, status: 200, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: null, chars };
|
|
38
|
+
} catch (e) {
|
|
39
|
+
const msg = e?.message || String(e);
|
|
40
|
+
return { id: model, ok: false, label: /timeout|abort/i.test(msg) ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/bench/probe.js
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
import { joinUrl } from "../providers/base.js";
|
|
2
|
-
import { getCustomNormalizer } from "../providers/registry.js";
|
|
3
|
-
import { createTransport } from "../transport/index.js";
|
|
1
|
+
import { joinUrl } from "../providers/base.js";
|
|
2
|
+
import { getCustomNormalizer } from "../providers/registry.js";
|
|
3
|
+
import { createTransport } from "../transport/index.js";
|
|
4
4
|
import { compatFetch } from "../compat.js";
|
|
5
|
-
|
|
6
|
-
function normalizeModelsPayload(json, baseUrl = "") {
|
|
7
|
-
if (!json) return [];
|
|
8
|
-
try {
|
|
9
|
-
const custom = getCustomNormalizer(baseUrl);
|
|
10
|
-
if (custom) {
|
|
11
|
-
const v = custom(json);
|
|
12
|
-
if (Array.isArray(v)) return v;
|
|
13
|
-
}
|
|
14
|
-
} catch {}
|
|
15
|
-
if (Array.isArray(json)) return json;
|
|
16
|
-
if (Array.isArray(json.data)) return json.data;
|
|
17
|
-
if (Array.isArray(json.models)) return json.models;
|
|
18
|
-
if (json.data && typeof json.data === "object" && Array.isArray(json.data.data)) return json.data.data;
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function toModelId(m) {
|
|
23
|
-
if (!m) return "";
|
|
24
|
-
if (typeof m === "string") return m;
|
|
25
|
-
if (typeof m.id === "string") return m.id;
|
|
26
|
-
if (typeof m.model === "string") return m.model;
|
|
27
|
-
if (typeof m.name === "string") return m.name;
|
|
28
|
-
return "";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function probeModels({
|
|
32
|
-
baseUrl,
|
|
33
|
-
modelsPath,
|
|
34
|
-
chatPath,
|
|
35
|
-
headers = {},
|
|
36
|
-
fetchImpl = compatFetch,
|
|
37
|
-
timeoutMs = 8000,
|
|
38
|
-
} = {}) {
|
|
39
|
-
const base = String(baseUrl || "").replace(/\/+$/, "");
|
|
40
|
-
if (!base) return { ok: false, error: "missing baseUrl", tried: [], data: [] };
|
|
41
|
-
const candidates = [];
|
|
42
|
-
const seen = new Set();
|
|
43
|
-
const push = (p) => {
|
|
44
|
-
const s = String(p || "").trim();
|
|
45
|
-
if (!s) return;
|
|
46
|
-
const norm = s.startsWith("/") ? s : `/${s}`;
|
|
47
|
-
if (!seen.has(norm)) { seen.add(norm); candidates.push(norm); }
|
|
48
|
-
};
|
|
49
|
-
if (modelsPath) push(modelsPath);
|
|
50
|
-
push("/v1/models");
|
|
51
|
-
push("/models");
|
|
52
|
-
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
53
|
-
const tried = [];
|
|
54
|
-
let lastError = "";
|
|
55
|
-
for (const p of candidates) {
|
|
56
|
-
const url = joinUrl(base, p);
|
|
57
|
-
tried.push(url);
|
|
58
|
-
try {
|
|
59
|
-
const res = await tr.request({ url, method: "GET", headers, stream: false });
|
|
60
|
-
if (!res.ok) { lastError = `HTTP ${res.status}`; continue; }
|
|
61
|
-
let json = {};
|
|
62
|
-
try { json = await res.json(); } catch { json = {}; }
|
|
63
|
-
const raw = normalizeModelsPayload(json, baseUrl);
|
|
64
|
-
const data = raw.map((m) => ({ id: toModelId(m), raw: m })).filter((x) => x.id).map((x) => ({ id: x.id, ...x.raw }));
|
|
65
|
-
return { ok: true, data, tried, url, rawCount: raw.length };
|
|
66
|
-
} catch (e) {
|
|
67
|
-
lastError = e?.message || String(e);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return { ok: false, error: lastError || "all probes failed", tried, data: [] };
|
|
71
|
-
}
|
|
5
|
+
|
|
6
|
+
function normalizeModelsPayload(json, baseUrl = "") {
|
|
7
|
+
if (!json) return [];
|
|
8
|
+
try {
|
|
9
|
+
const custom = getCustomNormalizer(baseUrl);
|
|
10
|
+
if (custom) {
|
|
11
|
+
const v = custom(json);
|
|
12
|
+
if (Array.isArray(v)) return v;
|
|
13
|
+
}
|
|
14
|
+
} catch {}
|
|
15
|
+
if (Array.isArray(json)) return json;
|
|
16
|
+
if (Array.isArray(json.data)) return json.data;
|
|
17
|
+
if (Array.isArray(json.models)) return json.models;
|
|
18
|
+
if (json.data && typeof json.data === "object" && Array.isArray(json.data.data)) return json.data.data;
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function toModelId(m) {
|
|
23
|
+
if (!m) return "";
|
|
24
|
+
if (typeof m === "string") return m;
|
|
25
|
+
if (typeof m.id === "string") return m.id;
|
|
26
|
+
if (typeof m.model === "string") return m.model;
|
|
27
|
+
if (typeof m.name === "string") return m.name;
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function probeModels({
|
|
32
|
+
baseUrl,
|
|
33
|
+
modelsPath,
|
|
34
|
+
chatPath,
|
|
35
|
+
headers = {},
|
|
36
|
+
fetchImpl = compatFetch,
|
|
37
|
+
timeoutMs = 8000,
|
|
38
|
+
} = {}) {
|
|
39
|
+
const base = String(baseUrl || "").replace(/\/+$/, "");
|
|
40
|
+
if (!base) return { ok: false, error: "missing baseUrl", tried: [], data: [] };
|
|
41
|
+
const candidates = [];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const push = (p) => {
|
|
44
|
+
const s = String(p || "").trim();
|
|
45
|
+
if (!s) return;
|
|
46
|
+
const norm = s.startsWith("/") ? s : `/${s}`;
|
|
47
|
+
if (!seen.has(norm)) { seen.add(norm); candidates.push(norm); }
|
|
48
|
+
};
|
|
49
|
+
if (modelsPath) push(modelsPath);
|
|
50
|
+
push("/v1/models");
|
|
51
|
+
push("/models");
|
|
52
|
+
const tr = createTransport({ fetchImpl, keepAlive: false, retry: {}, timeoutMs });
|
|
53
|
+
const tried = [];
|
|
54
|
+
let lastError = "";
|
|
55
|
+
for (const p of candidates) {
|
|
56
|
+
const url = joinUrl(base, p);
|
|
57
|
+
tried.push(url);
|
|
58
|
+
try {
|
|
59
|
+
const res = await tr.request({ url, method: "GET", headers, stream: false });
|
|
60
|
+
if (!res.ok) { lastError = `HTTP ${res.status}`; continue; }
|
|
61
|
+
let json = {};
|
|
62
|
+
try { json = await res.json(); } catch { json = {}; }
|
|
63
|
+
const raw = normalizeModelsPayload(json, baseUrl);
|
|
64
|
+
const data = raw.map((m) => ({ id: toModelId(m), raw: m })).filter((x) => x.id).map((x) => ({ id: x.id, ...x.raw }));
|
|
65
|
+
return { ok: true, data, tried, url, rawCount: raw.length };
|
|
66
|
+
} catch (e) {
|
|
67
|
+
lastError = e?.message || String(e);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { ok: false, error: lastError || "all probes failed", tried, data: [] };
|
|
71
|
+
}
|