mslxdff 0.1.65 → 0.1.66
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 -3159
- package/package.json +1 -1
- package/src/cli/bootstrap.js +408 -0
- package/src/cli/commands/daemon.js +151 -0
- package/src/cli/commands/group.js +272 -0
- package/src/cli/commands/model.js +405 -0
- package/src/cli/commands/provider/add.js +106 -0
- package/src/cli/commands/provider/allowlist.js +99 -0
- package/src/cli/commands/provider/config.js +82 -0
- package/src/cli/commands/provider/index.js +193 -0
- package/src/cli/commands/provider/keys.js +135 -0
- package/src/cli/commands/provider/models.js +99 -0
- package/src/cli/commands/provider.js +1 -0
- package/src/cli/commands/sync.js +143 -0
- package/src/cli/commands/system.js +236 -0
- package/src/cli/commands/workbuddy.js +91 -0
- package/src/cli/format.js +119 -0
- package/src/cli/group-helpers.js +69 -0
- package/src/cli/help.js +66 -0
- package/src/cli/index.js +59 -0
- package/src/cli/interactive.js +84 -0
- package/src/cli/policy.js +119 -0
- package/src/cli/status.js +332 -0
- package/src/cli/util.js +24 -0
- package/src/providers/base.js +159 -0
- package/src/providers/generic.js +26 -166
- package/src/providers/openrouter.js +32 -162
- package/src/providers/workbuddy.js +7 -32
- package/src/routes/chat/gateway.js +301 -0
- package/src/routes/chat/index.js +1 -384
- package/src/state/facade.js +57 -0
- package/src/state/memory.js +180 -0
- package/src/state/persist.js +42 -0
- package/src/state/schemas/allowlist.js +87 -0
- package/src/state/schemas/group.js +31 -0
- package/src/state/schemas/model.js +53 -0
- package/src/state/schemas/peer.js +31 -0
- package/src/state/schemas/port.js +10 -0
- package/src/state/schemas/provider.js +254 -0
- package/src/state/schemas/token.js +43 -0
- package/src/state/store.js +202 -0
- package/src/state.js +1 -711
package/src/providers/generic.js
CHANGED
|
@@ -1,36 +1,8 @@
|
|
|
1
|
-
import { joinModelId } from "./model-id.js";
|
|
2
1
|
import { createKeyRing } from "./keyring.js";
|
|
3
2
|
import { loadProviderKeys, loadProviderBaseUrl, loadProviderModelsPath, loadProviderChatPath } from "../state.js";
|
|
3
|
+
import { envInt, joinUrl, getUndici, createAgent, collectApiKeysGeneric, createChatRunner, createListModelsRunner, createPreheatRunner } from "./base.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
const b = String(base || "").trim().replace(/\/+$/, "");
|
|
7
|
-
const p = String(path || "").trim();
|
|
8
|
-
if (!p) return b;
|
|
9
|
-
const pp = p.startsWith("/") ? p : `/${p}`;
|
|
10
|
-
return `${b}${pp}`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
let UndiciAgent = null;
|
|
14
|
-
let UndiciFetch = null;
|
|
15
|
-
try {
|
|
16
|
-
const mod = await import("undici");
|
|
17
|
-
UndiciAgent = mod.Agent;
|
|
18
|
-
UndiciFetch = mod.fetch;
|
|
19
|
-
} catch {}
|
|
20
|
-
|
|
21
|
-
function envInt(name, fallback) {
|
|
22
|
-
const v = Number(process.env[name]);
|
|
23
|
-
return Number.isInteger(v) && v > 0 ? v : fallback;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function collectApiKeys(id, apiKeys, apiKey) {
|
|
27
|
-
const list = [
|
|
28
|
-
...(Array.isArray(apiKeys) ? apiKeys : [apiKeys].filter(Boolean)),
|
|
29
|
-
apiKey,
|
|
30
|
-
...(apiKeys === undefined && apiKey === undefined ? loadProviderKeys(id) : []),
|
|
31
|
-
].filter((k) => typeof k === "string" && k.trim().length);
|
|
32
|
-
return [...new Set(list.map((k) => k.trim()))];
|
|
33
|
-
}
|
|
5
|
+
const { UndiciFetch } = getUndici();
|
|
34
6
|
|
|
35
7
|
function resolveBaseUrl(id, baseUrl) {
|
|
36
8
|
if (baseUrl) return String(baseUrl).trim().replace(/\/+$/, "");
|
|
@@ -66,169 +38,57 @@ export function createGenericProvider({
|
|
|
66
38
|
if (!fetchImpl) fetchImpl = UndiciFetch || fetch;
|
|
67
39
|
const resolvedModelsPath = modelsPath || loadProviderModelsPath(id, file ? { file } : {});
|
|
68
40
|
const resolvedChatPath = chatPath || loadProviderChatPath(id, file ? { file } : {});
|
|
69
|
-
|
|
70
|
-
const ring = createKeyRing(collectApiKeys(id, apiKeys, apiKey), { cooldownMs });
|
|
41
|
+
const ring = createKeyRing(collectApiKeysGeneric(id, apiKeys, apiKey, loadProviderKeys), { cooldownMs });
|
|
71
42
|
|
|
72
43
|
let dispatcher = null;
|
|
73
44
|
let agent = null;
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
});
|
|
82
|
-
dispatcher = agent;
|
|
83
|
-
} catch {}
|
|
45
|
+
if (!noAgent) {
|
|
46
|
+
const a = createAgent({
|
|
47
|
+
keepAliveTimeout: envInt("MSLXDFF_GENERIC_KEEPALIVE_TIMEOUT", 30_000),
|
|
48
|
+
keepAliveMaxTimeout: envInt("MSLXDFF_GENERIC_KEEPALIVE_MAX_TIMEOUT", 60_000),
|
|
49
|
+
connections: envInt("MSLXDFF_GENERIC_KEEPALIVE_CONNECTIONS", 20),
|
|
50
|
+
});
|
|
51
|
+
agent = a.agent; dispatcher = a.dispatcher;
|
|
84
52
|
}
|
|
85
53
|
|
|
86
54
|
function buildHeaders(body, key) {
|
|
87
55
|
const isStream = body?.stream !== false;
|
|
88
56
|
const h = {
|
|
89
57
|
"Content-Type": "application/json",
|
|
90
|
-
|
|
58
|
+
Accept: isStream ? "text/event-stream" : "*/*",
|
|
91
59
|
"User-Agent": "mslxdff",
|
|
92
60
|
};
|
|
93
61
|
if (key) h["Authorization"] = `Bearer ${key}`;
|
|
94
62
|
return { ...h, ...extraHeaders };
|
|
95
63
|
}
|
|
96
64
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (dispatcher) opts.dispatcher = dispatcher;
|
|
103
|
-
const res = await fetchImpl(url, opts);
|
|
104
|
-
if (res.status === 401 && !activeRing.size) return { __needKey: true };
|
|
105
|
-
return res;
|
|
106
|
-
} catch (err) {
|
|
107
|
-
return err;
|
|
108
|
-
} finally {
|
|
109
|
-
clearTimeout(timer);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function runChat(body, activeRing, sourceKey) {
|
|
114
|
-
const url = joinUrl(resolvedBase, resolvedChatPath);
|
|
115
|
-
const t0 = performance.now();
|
|
116
|
-
const attempts = [];
|
|
117
|
-
let waitMs = 0;
|
|
118
|
-
const key = activeRing.next();
|
|
119
|
-
if (!key && activeRing.size > 0) {
|
|
120
|
-
const err = new Error(`${id}: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
|
|
121
|
-
err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
|
|
122
|
-
throw err;
|
|
123
|
-
}
|
|
124
|
-
for (let attempt = 0; ; attempt++) {
|
|
125
|
-
const t = performance.now();
|
|
126
|
-
const result = await attemptOnce(url, body, key, activeRing);
|
|
127
|
-
const type = result instanceof Error ? "network" : `http${result?.status}`;
|
|
128
|
-
attempts.push({ attempt, type, ms: Math.round(performance.now() - t) });
|
|
129
|
-
if (result?.__needKey) {
|
|
130
|
-
const err = new Error(`${id}: missing ${sourceKey} (chat requires a real key)`);
|
|
131
|
-
err._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
132
|
-
throw err;
|
|
133
|
-
}
|
|
134
|
-
if (result instanceof Error) {
|
|
135
|
-
const entry = retry?.network;
|
|
136
|
-
if (entry && attempt < entry.attempts) {
|
|
137
|
-
await sleep(entry.delayMs);
|
|
138
|
-
waitMs += entry.delayMs;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
activeRing.onError(key);
|
|
142
|
-
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
143
|
-
throw result;
|
|
144
|
-
}
|
|
145
|
-
const entry = retry?.[result.status];
|
|
146
|
-
if (entry && attempt < entry.attempts) {
|
|
147
|
-
await sleep(entry.delayMs);
|
|
148
|
-
waitMs += entry.delayMs;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
|
|
152
|
-
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
153
|
-
return result;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
65
|
+
const { runChat } = createChatRunner({
|
|
66
|
+
id, ring, cooldownMs, retry, fetchImpl, dispatcher, buildHeaders,
|
|
67
|
+
getUrl: () => joinUrl(resolvedBase, resolvedChatPath),
|
|
68
|
+
connectTimeoutMs,
|
|
69
|
+
});
|
|
156
70
|
|
|
157
71
|
async function chat(body) {
|
|
158
72
|
return runChat(body, ring, `MSLXDFF_${id.toUpperCase()}_KEY`);
|
|
159
73
|
}
|
|
160
|
-
|
|
161
74
|
async function chatWithKeys(body, keys) {
|
|
162
75
|
const tmp = createKeyRing(keys, { cooldownMs });
|
|
163
76
|
return runChat(body, tmp, "shared provider keys");
|
|
164
77
|
}
|
|
165
78
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const now = Date.now();
|
|
171
|
-
if (cache && now - fetchedAt < CACHE_TTL_MS) return cache;
|
|
172
|
-
const url = joinUrl(resolvedBase, resolvedModelsPath);
|
|
173
|
-
const controller = new AbortController();
|
|
174
|
-
const timer = setTimeout(() => controller.abort(new Error(`${id} models timed out`)), 15_000);
|
|
175
|
-
try {
|
|
176
|
-
const headers = { Accept: "application/json" };
|
|
177
|
-
const key = ring.next();
|
|
178
|
-
if (key) headers["Authorization"] = `Bearer ${key}`;
|
|
179
|
-
const opts = { headers, signal: controller.signal };
|
|
180
|
-
if (dispatcher) opts.dispatcher = dispatcher;
|
|
181
|
-
const res = await fetchImpl(url, opts);
|
|
182
|
-
if (!res.ok) return [];
|
|
183
|
-
const json = await res.json().catch(() => ({}));
|
|
184
|
-
const raw = Array.isArray(json.data) ? json.data : Array.isArray(json) ? json : [];
|
|
185
|
-
// 通用:不过滤 pricing,全部前缀化
|
|
186
|
-
cache = raw.filter((m) => m && typeof m.id === "string").map((m) => ({ ...m, id: joinModelId(id, m.id) }));
|
|
187
|
-
fetchedAt = now;
|
|
188
|
-
return cache;
|
|
189
|
-
} catch {
|
|
190
|
-
return [];
|
|
191
|
-
} finally {
|
|
192
|
-
clearTimeout(timer);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
79
|
+
const { listModels } = createListModelsRunner({
|
|
80
|
+
id, ring, dispatcher, fetchImpl,
|
|
81
|
+
getUrl: () => joinUrl(resolvedBase, resolvedModelsPath),
|
|
82
|
+
});
|
|
195
83
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const headers = { Accept: "application/json" };
|
|
201
|
-
const key = loadProviderKeys(id)[0] || ring.next();
|
|
202
|
-
if (key) headers["Authorization"] = `Bearer ${key}`;
|
|
203
|
-
const opts = { headers };
|
|
204
|
-
if (dispatcher) opts.dispatcher = dispatcher;
|
|
205
|
-
const res = await fetchImpl(url, opts);
|
|
206
|
-
try { if (res.body) await res.text().catch(() => {}); } catch {}
|
|
207
|
-
return { ok: res.ok, status: res.status, ms: Math.round(performance.now() - t0) };
|
|
208
|
-
} catch (err) {
|
|
209
|
-
return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
|
|
210
|
-
}
|
|
211
|
-
}
|
|
84
|
+
const { preheat } = createPreheatRunner({
|
|
85
|
+
dispatcher, fetchImpl, getUrl: () => joinUrl(resolvedBase, resolvedModelsPath),
|
|
86
|
+
id, ring, loadKeys: loadProviderKeys,
|
|
87
|
+
});
|
|
212
88
|
|
|
213
89
|
async function close() {
|
|
214
|
-
if (agent && typeof agent.close === "function") {
|
|
215
|
-
try { await agent.close(); } catch {}
|
|
216
|
-
}
|
|
90
|
+
if (agent && typeof agent.close === "function") { try { await agent.close(); } catch {} }
|
|
217
91
|
}
|
|
218
92
|
|
|
219
|
-
return {
|
|
220
|
-
id,
|
|
221
|
-
chat,
|
|
222
|
-
chatWithKeys,
|
|
223
|
-
listModels,
|
|
224
|
-
preheat,
|
|
225
|
-
close,
|
|
226
|
-
agent,
|
|
227
|
-
keyRing: ring,
|
|
228
|
-
baseUrl: resolvedBase,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function sleep(ms) {
|
|
233
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
93
|
+
return { id, chat, chatWithKeys, listModels, preheat, close, agent, keyRing: ring, baseUrl: resolvedBase };
|
|
234
94
|
}
|
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import { joinModelId } from "./model-id.js";
|
|
2
1
|
import { createKeyRing } from "./keyring.js";
|
|
3
2
|
import { loadProviderKeys } from "../state.js";
|
|
3
|
+
import { envInt, joinUrl, getUndici, createAgent, createChatRunner, createListModelsRunner, createPreheatRunner } from "./base.js";
|
|
4
|
+
import { joinModelId } from "./model-id.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
let UndiciFetch = null;
|
|
7
|
-
try {
|
|
8
|
-
const mod = await import("undici");
|
|
9
|
-
UndiciAgent = mod.Agent;
|
|
10
|
-
UndiciFetch = mod.fetch;
|
|
11
|
-
} catch {}
|
|
12
|
-
|
|
6
|
+
const { UndiciFetch } = getUndici();
|
|
13
7
|
const DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
|
|
14
8
|
|
|
15
|
-
function envInt(name, fallback) {
|
|
16
|
-
const v = Number(process.env[name]);
|
|
17
|
-
return Number.isInteger(v) && v > 0 ? v : fallback;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// 把 `apiKeys`(数组) + `apiKey`(单串) + state 的 key 合并成去重数组。
|
|
21
|
-
// state 为空时只看显式传入;两者皆空 → [](无 key)。
|
|
22
9
|
function collectApiKeys(apiKeys, apiKey) {
|
|
23
10
|
const list = [
|
|
24
11
|
...(Array.isArray(apiKeys) ? apiKeys : [apiKeys].filter(Boolean)),
|
|
@@ -46,172 +33,55 @@ export function createOpenRouterProvider({
|
|
|
46
33
|
noAgent = false,
|
|
47
34
|
} = {}) {
|
|
48
35
|
if (!fetchImpl) fetchImpl = UndiciFetch || fetch;
|
|
49
|
-
|
|
50
36
|
const ring = createKeyRing(collectApiKeys(apiKeys, apiKey), { cooldownMs });
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
connections: envInt("MSLXDFF_OA_KEEPALIVE_CONNECTIONS", 20),
|
|
60
|
-
pipelining: 1,
|
|
61
|
-
});
|
|
62
|
-
dispatcher = agent;
|
|
63
|
-
} catch {}
|
|
37
|
+
let dispatcher = null; let agent = null;
|
|
38
|
+
if (!noAgent) {
|
|
39
|
+
const a = createAgent({
|
|
40
|
+
keepAliveTimeout: envInt("MSLXDFF_OA_KEEPALIVE_TIMEOUT", 30_000),
|
|
41
|
+
keepAliveMaxTimeout: envInt("MSLXDFF_OA_KEEPALIVE_MAX_TIMEOUT", 60_000),
|
|
42
|
+
connections: envInt("MSLXDFF_OA_KEEPALIVE_CONNECTIONS", 20),
|
|
43
|
+
});
|
|
44
|
+
agent = a.agent; dispatcher = a.dispatcher;
|
|
64
45
|
}
|
|
65
46
|
|
|
66
47
|
function buildHeaders(body, key) {
|
|
67
48
|
const isStream = body?.stream !== false;
|
|
68
49
|
const h = {
|
|
69
50
|
"Content-Type": "application/json",
|
|
70
|
-
|
|
51
|
+
Accept: isStream ? "text/event-stream" : "*/*",
|
|
71
52
|
"User-Agent": "mslxdff",
|
|
53
|
+
"HTTP-Referer": process.env.MSLXDFF_OPENROUTER_REFERER || "https://github.com/mslxdff",
|
|
54
|
+
"X-Title": process.env.MSLXDFF_OPENROUTER_TITLE || "mslxdff",
|
|
72
55
|
};
|
|
73
56
|
if (key) h["Authorization"] = `Bearer ${key}`;
|
|
74
|
-
h["HTTP-Referer"] = process.env.MSLXDFF_OPENROUTER_REFERER || "https://github.com/mslxdff";
|
|
75
|
-
h["X-Title"] = process.env.MSLXDFF_OPENROUTER_TITLE || "mslxdff";
|
|
76
57
|
return { ...h, ...extraHeaders };
|
|
77
58
|
}
|
|
78
59
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (dispatcher) opts.dispatcher = dispatcher;
|
|
85
|
-
const res = await fetchImpl(url, opts);
|
|
86
|
-
if (res.status === 401 && !activeRing.size) return { __needKey: true };
|
|
87
|
-
return res;
|
|
88
|
-
} catch (err) {
|
|
89
|
-
return err;
|
|
90
|
-
} finally {
|
|
91
|
-
clearTimeout(timer);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function runChat(body, activeRing, sourceKey) {
|
|
96
|
-
const url = `${baseUrl}/chat/completions`;
|
|
97
|
-
const t0 = performance.now();
|
|
98
|
-
const attempts = [];
|
|
99
|
-
let waitMs = 0;
|
|
60
|
+
const { runChat } = createChatRunner({
|
|
61
|
+
id: "openrouter", ring, cooldownMs, retry, fetchImpl, dispatcher, buildHeaders,
|
|
62
|
+
getUrl: () => `${baseUrl}/chat/completions`,
|
|
63
|
+
connectTimeoutMs,
|
|
64
|
+
});
|
|
100
65
|
|
|
101
|
-
|
|
102
|
-
// 没有配置任何 key 时不在这拦截——下降到 401 无 key 报错路径(__needKey)。
|
|
103
|
-
const key = activeRing.next();
|
|
104
|
-
if (!key && activeRing.size > 0) {
|
|
105
|
-
const err = new Error(`openrouter: all API keys are in cooldown (last error < ${cooldownMs}ms ago) — provider temporarily unavailable`);
|
|
106
|
-
err._t = { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0), cooldownMs };
|
|
107
|
-
throw err;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
for (let attempt = 0; ; attempt++) {
|
|
111
|
-
const t = performance.now();
|
|
112
|
-
const result = await attemptOnce(url, body, key, activeRing);
|
|
113
|
-
const type = result instanceof Error ? "network" : `http${result?.status}`;
|
|
114
|
-
attempts.push({ attempt, type, ms: Math.round(performance.now() - t) });
|
|
115
|
-
if (result?.__needKey) {
|
|
116
|
-
const err = new Error(`openrouter: missing ${sourceKey} (chat requires a real key)`);
|
|
117
|
-
err._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
118
|
-
throw err;
|
|
119
|
-
}
|
|
120
|
-
if (result instanceof Error) {
|
|
121
|
-
const entry = retry?.network;
|
|
122
|
-
if (entry && attempt < entry.attempts) {
|
|
123
|
-
await sleep(entry.delayMs);
|
|
124
|
-
waitMs += entry.delayMs;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
activeRing.onError(key);
|
|
128
|
-
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
129
|
-
throw result;
|
|
130
|
-
}
|
|
131
|
-
const entry = retry?.[result.status];
|
|
132
|
-
if (entry && attempt < entry.attempts) {
|
|
133
|
-
await sleep(entry.delayMs);
|
|
134
|
-
waitMs += entry.delayMs;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (result.status === 401 || result.status === 403 || result.status === 429 || result.status >= 500) activeRing.onError(key);
|
|
138
|
-
result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
|
|
139
|
-
return result;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function chat(body) {
|
|
144
|
-
return runChat(body, ring, "MSLXDFF_OPENROUTER_KEY");
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ADR-0008 瞬时共享:用组员方传来的临时 key 发布请求(不污染本 provider 的持久 ring)。
|
|
66
|
+
async function chat(body) { return runChat(body, ring, "MSLXDFF_OPENROUTER_KEY"); }
|
|
148
67
|
async function chatWithKeys(body, keys) {
|
|
149
68
|
const tmp = createKeyRing(keys, { cooldownMs });
|
|
150
69
|
return runChat(body, tmp, "shared provider keys");
|
|
151
70
|
}
|
|
152
71
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (cache && now - fetchedAt < CACHE_TTL_MS) return cache;
|
|
160
|
-
const url = `${baseUrl}/models`;
|
|
161
|
-
const controller = new AbortController();
|
|
162
|
-
const timer = setTimeout(() => controller.abort(new Error("openrouter models timed out")), 15_000);
|
|
163
|
-
try {
|
|
164
|
-
const opts = { headers: { Accept: "application/json" }, signal: controller.signal };
|
|
165
|
-
if (dispatcher) opts.dispatcher = dispatcher;
|
|
166
|
-
const res = await fetchImpl(url, opts);
|
|
167
|
-
if (!res.ok) return [];
|
|
168
|
-
const json = await res.json().catch(() => ({}));
|
|
169
|
-
const raw = Array.isArray(json.data) ? json.data : [];
|
|
170
|
-
const free = raw.filter((m) =>
|
|
171
|
-
Number(m.pricing?.prompt || 0) === 0 && Number(m.pricing?.completion || 0) === 0
|
|
172
|
-
);
|
|
173
|
-
cache = free.map((m) => ({ ...m, id: joinModelId("openrouter", m.id) }));
|
|
174
|
-
fetchedAt = now;
|
|
175
|
-
return cache;
|
|
176
|
-
} catch {
|
|
177
|
-
return [];
|
|
178
|
-
} finally {
|
|
179
|
-
clearTimeout(timer);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
72
|
+
const { listModels } = createListModelsRunner({
|
|
73
|
+
id: "openrouter", ring, dispatcher, fetchImpl,
|
|
74
|
+
getUrl: () => `${baseUrl}/models`,
|
|
75
|
+
// openrouter 仅暴露免费模型
|
|
76
|
+
mapModel: (raw) => raw.filter((m) => Number(m.pricing?.prompt || 0) === 0 && Number(m.pricing?.completion || 0) === 0),
|
|
77
|
+
});
|
|
182
78
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const opts = { headers: { Accept: "application/json" } };
|
|
188
|
-
if (dispatcher) opts.dispatcher = dispatcher;
|
|
189
|
-
const res = await fetchImpl(url, opts);
|
|
190
|
-
try { if (res.body) await res.text().catch(() => {}); } catch {}
|
|
191
|
-
return { ok: res.ok, status: res.status, ms: Math.round(performance.now() - t0) };
|
|
192
|
-
} catch (err) {
|
|
193
|
-
return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
|
|
194
|
-
}
|
|
195
|
-
}
|
|
79
|
+
const { preheat } = createPreheatRunner({
|
|
80
|
+
dispatcher, fetchImpl, getUrl: () => `${baseUrl}/models`,
|
|
81
|
+
id: "openrouter", ring,
|
|
82
|
+
});
|
|
196
83
|
|
|
197
|
-
async function close() {
|
|
198
|
-
if (agent && typeof agent.close === "function") {
|
|
199
|
-
try { await agent.close(); } catch {}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
84
|
+
async function close() { if (agent?.close) { try { await agent.close(); } catch {} } }
|
|
202
85
|
|
|
203
|
-
return {
|
|
204
|
-
id: "openrouter",
|
|
205
|
-
chat,
|
|
206
|
-
chatWithKeys,
|
|
207
|
-
listModels,
|
|
208
|
-
preheat,
|
|
209
|
-
close,
|
|
210
|
-
agent,
|
|
211
|
-
keyRing: ring,
|
|
212
|
-
};
|
|
86
|
+
return { id: "openrouter", chat, chatWithKeys, listModels, preheat, close, agent, keyRing: ring };
|
|
213
87
|
}
|
|
214
|
-
|
|
215
|
-
function sleep(ms) {
|
|
216
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
217
|
-
}
|
|
@@ -6,25 +6,8 @@ import { join, dirname } from "node:path";
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { getCachedBalance, setCachedBalance } from "./workbuddy-balance.js";
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
const mod = await import("undici");
|
|
13
|
-
UndiciAgent = mod.Agent;
|
|
14
|
-
UndiciFetch = mod.fetch;
|
|
15
|
-
} catch {}
|
|
16
|
-
|
|
17
|
-
function joinUrl(base, path) {
|
|
18
|
-
const b = String(base || "").trim().replace(/\/+$/, "");
|
|
19
|
-
const p = String(path || "").trim();
|
|
20
|
-
if (!p) return b;
|
|
21
|
-
const pp = p.startsWith("/") ? p : `/${p}`;
|
|
22
|
-
return `${b}${pp}`;
|
|
23
|
-
}
|
|
24
|
-
function envInt(name, fallback) {
|
|
25
|
-
const v = Number(process.env[name]);
|
|
26
|
-
return Number.isInteger(v) && v > 0 ? v : fallback;
|
|
27
|
-
}
|
|
9
|
+
import { envInt, joinUrl, getUndici, createAgent } from "./base.js";
|
|
10
|
+
const { UndiciAgent, UndiciFetch } = getUndici();
|
|
28
11
|
function isTestEnv() {
|
|
29
12
|
if (process.env.NODE_ENV === "test") return true;
|
|
30
13
|
if (process.env.MSLXDFF_STATE_FILE && String(process.env.MSLXDFF_STATE_FILE).includes("mslxdff-test")) return true;
|
|
@@ -210,19 +193,11 @@ export function createWorkbuddyProvider({
|
|
|
210
193
|
|
|
211
194
|
let ring = createKeyRing(keys, { cooldownMs });
|
|
212
195
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
keepAliveTimeout: envInt("MSLXDFF_WORKBUDDY_KEEPALIVE_TIMEOUT", 30_000),
|
|
219
|
-
keepAliveMaxTimeout: envInt("MSLXDFF_WORKBUDDY_KEEPALIVE_MAX_TIMEOUT", 60_000),
|
|
220
|
-
connections: envInt("MSLXDFF_WORKBUDDY_KEEPALIVE_CONNECTIONS", 20),
|
|
221
|
-
pipelining: 1,
|
|
222
|
-
});
|
|
223
|
-
dispatcher = agent;
|
|
224
|
-
} catch {}
|
|
225
|
-
}
|
|
196
|
+
const { agent, dispatcher } = createAgent({
|
|
197
|
+
keepAliveTimeout: envInt("MSLXDFF_WORKBUDDY_KEEPALIVE_TIMEOUT", 30_000),
|
|
198
|
+
keepAliveMaxTimeout: envInt("MSLXDFF_WORKBUDDY_KEEPALIVE_MAX_TIMEOUT", 60_000),
|
|
199
|
+
connections: envInt("MSLXDFF_WORKBUDDY_KEEPALIVE_CONNECTIONS", 20),
|
|
200
|
+
});
|
|
226
201
|
|
|
227
202
|
function authForKey(key) {
|
|
228
203
|
const idx = keys.indexOf(key);
|