mslxdff 0.1.1 → 0.1.3
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/README.md +157 -83
- package/bin/mslxdff.js +576 -0
- package/package.json +26 -26
- package/src/auto.js +99 -0
- package/src/daemon.js +63 -63
- package/src/groups.js +189 -0
- package/src/logs.js +80 -0
- package/src/models.js +127 -91
- package/src/peers.js +83 -0
- package/src/reasoning.js +32 -32
- package/src/routes.js +309 -125
- package/src/server.js +37 -27
- package/src/state.js +119 -39
- package/src/upstream.js +67 -67
- package/bin/mslxdfree.js +0 -78
package/src/models.js
CHANGED
|
@@ -1,92 +1,128 @@
|
|
|
1
|
-
const KNOWN_FREE_OPENCODE_MODELS = ["big-pickle"];
|
|
2
|
-
const CACHE_TTL_MS =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
1
|
+
const KNOWN_FREE_OPENCODE_MODELS = ["big-pickle"];
|
|
2
|
+
const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
3
|
+
const DEFAULT_REFRESH_MS = 2 * 60 * 60 * 1000;
|
|
4
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
|
|
7
|
+
export function isFreeModel(id) {
|
|
8
|
+
return (typeof id === "string" && id.endsWith("-free")) ||
|
|
9
|
+
KNOWN_FREE_OPENCODE_MODELS.includes(id);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function filterFreeModels(list) {
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const m of list || []) {
|
|
16
|
+
if (!(m && m.id)) continue;
|
|
17
|
+
if (!isFreeModel(m.id)) continue;
|
|
18
|
+
if (seen.has(m.id)) continue;
|
|
19
|
+
seen.add(m.id);
|
|
20
|
+
out.push(m);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createModelsService({ baseUrl, headers, ttlMs = CACHE_TTL_MS, refreshMs = DEFAULT_REFRESH_MS, cacheFile } = {}) {
|
|
26
|
+
let cache = null;
|
|
27
|
+
let fetchedAt = 0;
|
|
28
|
+
let inflight = null;
|
|
29
|
+
let timer = null;
|
|
30
|
+
|
|
31
|
+
async function load() {
|
|
32
|
+
const data = await fetchUpstreamModels({ baseUrl, headers });
|
|
33
|
+
cache = data;
|
|
34
|
+
fetchedAt = Date.now();
|
|
35
|
+
if (cacheFile) persistModels(data, cacheFile);
|
|
36
|
+
return data;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function get() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (cache && now - fetchedAt < ttlMs) return cache;
|
|
42
|
+
if (inflight) return inflight;
|
|
43
|
+
|
|
44
|
+
inflight = (async () => {
|
|
45
|
+
try {
|
|
46
|
+
return await load();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// serve stale on failure if we have it, else rethrow
|
|
49
|
+
if (cache) return cache;
|
|
50
|
+
throw err;
|
|
51
|
+
} finally {
|
|
52
|
+
inflight = null;
|
|
53
|
+
}
|
|
54
|
+
})();
|
|
55
|
+
return inflight;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function startAutoRefresh(intervalMs = refreshMs) {
|
|
59
|
+
if (timer) return stopAutoRefresh;
|
|
60
|
+
timer = setInterval(() => {
|
|
61
|
+
void load().catch(() => {
|
|
62
|
+
// keep serving stale cache on background refresh failure
|
|
63
|
+
});
|
|
64
|
+
}, intervalMs);
|
|
65
|
+
timer.unref?.();
|
|
66
|
+
return stopAutoRefresh;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stopAutoRefresh() {
|
|
70
|
+
if (timer) {
|
|
71
|
+
clearInterval(timer);
|
|
72
|
+
timer = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { get, startAutoRefresh, stopAutoRefresh };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function fetchUpstreamModels({ baseUrl, headers, connectTimeoutMs = 30_000 }) {
|
|
80
|
+
const url = `${baseUrl}/zen/v1/models`;
|
|
81
|
+
for (let attempt = 0; ; attempt++) {
|
|
82
|
+
const res = await attemptFetch(url, headers, connectTimeoutMs);
|
|
83
|
+
if (res instanceof Error) {
|
|
84
|
+
if (attempt < NETWORK_RETRIES) continue;
|
|
85
|
+
throw res;
|
|
86
|
+
}
|
|
87
|
+
if (isRetryable(res.status) && attempt < STATUS_RETRIES) {
|
|
88
|
+
await sleep(2000);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!res.ok) throw new Error(`models fetch failed: HTTP ${res.status}`);
|
|
92
|
+
const json = await res.json().catch(() => ({}));
|
|
93
|
+
const raw = Array.isArray(json) ? json : json.data ?? json.models ?? [];
|
|
94
|
+
return { object: "list", data: filterFreeModels(raw) };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function attemptFetch(url, headers, connectTimeoutMs) {
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const timer = setTimeout(() => controller.abort(), connectTimeoutMs);
|
|
101
|
+
try {
|
|
102
|
+
return await fetch(url, { headers, signal: controller.signal });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return err;
|
|
105
|
+
} finally {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isRetryable(status) {
|
|
111
|
+
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function persistModels(data, cacheFile) {
|
|
115
|
+
try {
|
|
116
|
+
mkdirSync(dirname(cacheFile), { recursive: true });
|
|
117
|
+
writeFileSync(cacheFile, JSON.stringify({ cachedAt: Date.now(), ...data }));
|
|
118
|
+
} catch {
|
|
119
|
+
// persistence is best-effort
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sleep(ms) {
|
|
124
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const NETWORK_RETRIES = 2;
|
|
92
128
|
const STATUS_RETRIES = 2;
|
package/src/peers.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { loadPeers, savePeers, loadPeerErrors, savePeerErrors } from "./state.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PEER_COOLDOWN_MS = 30_000;
|
|
4
|
+
export const DEFAULT_MAX_HOPS = 3;
|
|
5
|
+
|
|
6
|
+
export function normalizePeerUrl(url) {
|
|
7
|
+
return String(url || "").trim().replace(/\/+$/, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createPeersService({
|
|
11
|
+
file,
|
|
12
|
+
now = () => Date.now(),
|
|
13
|
+
cooldownMs = DEFAULT_PEER_COOLDOWN_MS,
|
|
14
|
+
peers: seedPeers,
|
|
15
|
+
errors: seedErrors,
|
|
16
|
+
persistPeers = (list, f = file) => savePeers(list, f ? { file: f } : {}),
|
|
17
|
+
persistErrors = (errors, f = file) => savePeerErrors(errors, f ? { file: f } : {}),
|
|
18
|
+
} = {}) {
|
|
19
|
+
const list = (seedPeers ?? loadPeers(file ? { file } : {}))
|
|
20
|
+
.map((p) => ({ ...p, url: normalizePeerUrl(p.url) }))
|
|
21
|
+
.filter((p) => p && p.url);
|
|
22
|
+
const lastErrorAt = { ...(seedErrors ?? loadPeerErrors(file ? { file } : {})) };
|
|
23
|
+
|
|
24
|
+
function all() {
|
|
25
|
+
return [...list];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function add(peer) {
|
|
29
|
+
const url = normalizePeerUrl(peer?.url);
|
|
30
|
+
if (!url) return false;
|
|
31
|
+
const existing = list.find((p) => p.url === url);
|
|
32
|
+
const entry = { ...peer, url };
|
|
33
|
+
if (existing) Object.assign(existing, entry);
|
|
34
|
+
else list.push(entry);
|
|
35
|
+
persistPeers([...list]);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function remove(url) {
|
|
40
|
+
const target = normalizePeerUrl(url);
|
|
41
|
+
const idx = list.findIndex((p) => p.url === target);
|
|
42
|
+
if (idx < 0) return false;
|
|
43
|
+
list.splice(idx, 1);
|
|
44
|
+
persistPeers([...list]);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function removeByGroup(group) {
|
|
49
|
+
const before = list.length;
|
|
50
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
51
|
+
if (list[i].group === group) list.splice(i, 1);
|
|
52
|
+
}
|
|
53
|
+
if (list.length !== before) persistPeers([...list]);
|
|
54
|
+
return before - list.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isCooling(url) {
|
|
58
|
+
if (!cooldownMs) return false;
|
|
59
|
+
const err = lastErrorAt[url];
|
|
60
|
+
return typeof err === "number" && now() - err < cooldownMs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function available() {
|
|
64
|
+
return list.filter((p) => !isCooling(p.url));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let cursor = 0;
|
|
68
|
+
|
|
69
|
+
function next() {
|
|
70
|
+
const avail = available();
|
|
71
|
+
if (!avail.length) return null;
|
|
72
|
+
cursor = cursor % avail.length;
|
|
73
|
+
return avail[cursor++];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function recordError(url) {
|
|
77
|
+
if (!url) return;
|
|
78
|
+
lastErrorAt[url] = now();
|
|
79
|
+
await persistErrors({ ...lastErrorAt });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { all, add, remove, removeByGroup, isCooling, available, next, recordError, errors: () => ({ ...lastErrorAt }) };
|
|
83
|
+
}
|
package/src/reasoning.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
const PLACEHOLDER = " ";
|
|
2
|
-
|
|
3
|
-
const MODEL_RULES = [
|
|
4
|
-
{ match: (m) => /^kimi-/i.test(m || ""), scope: "toolCalls" },
|
|
5
|
-
{ match: (m) => /deepseek/i.test(m || ""), scope: "all" },
|
|
6
|
-
];
|
|
7
|
-
|
|
8
|
-
export function normalizeModel(model) {
|
|
9
|
-
return model.startsWith("oc/") ? model.slice(3) : model;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function shouldInject(message, scope) {
|
|
13
|
-
if (message?.role !== "assistant") return false;
|
|
14
|
-
const rc = message.reasoning_content;
|
|
15
|
-
if (typeof rc === "string" && rc.length > 0) return false;
|
|
16
|
-
if (scope === "toolCalls") {
|
|
17
|
-
return Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
18
|
-
}
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function applyRule(body, rule) {
|
|
23
|
-
if (!rule || !body?.messages) return body;
|
|
24
|
-
const messages = body.messages.map((m) =>
|
|
25
|
-
shouldInject(m, rule.scope) ? { ...m, reasoning_content: PLACEHOLDER } : m
|
|
26
|
-
);
|
|
27
|
-
return { ...body, messages };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function injectReasoningContent(model, body) {
|
|
31
|
-
const rule = MODEL_RULES.find((r) => r.match(model));
|
|
32
|
-
return applyRule(body, rule);
|
|
1
|
+
const PLACEHOLDER = " ";
|
|
2
|
+
|
|
3
|
+
const MODEL_RULES = [
|
|
4
|
+
{ match: (m) => /^kimi-/i.test(m || ""), scope: "toolCalls" },
|
|
5
|
+
{ match: (m) => /deepseek/i.test(m || ""), scope: "all" },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export function normalizeModel(model) {
|
|
9
|
+
return model.startsWith("oc/") ? model.slice(3) : model;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function shouldInject(message, scope) {
|
|
13
|
+
if (message?.role !== "assistant") return false;
|
|
14
|
+
const rc = message.reasoning_content;
|
|
15
|
+
if (typeof rc === "string" && rc.length > 0) return false;
|
|
16
|
+
if (scope === "toolCalls") {
|
|
17
|
+
return Array.isArray(message.tool_calls) && message.tool_calls.length > 0;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function applyRule(body, rule) {
|
|
23
|
+
if (!rule || !body?.messages) return body;
|
|
24
|
+
const messages = body.messages.map((m) =>
|
|
25
|
+
shouldInject(m, rule.scope) ? { ...m, reasoning_content: PLACEHOLDER } : m
|
|
26
|
+
);
|
|
27
|
+
return { ...body, messages };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function injectReasoningContent(model, body) {
|
|
31
|
+
const rule = MODEL_RULES.find((r) => r.match(model));
|
|
32
|
+
return applyRule(body, rule);
|
|
33
33
|
}
|