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/src/models.js CHANGED
@@ -1,92 +1,128 @@
1
- const KNOWN_FREE_OPENCODE_MODELS = ["big-pickle"];
2
- const CACHE_TTL_MS = 10 * 60 * 1000;
3
-
4
- export function isFreeModel(id) {
5
- return (typeof id === "string" && id.endsWith("-free")) ||
6
- KNOWN_FREE_OPENCODE_MODELS.includes(id);
7
- }
8
-
9
- export function filterFreeModels(list) {
10
- const seen = new Set();
11
- const out = [];
12
- for (const m of list || []) {
13
- if (!(m && m.id)) continue;
14
- if (!isFreeModel(m.id)) continue;
15
- if (seen.has(m.id)) continue;
16
- seen.add(m.id);
17
- out.push(m);
18
- }
19
- return out;
20
- }
21
-
22
- export function createModelsService({ baseUrl, headers, ttlMs = CACHE_TTL_MS } = {}) {
23
- let cache = null;
24
- let fetchedAt = 0;
25
- let inflight = null;
26
-
27
- async function get() {
28
- const now = Date.now();
29
- if (cache && now - fetchedAt < ttlMs) return cache;
30
- if (inflight) return inflight;
31
-
32
- inflight = (async () => {
33
- try {
34
- const data = await fetchUpstreamModels({ baseUrl, headers });
35
- cache = data;
36
- fetchedAt = Date.now();
37
- return data;
38
- } catch (err) {
39
- // serve stale on failure if we have it, else rethrow
40
- if (cache) return cache;
41
- throw err;
42
- } finally {
43
- inflight = null;
44
- }
45
- })();
46
- return inflight;
47
- }
48
-
49
- return { get };
50
- }
51
-
52
- async function fetchUpstreamModels({ baseUrl, headers, connectTimeoutMs = 30_000 }) {
53
- const url = `${baseUrl}/zen/v1/models`;
54
- for (let attempt = 0; ; attempt++) {
55
- const res = await attemptFetch(url, headers, connectTimeoutMs);
56
- if (res instanceof Error) {
57
- if (attempt < NETWORK_RETRIES) continue;
58
- throw res;
59
- }
60
- if (isRetryable(res.status) && attempt < STATUS_RETRIES) {
61
- await sleep(2000);
62
- continue;
63
- }
64
- if (!res.ok) throw new Error(`models fetch failed: HTTP ${res.status}`);
65
- const json = await res.json().catch(() => ({}));
66
- const raw = Array.isArray(json) ? json : json.data ?? json.models ?? [];
67
- return { object: "list", data: filterFreeModels(raw) };
68
- }
69
- }
70
-
71
- async function attemptFetch(url, headers, connectTimeoutMs) {
72
- const controller = new AbortController();
73
- const timer = setTimeout(() => controller.abort(), connectTimeoutMs);
74
- try {
75
- return await fetch(url, { headers, signal: controller.signal });
76
- } catch (err) {
77
- return err;
78
- } finally {
79
- clearTimeout(timer);
80
- }
81
- }
82
-
83
- function isRetryable(status) {
84
- return status === 429 || status === 502 || status === 503 || status === 504;
85
- }
86
-
87
- function sleep(ms) {
88
- return new Promise((r) => setTimeout(r, ms));
89
- }
90
-
91
- const NETWORK_RETRIES = 2;
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
  }