mslxdff 0.1.70 → 0.1.71
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/bench/probe.js +10 -1
- package/src/bench/runner.js +4 -1
- package/src/chat/repl.js +2 -1
- package/src/providers/cline.js +112 -0
- package/src/providers/registry.js +41 -0
- package/src/runtime/bootstrap.js +15 -11
package/package.json
CHANGED
package/src/bench/probe.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { joinUrl } from "../providers/base.js";
|
|
2
|
+
import { getCustomNormalizer } from "../providers/registry.js";
|
|
2
3
|
|
|
3
|
-
function normalizeModelsPayload(json) {
|
|
4
|
+
function normalizeModelsPayload(json, baseUrl = "") {
|
|
4
5
|
if (!json) return [];
|
|
6
|
+
// 可扩展:优先走注册表的定制化解析(如 cline.bot 仅 free)
|
|
7
|
+
try {
|
|
8
|
+
const custom = getCustomNormalizer(baseUrl);
|
|
9
|
+
if (custom) {
|
|
10
|
+
const v = custom(json);
|
|
11
|
+
if (Array.isArray(v)) return v;
|
|
12
|
+
}
|
|
13
|
+
} catch {}
|
|
5
14
|
if (Array.isArray(json)) return json;
|
|
6
15
|
if (Array.isArray(json.data)) return json.data;
|
|
7
16
|
if (Array.isArray(json.models)) return json.models;
|
package/src/bench/runner.js
CHANGED
|
@@ -31,7 +31,10 @@ export async function runOne({
|
|
|
31
31
|
if (!model) return { id: model, ok: false, error: "missing model", label: "配置错误", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
32
32
|
if (!apiKey) return { id: model, ok: false, error: "missing apiKey", label: "未配置 Key", ttfbMs: null, totalMs: 0, tps: null, charsPerSec: null, tokens: null };
|
|
33
33
|
const url = joinUrl(String(baseUrl).replace(/\/+$/, ""), chatPath);
|
|
34
|
-
|
|
34
|
+
let rawModel = String(model || "").trim();
|
|
35
|
+
// allowlist 已是 raw(如 z-ai/glm-5.3-flash),仅当带供应商前缀时才剥
|
|
36
|
+
if (providerId && rawModel.startsWith(`${providerId}/`)) rawModel = rawModel.slice(providerId.length + 1);
|
|
37
|
+
const body = { model: rawModel, stream: false, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens };
|
|
35
38
|
// workbuddy 需额外 workbuddy 透传头由调用方 headers 注入;此处直接透传
|
|
36
39
|
const finalHeaders = { "Content-Type": "application/json", Accept: "application/json", ...headers };
|
|
37
40
|
if (apiKey && !finalHeaders.Authorization) finalHeaders.Authorization = `Bearer ${apiKey}`;
|
package/src/chat/repl.js
CHANGED
|
@@ -237,7 +237,8 @@ function printFooter({ model, provider, latency, usage, totalMs, fallback, fallb
|
|
|
237
237
|
try { gw = collectStats(); } catch {}
|
|
238
238
|
const prov = provider && provider !== "opencode" ? `${provider}/` : "";
|
|
239
239
|
const baseLabel = model ? `${prov}${model}` : "—";
|
|
240
|
-
const
|
|
240
|
+
const via = provider && !baseLabel.startsWith(`${provider}/`) ? `: ${provider}` : "";
|
|
241
|
+
const modelLabel = model ? (fallbackGateway || viaGateway ? `${baseLabel} (gateway auto${via})` : baseLabel) : "—";
|
|
241
242
|
const latLabel = latency ? `${latency}ms` : "—";
|
|
242
243
|
const totalLabel = totalMs ? ` · 总耗时 ${totalMs}ms` : "";
|
|
243
244
|
const tokLabel = usage ? ` · tokens ${usage.prompt_tokens ?? "?"}→${usage.completion_tokens ?? "?"}` : "";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createKeyRing } from "./keyring.js";
|
|
2
|
+
import { loadProviderKeys, loadProviderBaseUrl, loadProviderModelsPath, loadProviderChatPath } from "../state.js";
|
|
3
|
+
import { envInt, joinUrl, getUndici, createAgent, collectApiKeysGeneric, createChatRunner, createPreheatRunner } from "./base.js";
|
|
4
|
+
import { joinModelId } from "./model-id.js";
|
|
5
|
+
|
|
6
|
+
const { UndiciFetch } = getUndici();
|
|
7
|
+
|
|
8
|
+
function resolveBaseUrl(id, baseUrl) {
|
|
9
|
+
if (baseUrl) return String(baseUrl).trim().replace(/\/+$/, "");
|
|
10
|
+
const env = loadProviderBaseUrl(id);
|
|
11
|
+
if (env) return env;
|
|
12
|
+
return "https://api.cline.bot";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isClineBotHost(baseUrl) {
|
|
16
|
+
try {
|
|
17
|
+
const u = new URL(baseUrl);
|
|
18
|
+
return u.hostname === "api.cline.bot" || u.hostname.endsWith(".cline.bot");
|
|
19
|
+
} catch { return String(baseUrl).includes("cline.bot"); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createClineProvider({
|
|
23
|
+
id = "cline",
|
|
24
|
+
baseUrl,
|
|
25
|
+
apiKeys,
|
|
26
|
+
apiKey,
|
|
27
|
+
modelsPath,
|
|
28
|
+
chatPath,
|
|
29
|
+
connectTimeoutMs = Number(process.env.MSLXDFF_CLINE_TIMEOUT_MS) || 30_000,
|
|
30
|
+
cooldownMs = envInt("MSLXDFF_CLINE_COOLDOWN_MS", 30_000),
|
|
31
|
+
retry = {
|
|
32
|
+
network: { attempts: 2, delayMs: 300 },
|
|
33
|
+
429: { attempts: 1, delayMs: 100 },
|
|
34
|
+
502: { attempts: 1, delayMs: 100 },
|
|
35
|
+
503: { attempts: 1, delayMs: 100 },
|
|
36
|
+
504: { attempts: 1, delayMs: 100 },
|
|
37
|
+
},
|
|
38
|
+
fetchImpl,
|
|
39
|
+
} = {}) {
|
|
40
|
+
const resolvedBase = resolveBaseUrl(id, baseUrl);
|
|
41
|
+
const resolvedModelsPath = modelsPath || loadProviderModelsPath(id) || "/ai/cline/recommended-models";
|
|
42
|
+
// base 已含 /api/v1 时,chat 仅需 /chat/completions,否则会拼成 /api/v1/v1/...
|
|
43
|
+
const defaultChat = String(resolvedBase).includes("/api/v1") ? "/chat/completions" : "/v1/chat/completions";
|
|
44
|
+
const resolvedChatPath = chatPath || loadProviderChatPath(id) || defaultChat;
|
|
45
|
+
if (!fetchImpl) fetchImpl = UndiciFetch || fetch;
|
|
46
|
+
const ring = createKeyRing(collectApiKeysGeneric(id, apiKeys, apiKey, loadProviderKeys), { cooldownMs });
|
|
47
|
+
|
|
48
|
+
let dispatcher = null;
|
|
49
|
+
let agent = null;
|
|
50
|
+
const a = createAgent({
|
|
51
|
+
keepAliveTimeout: envInt("MSLXDFF_CLINE_KEEPALIVE_TIMEOUT", 30_000),
|
|
52
|
+
keepAliveMaxTimeout: envInt("MSLXDFF_CLINE_KEEPALIVE_MAX_TIMEOUT", 60_000),
|
|
53
|
+
connections: envInt("MSLXDFF_CLINE_KEEPALIVE_CONNECTIONS", 20),
|
|
54
|
+
});
|
|
55
|
+
agent = a.agent; dispatcher = a.dispatcher;
|
|
56
|
+
|
|
57
|
+
function buildHeaders(body, key) {
|
|
58
|
+
const isStream = body?.stream !== false;
|
|
59
|
+
return {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
Accept: isStream ? "text/event-stream" : "*/*",
|
|
62
|
+
"User-Agent": "mslxdff",
|
|
63
|
+
...(key ? { Authorization: `Bearer ${key}` } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { runChat } = createChatRunner({
|
|
68
|
+
id, ring, cooldownMs, retry, fetchImpl, dispatcher, buildHeaders,
|
|
69
|
+
getUrl: () => joinUrl(resolvedBase, resolvedChatPath),
|
|
70
|
+
connectTimeoutMs,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
async function chat(body) { return runChat(body, ring, `MSLXDFF_${id.toUpperCase()}_KEY`); }
|
|
74
|
+
async function chatWithKeys(body, keys) {
|
|
75
|
+
const tmp = createKeyRing(keys, { cooldownMs });
|
|
76
|
+
return runChat(body, tmp, "shared provider keys");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function listModels() {
|
|
80
|
+
const url = joinUrl(resolvedBase, resolvedModelsPath);
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const timer = setTimeout(() => controller.abort(new Error(`${id} models timed out`)), 15_000);
|
|
83
|
+
try {
|
|
84
|
+
const headers = { Accept: "application/json" };
|
|
85
|
+
const key = ring.next();
|
|
86
|
+
if (key) headers["Authorization"] = `Bearer ${key}`;
|
|
87
|
+
const opts = { headers, signal: controller.signal };
|
|
88
|
+
if (dispatcher) opts.dispatcher = dispatcher;
|
|
89
|
+
const res = await fetchImpl(url, opts);
|
|
90
|
+
if (!res.ok) return [];
|
|
91
|
+
const json = await res.json().catch(() => ({}));
|
|
92
|
+
// 定制:cline.bot 域名时,仅取 free 数组;其他回退通用
|
|
93
|
+
if (isClineBotHost(resolvedBase) && Array.isArray(json.free)) {
|
|
94
|
+
return json.free.filter((m) => m && typeof m.id === "string").map((m) => ({ ...m, id: joinModelId(id, m.id) }));
|
|
95
|
+
}
|
|
96
|
+
const raw = Array.isArray(json.data) ? json.data : Array.isArray(json.models) ? json.models : Array.isArray(json) ? json : [];
|
|
97
|
+
return raw.filter((m) => m && typeof m.id === "string").map((m) => ({ ...m, id: joinModelId(id, m.id) }));
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { preheat } = createPreheatRunner({
|
|
106
|
+
dispatcher, fetchImpl, getUrl: () => joinUrl(resolvedBase, resolvedModelsPath),
|
|
107
|
+
id, ring, loadKeys: loadProviderKeys,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
async function close() { if (agent?.close) try { await agent.close(); } catch {} }
|
|
111
|
+
return { id, chat, chatWithKeys, listModels, preheat, close, agent, keyRing: ring, baseUrl: resolvedBase };
|
|
112
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 可扩展供应商注册表:新增特殊供应商时,仅在此文件注册 + 新增对应 provider 文件即可
|
|
3
|
+
* 每个条目:{ id, match(id, baseUrl) => bool, load() => Promise<factory> }
|
|
4
|
+
* 匹配优先级:按数组顺序,首个命中即用;未命中走通用 generic
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const customProviders = [
|
|
8
|
+
{
|
|
9
|
+
id: "workbuddy",
|
|
10
|
+
match: (id, baseUrl) => id === "workbuddy" || String(baseUrl).includes("copilot.tencent"),
|
|
11
|
+
load: () => import("./workbuddy.js").then((m) => m.createWorkbuddyProvider),
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "cline",
|
|
15
|
+
match: (id, baseUrl) => id === "cline" || id === "clinebot" || String(baseUrl).includes("cline.bot"),
|
|
16
|
+
load: () => import("./cline.js").then((m) => m.createClineProvider),
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// 供 bench/probe 等需要定制化解析模型列表的场景
|
|
21
|
+
export const customNormalizers = [
|
|
22
|
+
{
|
|
23
|
+
id: "cline",
|
|
24
|
+
match: (baseUrl) => String(baseUrl).includes("cline.bot"),
|
|
25
|
+
normalize: (json) => Array.isArray(json?.free) ? json.free : null,
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export async function getCustomProviderFactory(id, baseUrl) {
|
|
30
|
+
for (const entry of customProviders) {
|
|
31
|
+
try { if (entry.match(id, baseUrl)) return await entry.load(); } catch {}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getCustomNormalizer(baseUrl) {
|
|
37
|
+
for (const entry of customNormalizers) {
|
|
38
|
+
try { if (entry.match(baseUrl)) return entry.normalize; } catch {}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
package/src/runtime/bootstrap.js
CHANGED
|
@@ -76,24 +76,28 @@ export async function startDaemonMain(VERSION) {
|
|
|
76
76
|
const genericConfigs = loadProviderConfigs();
|
|
77
77
|
for (const [gid, cfg] of Object.entries(genericConfigs)) {
|
|
78
78
|
if (gid === "opencode" || gid === "openrouter") continue;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
const base = String(cfg?.baseUrl || "").trim();
|
|
80
|
+
const keys = Array.isArray(cfg?.keys) ? cfg.keys.filter((k) => typeof k === "string" && k.trim()) : [];
|
|
81
|
+
const auths = Array.isArray(cfg?.auths) ? cfg.auths : [];
|
|
82
|
+
// 可扩展:优先走注册表定制 provider(如 workbuddy、cline),新增供应商仅需在 registry.js 注册
|
|
83
|
+
const { getCustomProviderFactory } = await import("../providers/registry.js");
|
|
84
|
+
const customFactory = await getCustomProviderFactory(gid, base);
|
|
85
|
+
if (customFactory) {
|
|
86
|
+
if (gid === "workbuddy" && !keys.length) continue;
|
|
87
|
+
if (gid !== "workbuddy" && (!base || !keys.length)) continue;
|
|
84
88
|
try {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
const provider = gid === "workbuddy"
|
|
90
|
+
? await customFactory({ baseUrl: base || "https://copilot.tencent.com", apiKeys: keys, auths })
|
|
91
|
+
: await customFactory({ id: gid, baseUrl: base, apiKeys: keys });
|
|
92
|
+
providers.push(provider);
|
|
93
|
+
console.log(`provider enabled: ${gid} (${keys.length} key${keys.length > 1 ? "s" : ""}) baseUrl=${base || provider.baseUrl} [custom]`);
|
|
94
|
+
appendEvent({ ts: Date.now(), type: "provider-enabled", provider: gid, keys: keys.length, baseUrl: base || provider.baseUrl });
|
|
89
95
|
} catch (err) {
|
|
90
96
|
console.log(`provider ${gid} failed: ${err?.message || err}`);
|
|
91
97
|
appendEvent({ ts: Date.now(), type: "provider-error", provider: gid, error: String(err?.message || err) });
|
|
92
98
|
}
|
|
93
99
|
continue;
|
|
94
100
|
}
|
|
95
|
-
const base = String(cfg?.baseUrl || "").trim();
|
|
96
|
-
const keys = Array.isArray(cfg?.keys) ? cfg.keys.filter((k) => typeof k === "string" && k.trim()) : [];
|
|
97
101
|
if (!base || !keys.length) continue;
|
|
98
102
|
try {
|
|
99
103
|
const { createGenericProvider } = await import("../providers/generic.js");
|