mslxdff 0.1.39 → 0.1.42

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/upstream.js CHANGED
@@ -1,9 +1,31 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
+ let UndiciAgent = null;
4
+ let UndiciFetch = null;
5
+ try {
6
+ const mod = await import("undici");
7
+ UndiciAgent = mod.Agent;
8
+ UndiciFetch = mod.fetch;
9
+ } catch {
10
+ // undici not installed — fallback to no dispatcher (still works, just no keep-alive tuning)
11
+ }
12
+
3
13
  function genId(prefix) {
4
14
  return `${prefix}${crypto.randomUUID().replace(/-/g, "")}`;
5
15
  }
6
16
 
17
+ function envInt(name, fallback) {
18
+ const v = Number(process.env[name]);
19
+ return Number.isInteger(v) && v > 0 ? v : fallback;
20
+ }
21
+
22
+ function isPreheatDisabled() {
23
+ const raw = process.env.MSLXDFF_PREHEAT;
24
+ if (raw === undefined || raw === null || raw === "") return false;
25
+ const s = String(raw).trim().toLowerCase();
26
+ return s === "0" || s === "off" || s === "false" || s === "no" || s === "disable" || s === "disabled";
27
+ }
28
+
7
29
  export function createUpstreamClient({
8
30
  baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
9
31
  authToken = process.env.UPSTREAM_AUTH_TOKEN || "public",
@@ -15,8 +37,10 @@ export function createUpstreamClient({
15
37
  503: { attempts: 1, delayMs: 500 },
16
38
  504: { attempts: 1, delayMs: 500 },
17
39
  },
18
- fetchImpl = fetch,
40
+ fetchImpl,
19
41
  } = {}) {
42
+ // 使用 undici 的 fetch 与 Agent 配对,避免 global fetch 与 npm undici Agent 不兼容
43
+ if (!fetchImpl) fetchImpl = UndiciFetch || fetch;
20
44
  const baseHeaders = {
21
45
  "Content-Type": "application/json",
22
46
  "Authorization": `Bearer ${authToken}`,
@@ -35,6 +59,27 @@ export function createUpstreamClient({
35
59
  };
36
60
  }
37
61
 
62
+ // Keep-Alive Agent(显式复用 TCP+TLS)
63
+ const keepAliveTimeout = envInt("MSLXDFF_UPSTREAM_KEEPALIVE_TIMEOUT", 30_000);
64
+ const keepAliveMaxTimeout = envInt("MSLXDFF_UPSTREAM_KEEPALIVE_MAX_TIMEOUT", 60_000);
65
+ const keepAliveConnections = envInt("MSLXDFF_UPSTREAM_KEEPALIVE_CONNECTIONS", 20);
66
+ let dispatcher = null;
67
+ let agent = null;
68
+ if (UndiciAgent) {
69
+ try {
70
+ agent = new UndiciAgent({
71
+ keepAliveTimeout,
72
+ keepAliveMaxTimeout,
73
+ connections: keepAliveConnections,
74
+ pipelining: 1,
75
+ });
76
+ dispatcher = agent;
77
+ } catch {
78
+ agent = null;
79
+ dispatcher = null;
80
+ }
81
+ }
82
+
38
83
  async function chat(body) {
39
84
  const url = `${baseUrl}/zen/v1/chat/completions`;
40
85
  const t0 = performance.now();
@@ -85,12 +130,14 @@ export function createUpstreamClient({
85
130
  );
86
131
  try {
87
132
  const headers = buildHeaders(body);
88
- const res = await fetchImpl(url, {
133
+ const opts = {
89
134
  method: "POST",
90
135
  headers,
91
136
  body: JSON.stringify(body),
92
137
  signal: controller.signal,
93
- });
138
+ };
139
+ if (dispatcher) opts.dispatcher = dispatcher;
140
+ const res = await fetchImpl(url, opts);
94
141
  return res;
95
142
  } catch (err) {
96
143
  return err;
@@ -99,9 +146,46 @@ export function createUpstreamClient({
99
146
  }
100
147
  }
101
148
 
149
+ async function preheat() {
150
+ if (isPreheatDisabled()) return { ok: false, skipped: true, reason: "disabled" };
151
+ const url = `${baseUrl}/zen/v1/models`;
152
+ const controller = new AbortController();
153
+ const timer = setTimeout(() => controller.abort(new Error("preheat timed out after 3000ms")), 3000);
154
+ const t0 = performance.now();
155
+ try {
156
+ const headers = buildHeaders({ stream: false });
157
+ const opts = {
158
+ method: "GET",
159
+ headers,
160
+ signal: controller.signal,
161
+ };
162
+ if (dispatcher) opts.dispatcher = dispatcher;
163
+ const res = await fetchImpl(url, opts);
164
+ const ms = Math.round(performance.now() - t0);
165
+ // 消耗 body 以释放连接回池(即使不需要内容)
166
+ try { if (res.body) await res.text().catch(() => {}); } catch {}
167
+ return { ok: res.ok, status: res.status, ms };
168
+ } catch (err) {
169
+ return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
170
+ } finally {
171
+ clearTimeout(timer);
172
+ }
173
+ }
174
+
175
+ let closed = false;
176
+ async function close() {
177
+ if (closed) return;
178
+ closed = true;
179
+ if (agent && typeof agent.close === "function") {
180
+ try { await agent.close(); } catch {}
181
+ } else if (dispatcher && typeof dispatcher.close === "function" && dispatcher !== agent) {
182
+ try { await dispatcher.close(); } catch {}
183
+ }
184
+ }
185
+
102
186
  // 兼容旧调用:headers 为动态生成,暴露 getter 快照(用于测试/展示)
103
187
  const headers = buildHeaders({});
104
- return { chat, headers, buildHeaders };
188
+ return { chat, preheat, close, headers, buildHeaders, dispatcher, agent, [Symbol.asyncDispose]: close };
105
189
  }
106
190
 
107
191
  function sleep(ms) {