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.
Files changed (42) hide show
  1. package/bin/mslxdff.js +3 -3159
  2. package/package.json +1 -1
  3. package/src/cli/bootstrap.js +408 -0
  4. package/src/cli/commands/daemon.js +151 -0
  5. package/src/cli/commands/group.js +272 -0
  6. package/src/cli/commands/model.js +405 -0
  7. package/src/cli/commands/provider/add.js +106 -0
  8. package/src/cli/commands/provider/allowlist.js +99 -0
  9. package/src/cli/commands/provider/config.js +82 -0
  10. package/src/cli/commands/provider/index.js +193 -0
  11. package/src/cli/commands/provider/keys.js +135 -0
  12. package/src/cli/commands/provider/models.js +99 -0
  13. package/src/cli/commands/provider.js +1 -0
  14. package/src/cli/commands/sync.js +143 -0
  15. package/src/cli/commands/system.js +236 -0
  16. package/src/cli/commands/workbuddy.js +91 -0
  17. package/src/cli/format.js +119 -0
  18. package/src/cli/group-helpers.js +69 -0
  19. package/src/cli/help.js +66 -0
  20. package/src/cli/index.js +59 -0
  21. package/src/cli/interactive.js +84 -0
  22. package/src/cli/policy.js +119 -0
  23. package/src/cli/status.js +332 -0
  24. package/src/cli/util.js +24 -0
  25. package/src/providers/base.js +159 -0
  26. package/src/providers/generic.js +26 -166
  27. package/src/providers/openrouter.js +32 -162
  28. package/src/providers/workbuddy.js +7 -32
  29. package/src/routes/chat/gateway.js +301 -0
  30. package/src/routes/chat/index.js +1 -384
  31. package/src/state/facade.js +57 -0
  32. package/src/state/memory.js +180 -0
  33. package/src/state/persist.js +42 -0
  34. package/src/state/schemas/allowlist.js +87 -0
  35. package/src/state/schemas/group.js +31 -0
  36. package/src/state/schemas/model.js +53 -0
  37. package/src/state/schemas/peer.js +31 -0
  38. package/src/state/schemas/port.js +10 -0
  39. package/src/state/schemas/provider.js +254 -0
  40. package/src/state/schemas/token.js +43 -0
  41. package/src/state/store.js +202 -0
  42. package/src/state.js +1 -711
@@ -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
- function joinUrl(base, path) {
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 (UndiciAgent && !noAgent) {
75
- try {
76
- agent = new UndiciAgent({
77
- keepAliveTimeout: envInt("MSLXDFF_GENERIC_KEEPALIVE_TIMEOUT", 30_000),
78
- keepAliveMaxTimeout: envInt("MSLXDFF_GENERIC_KEEPALIVE_MAX_TIMEOUT", 60_000),
79
- connections: envInt("MSLXDFF_GENERIC_KEEPALIVE_CONNECTIONS", 20),
80
- pipelining: 1,
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
- "Accept": isStream ? "text/event-stream" : "*/*",
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
- async function attemptOnce(url, body, key, activeRing) {
98
- const controller = new AbortController();
99
- const timer = setTimeout(() => controller.abort(new Error(`${id} timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
100
- try {
101
- const opts = { method: "POST", headers: buildHeaders(body, key), body: JSON.stringify(body), signal: controller.signal };
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 CACHE_TTL_MS = 10 * 60 * 1000;
167
- let cache = null;
168
- let fetchedAt = 0;
169
- async function listModels() {
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
- async function preheat() {
197
- const url = joinUrl(resolvedBase, resolvedModelsPath);
198
- const t0 = performance.now();
199
- try {
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
- let UndiciAgent = null;
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
- let dispatcher = null;
53
- let agent = null;
54
- if (UndiciAgent && !noAgent) {
55
- try {
56
- agent = new UndiciAgent({
57
- keepAliveTimeout: envInt("MSLXDFF_OA_KEEPALIVE_TIMEOUT", 30_000),
58
- keepAliveMaxTimeout: envInt("MSLXDFF_OA_KEEPALIVE_MAX_TIMEOUT", 60_000),
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
- "Accept": isStream ? "text/event-stream" : "*/*",
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
- async function attemptOnce(url, body, key, activeRing) {
80
- const controller = new AbortController();
81
- const timer = setTimeout(() => controller.abort(new Error(`openrouter timed out after ${connectTimeoutMs}ms`)), connectTimeoutMs);
82
- try {
83
- const opts = { method: "POST", headers: buildHeaders(body, key), body: JSON.stringify(body), signal: controller.signal };
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
- // 轮转取一个可用 key;仅当"有 key 但全部在冷却"才算供应商暂时失效。
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
- // 拉免费模型:pricing 全 0 + 前缀化;可匿名
154
- const CACHE_TTL_MS = 10 * 60 * 1000;
155
- let cache = null;
156
- let fetchedAt = 0;
157
- async function listModels() {
158
- const now = Date.now();
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
- async function preheat() {
184
- const url = `${baseUrl}/models`;
185
- const t0 = performance.now();
186
- try {
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
- let UndiciAgent = null;
10
- let UndiciFetch = null;
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
- let dispatcher = null;
214
- let agent = null;
215
- if (UndiciAgent) {
216
- try {
217
- agent = new UndiciAgent({
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);