mslxdff 0.1.87 → 0.1.89

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
@@ -2,37 +2,24 @@ import crypto from "node:crypto";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import { appendFile } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
- import os from "node:os";
5
+ import { performance } from "node:perf_hooks";
6
6
  import { isFreeModel } from "./models.js";
7
- import { defaultStateFile } from "./state.js";
8
7
  import { fmtShanghaiYMDHMS } from "./time.js";
9
-
10
- let UndiciAgent = null;
11
- let UndiciFetch = null;
12
- try {
13
- const mod = await import("undici");
14
- UndiciAgent = mod.Agent;
15
- UndiciFetch = mod.fetch;
16
- } catch {
17
- // undici not installed — fallback to no dispatcher (still works, just no keep-alive tuning)
18
- }
8
+ import { createTransport } from "./transport/index.js";
19
9
 
20
10
  function genId(prefix) {
21
11
  return `${prefix}${crypto.randomUUID().replace(/-/g, "")}`;
22
12
  }
23
-
24
13
  function envInt(name, fallback) {
25
14
  const v = Number(process.env[name]);
26
15
  return Number.isInteger(v) && v > 0 ? v : fallback;
27
16
  }
28
-
29
17
  function isPreheatDisabled() {
30
18
  const raw = process.env.MSLXDFF_PREHEAT;
31
19
  if (raw === undefined || raw === null || raw === "") return false;
32
20
  const s = String(raw).trim().toLowerCase();
33
21
  return s === "0" || s === "off" || s === "false" || s === "no" || s === "disable" || s === "disabled";
34
22
  }
35
-
36
23
  export function createUpstreamClient({
37
24
  baseUrl = process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
38
25
  authToken = process.env.UPSTREAM_AUTH_TOKEN || "public",
@@ -48,17 +35,6 @@ export function createUpstreamClient({
48
35
  hooks,
49
36
  keepAlive = true,
50
37
  } = {}) {
51
- // 使用 undici 的 fetch 与 Agent 配对,避免 global fetch 与 npm undici Agent 不兼容
52
- if (!fetchImpl) fetchImpl = UndiciFetch || fetch;
53
- // hooks: async (name, ctx) => ({ value, changed }) | null — 由 bin 用 runHook 绑定;错误已在上层隔离
54
- const applyHook = async (name, ctx) => {
55
- if (!hooks) return null;
56
- try {
57
- return await hooks(name, ctx);
58
- } catch {
59
- return null;
60
- }
61
- };
62
38
  function isAnonFirst() {
63
39
  const raw = process.env.MSLXDFF_OPENCOD_ANON_FIRST ?? process.env.MSLXDFF_ANON_FIRST ?? process.env.MSLXDFF_OPENCOD_ANON;
64
40
  if (raw === undefined || raw === null || raw === "") return authToken === "public";
@@ -70,7 +46,7 @@ export function createUpstreamClient({
70
46
  const baseHeaders = anonFirst
71
47
  ? {
72
48
  "Content-Type": "application/json",
73
- "Authorization": "",
49
+ Authorization: "",
74
50
  "x-opencode-client": "desktop",
75
51
  "User-Agent": "opencode",
76
52
  "HTTP-Referer": "https://hermes-agent.nousresearch.com",
@@ -78,53 +54,40 @@ export function createUpstreamClient({
78
54
  }
79
55
  : {
80
56
  "Content-Type": "application/json",
81
- "Authorization": `Bearer ${authToken}`,
57
+ Authorization: `Bearer ${authToken}`,
82
58
  "x-opencode-client": "desktop",
83
59
  };
84
-
85
60
  function buildHeaders(body, { anonymous = anonFirst } = {}) {
86
61
  const isStream = body?.stream !== false;
87
62
  const base = {
88
63
  ...baseHeaders,
89
- "Accept": isStream ? "text/event-stream" : "*/*",
64
+ Accept: isStream ? "text/event-stream" : "*/*",
90
65
  "User-Agent": "opencode",
91
66
  "x-opencode-session": genId("ses_"),
92
67
  "x-opencode-request": genId("msg_"),
93
68
  "x-opencode-project": "global",
94
69
  };
95
70
  if (anonymous) {
96
- // hermes 的 free 匿名通道:空 Authorization,保留 opencode UA 以兼容 big-pickle/mimo 的 UA 门禁
97
- return {
98
- ...base,
99
- "Authorization": "",
100
- "HTTP-Referer": "https://hermes-agent.nousresearch.com",
101
- "X-Title": "Hermes Agent",
102
- };
71
+ return { ...base, Authorization: "", "HTTP-Referer": "https://hermes-agent.nousresearch.com", "X-Title": "Hermes Agent" };
103
72
  }
104
- // 显式 public 回退(anonFirst 时去掉 hermes 头)
105
73
  if (anonFirst) {
106
74
  const { "HTTP-Referer": _a, "X-Title": _b, ...rest } = base;
107
- return { ...rest, "Authorization": `Bearer ${authToken}` };
75
+ return { ...rest, Authorization: `Bearer ${authToken}` };
108
76
  }
109
77
  return base;
110
78
  }
111
-
112
79
  function shouldTryAnonFree() {
113
80
  const raw = process.env.MSLXDFF_FREE_ANON;
114
81
  if (raw === "0" || raw === "off" || raw === "false" || raw === "no") return false;
115
82
  return true;
116
83
  }
117
-
118
84
  function isResponsesModel(model) {
119
- const m = String(model || "").toLowerCase();
120
- return m.startsWith("muse-spark");
85
+ return String(model || "").toLowerCase().startsWith("muse-spark");
121
86
  }
122
-
123
87
  function chatToResponsesBody(chatBody) {
124
88
  const msgs = Array.isArray(chatBody?.messages) ? chatBody.messages : [];
125
89
  const system = msgs.filter((m) => m.role === "system").map((m) => String(m.content || "")).join("\n");
126
90
  const nonSystem = msgs.filter((m) => m.role !== "system");
127
- // 取最后一条 user 内容作为 input,其余拼到 input 前(保留上下文)
128
91
  const inputParts = nonSystem.map((m) => {
129
92
  const c = m.content;
130
93
  if (typeof c === "string") return `${m.role}: ${c}`;
@@ -132,11 +95,7 @@ export function createUpstreamClient({
132
95
  return `${m.role}: ${String(c || "")}`;
133
96
  });
134
97
  const input = inputParts.join("\n\n") || "hi";
135
- const out = {
136
- model: chatBody.model,
137
- input,
138
- stream: false, // muse-spark 的 chat 流式经 responses 的 SSE 格式不同,先以非流式 200 保证可用
139
- };
98
+ const out = { model: chatBody.model, input, stream: false };
140
99
  if (system) out.instructions = system;
141
100
  if (chatBody.tools) out.tools = chatBody.tools;
142
101
  if (chatBody.tool_choice) out.tool_choice = chatBody.tool_choice;
@@ -144,9 +103,7 @@ export function createUpstreamClient({
144
103
  if (chatBody.max_tokens != null) out.max_output_tokens = chatBody.max_tokens;
145
104
  return out;
146
105
  }
147
-
148
106
  function responsesToChatJson(respJson) {
149
- // responses: {id, model, output:[{type:reasoning},{type:message, content:[{type:output_text,text}]}]}
150
107
  let text = "";
151
108
  for (const item of respJson.output || []) {
152
109
  if (item.type === "message" && item.role === "assistant") {
@@ -157,7 +114,6 @@ export function createUpstreamClient({
157
114
  }
158
115
  }
159
116
  if (!text) {
160
- // 兜底:取 output_text 的第一个
161
117
  for (const item of respJson.output || []) {
162
118
  if (item.type === "message") {
163
119
  const t = item.content?.[0]?.text;
@@ -165,288 +121,158 @@ export function createUpstreamClient({
165
121
  }
166
122
  }
167
123
  }
168
- if (!text) text = "";
169
124
  const chatJson = {
170
125
  id: respJson.id || `resp_${Date.now()}`,
171
126
  object: "chat.completion",
172
127
  created: Math.floor((respJson.created_at || Date.now() / 1000)),
173
128
  model: respJson.model,
174
- choices: [
175
- {
176
- index: 0,
177
- finish_reason: respJson.status === "completed" ? "stop" : "length",
178
- message: { role: "assistant", content: text },
179
- },
180
- ],
129
+ choices: [{ index: 0, finish_reason: respJson.status === "completed" ? "stop" : "length", message: { role: "assistant", content: text } }],
181
130
  usage: respJson.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
182
131
  };
183
132
  return chatJson;
184
133
  }
185
-
186
134
  function freeAnonLogFile() {
187
- // 按用户要求:放在当前项目目录,方便一眼看到;可用 env 覆盖
188
135
  return process.env.MSLXDFF_FREE_ANON_LOG || join(process.cwd(), "free-anon-extra.txt");
189
136
  }
190
-
191
137
  function logFreeAnon({ model, publicStatus, anonStatus, anonAttempts, hit, totalMs }) {
192
138
  const file = freeAnonLogFile();
193
139
  const line = `${fmtShanghaiYMDHMS(new Date())} model=${model} public=429 anonTries=${anonAttempts} anonStatus=${anonStatus ?? "none"} hit=${hit ? "YES额外额度" : "NO无额外"} totalMs=${totalMs} count=${hit ? "1" : "0"}\n`;
194
- try {
195
- mkdirSync(dirname(file), { recursive: true });
196
- } catch {}
140
+ try { mkdirSync(dirname(file), { recursive: true }); } catch {}
197
141
  appendFile(file, line).catch(() => {});
198
- // 同时在控制台留痕(daemon 日志也能看到)
199
- try {
200
- if (process.env.MSLXDFF_DEBUG === "1") console.log(`[free-anon] ${line.trim()}`);
201
- } catch {}
142
+ try { if (process.env.MSLXDFF_DEBUG === "1") console.log(`[free-anon] ${line.trim()}`); } catch {}
202
143
  }
203
-
204
- // 连续命中计数(内存,方便 txt 里看出“连续几次都有”)
205
144
  let consecutiveHits = 0;
206
145
 
207
- // Keep-Alive Agent(显式复用 TCP+TLS)
208
- const keepAliveTimeout = envInt("MSLXDFF_UPSTREAM_KEEPALIVE_TIMEOUT", 30_000);
209
- const keepAliveMaxTimeout = envInt("MSLXDFF_UPSTREAM_KEEPALIVE_MAX_TIMEOUT", 60_000);
210
- const keepAliveConnections = envInt("MSLXDFF_UPSTREAM_KEEPALIVE_CONNECTIONS", 20);
211
- let dispatcher = null;
212
- let agent = null;
213
- if (keepAlive && UndiciAgent) {
214
- try {
215
- agent = new UndiciAgent({
216
- keepAliveTimeout,
217
- keepAliveMaxTimeout,
218
- connections: keepAliveConnections,
219
- pipelining: 1,
220
- });
221
- dispatcher = agent;
222
- } catch {
223
- agent = null;
224
- dispatcher = null;
225
- }
226
- }
146
+ // 深模块:网络层委托 StreamingTransport
147
+ const transport = createTransport({
148
+ keepAlive,
149
+ fetchImpl,
150
+ timeoutMs: connectTimeoutMs,
151
+ retry,
152
+ hooks,
153
+ });
227
154
 
228
155
  async function chat(body) {
229
156
  const isResp = isResponsesModel(body?.model);
230
157
  const url = isResp ? `${baseUrl}/zen/v1/responses` : `${baseUrl}/zen/v1/chat/completions`;
231
158
  const reqBody = isResp ? chatToResponsesBody(body) : body;
232
159
  const t0 = performance.now();
233
- const attempts = [];
234
- let waitMs = 0;
235
- for (let attempt = 0; ; attempt++) {
236
- const t = performance.now();
237
- const result = await attemptOnce(url, reqBody);
238
- attempts.push({
239
- attempt,
240
- type: result instanceof Error ? "network" : `http${result.status}`,
241
- ms: Math.round(performance.now() - t),
160
+
161
+ // 首发请求(transport 已处理 network/429 等重试)
162
+ let res;
163
+ try {
164
+ res = await transport.request({
165
+ url,
166
+ method: "POST",
167
+ headers: buildHeaders(reqBody),
168
+ body: reqBody,
169
+ stream: body?.stream !== false,
170
+ timeoutMs: connectTimeoutMs,
171
+ retry,
242
172
  });
243
- if (result instanceof Error) {
244
- const entry = retry?.network;
245
- if (entry && attempt < entry.attempts) {
246
- await sleep(entry.delayMs);
247
- waitMs += entry.delayMs;
173
+ } catch (e) {
174
+ e._t = e._t || { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - t0) };
175
+ throw e;
176
+ }
177
+
178
+ // 匿名兜底:仅非 anonFirst 时,public 429 + free 模型才走 hermes 空头重试
179
+ if (!anonFirst && res.status === 429 && isFreeModel(body?.model) && shouldTryAnonFree()) {
180
+ const anonRetries = envInt("MSLXDFF_FREE_ANON_RETRIES", 3);
181
+ const anonDelay = envInt("MSLXDFF_FREE_ANON_DELAY_MS", 1000);
182
+ let anonRes = null;
183
+ for (let i = 0; i < anonRetries; i++) {
184
+ await new Promise((r) => setTimeout(r, anonDelay));
185
+ try {
186
+ anonRes = await transport.request({
187
+ url,
188
+ method: "POST",
189
+ headers: buildHeaders(reqBody, { anonymous: true }),
190
+ body: reqBody,
191
+ stream: body?.stream !== false,
192
+ timeoutMs: connectTimeoutMs,
193
+ retry,
194
+ });
195
+ } catch (e) {
248
196
  continue;
249
197
  }
250
- result._t = {
251
- attempts,
252
- waitMs,
253
- totalMs: Math.round(performance.now() - t0),
254
- };
255
- throw result;
256
- }
257
- const entry = retry?.[result.status];
258
- if (entry && attempt < entry.attempts) {
259
- await sleep(entry.delayMs);
260
- waitMs += entry.delayMs;
261
- continue;
262
- }
263
- // 额外额度探测:仅当非 anon 优先时,public 429 且为 free 模型时,用空头(hermes 方式)每秒重试 3 次;anon 优先时首发即高额度,无需再 public 撞墙
264
- if (!anonFirst && result.status === 429 && isFreeModel(body?.model) && shouldTryAnonFree()) {
265
- const anonRetries = envInt("MSLXDFF_FREE_ANON_RETRIES", 3);
266
- const anonDelay = envInt("MSLXDFF_FREE_ANON_DELAY_MS", 1000);
267
- let anonResult = null;
268
- let hit = false;
269
- let hitAt = -1;
270
- for (let i = 0; i < anonRetries; i++) {
271
- await sleep(anonDelay);
272
- waitMs += anonDelay;
273
- const t2 = performance.now();
274
- anonResult = await attemptOnce(url, reqBody, { anonymous: true });
275
- attempts.push({
276
- attempt: `anon-${i}`,
277
- type: anonResult instanceof Error ? "network" : `http${anonResult.status}`,
278
- ms: Math.round(performance.now() - t2),
279
- });
280
- if (anonResult instanceof Error) continue;
281
- if (anonResult.status !== 429) {
282
- // responses 模型的 anon 命中也需转回 chat 形状
283
- let outAnon = anonResult;
284
- if (isResp && anonResult.ok) {
285
- try {
286
- const txt = await anonResult.text();
287
- const j = JSON.parse(txt);
288
- if (j && Array.isArray(j.output)) {
289
- const chatJson = responsesToChatJson(j);
290
- const newHeaders = new Headers(anonResult.headers);
291
- newHeaders.set("content-type", "application/json");
292
- outAnon = new Response(JSON.stringify(chatJson), { status: anonResult.status, headers: newHeaders });
293
- } else {
294
- outAnon = new Response(txt, { status: anonResult.status, headers: anonResult.headers });
295
- }
296
- } catch { outAnon = anonResult; }
297
- }
298
- outAnon._t = {
299
- attempts,
300
- waitMs,
301
- totalMs: Math.round(performance.now() - t0),
302
- anonTried: true,
303
- anonAttempts: i + 1,
304
- };
305
- hit = true;
306
- hitAt = i + 1;
307
- consecutiveHits += 1;
308
- logFreeAnon({ model: body?.model, publicStatus: 429, anonStatus: anonResult.status, anonAttempts: i + 1, hit: true, totalMs: outAnon._t.totalMs });
309
- // 额外在 txt 里强调连续命中
198
+ if (anonRes.status !== 429) {
199
+ // responses 模型需转回 chat 形状
200
+ let outAnon = anonRes;
201
+ if (isResp && anonRes.ok) {
310
202
  try {
311
- if (consecutiveHits >= 2) {
312
- const file = freeAnonLogFile();
313
- appendFile(file, ` -> 连续额外额度 ${consecutiveHits} 次\n`).catch(() => {});
203
+ const txt = await anonRes.text();
204
+ const j = JSON.parse(txt);
205
+ if (j && Array.isArray(j.output)) {
206
+ const chatJson = responsesToChatJson(j);
207
+ outAnon = new Response(JSON.stringify(chatJson), { status: anonRes.status, headers: new Headers(anonRes.headers) });
208
+ outAnon.headers.set("content-type", "application/json");
209
+ } else {
210
+ outAnon = new Response(txt, { status: anonRes.status, headers: anonRes.headers });
314
211
  }
315
- } catch {}
316
- if (process.env.MSLXDFF_DEBUG === "1") try { console.log(`[free-anon] ${body?.model} public 429 -> anon ${anonResult.status} after ${i + 1} try HIT`); } catch {}
317
- return outAnon;
212
+ } catch { outAnon = anonRes; }
318
213
  }
319
- }
320
- // 3 次 anon 仍 429
321
- if (anonResult) {
322
- anonResult._t = {
323
- attempts,
324
- waitMs,
325
- totalMs: Math.round(performance.now() - t0),
326
- anonTried: true,
327
- anonAttempts: anonRetries,
328
- };
329
- consecutiveHits = 0;
330
- logFreeAnon({ model: body?.model, publicStatus: 429, anonStatus: anonResult.status, anonAttempts: anonRetries, hit: false, totalMs: anonResult._t.totalMs });
331
- if (process.env.MSLXDFF_DEBUG === "1") try { console.log(`[free-anon] ${body?.model} public 429 -> anon 429 x${anonRetries} MISS`); } catch {}
332
- return anonResult;
333
- }
334
- }
335
- // Muse Spark 走 /responses,需把 responses 的 JSON 转回 chat.completions 形状,保持上层无感
336
- if (isResp && result.ok) {
337
- try {
338
- const txt = await result.text();
339
- const j = JSON.parse(txt);
340
- // responses 成功态:{output:[...]};chat 态:{choices:[...]}。非 responses 形状则原样回
341
- if (j && Array.isArray(j.output)) {
342
- const chatJson = responsesToChatJson(j);
343
- const newBody = JSON.stringify(chatJson);
344
- const newHeaders = new Headers(result.headers);
345
- newHeaders.set("content-type", "application/json");
346
- const transformed = new Response(newBody, { status: result.status, headers: newHeaders });
347
- transformed._t = {
348
- attempts,
349
- waitMs,
350
- totalMs: Math.round(performance.now() - t0),
351
- };
352
- // 保留原始 timing 供日志
353
- if (result._t) transformed._t = { ...transformed._t, ...result._t };
354
- return transformed;
214
+ outAnon._t = { ...(outAnon._t || {}), anonTried: true, anonAttempts: i + 1, totalMs: Math.round(performance.now() - t0) };
215
+ consecutiveHits += 1;
216
+ logFreeAnon({ model: body?.model, publicStatus: 429, anonStatus: anonRes.status, anonAttempts: i + 1, hit: true, totalMs: outAnon._t.totalMs });
217
+ if (consecutiveHits >= 2) {
218
+ try { appendFile(freeAnonLogFile(), ` -> 连续额外额度 ${consecutiveHits} 次\n`).catch(() => {}); } catch {}
355
219
  }
356
- // 不是 responses 形状,回退:用原文本重建可读 Response
357
- const fallback = new Response(txt, { status: result.status, headers: result.headers });
358
- fallback._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
359
- return fallback;
360
- } catch {
361
- result._t = { attempts, waitMs, totalMs: Math.round(performance.now() - t0) };
362
- return result;
220
+ return outAnon;
363
221
  }
364
222
  }
365
- result._t = {
366
- attempts,
367
- waitMs,
368
- totalMs: Math.round(performance.now() - t0),
369
- };
370
- return result;
223
+ if (anonRes) {
224
+ anonRes._t = { ...(anonRes._t || {}), anonTried: true, anonAttempts: anonRetries, totalMs: Math.round(performance.now() - t0) };
225
+ consecutiveHits = 0;
226
+ logFreeAnon({ model: body?.model, publicStatus: 429, anonStatus: anonRes.status, anonAttempts: anonRetries, hit: false, totalMs: anonRes._t.totalMs });
227
+ return anonRes;
228
+ }
371
229
  }
372
- }
373
230
 
374
- async function attemptOnce(url, body, { anonymous = anonFirst } = {}) {
375
- const controller = new AbortController();
376
- const timer = setTimeout(() =>
377
- controller.abort(new Error(`upstream timed out after ${connectTimeoutMs}ms`)),
378
- connectTimeoutMs
379
- );
380
- try {
381
- let reqUrl = url;
382
- let headers = buildHeaders(body, { anonymous });
383
- // 插件 hook:upstream:headers — 返回 { headers } 可替换请求头
384
- const hh = await applyHook("upstream:headers", { url, body, headers });
385
- if (hh?.changed && hh.value?.headers) headers = hh.value.headers;
386
- // 插件 hook:upstream:before-request — 返回 { url, headers } 可改目标地址/头(上游不限于 opencode)
387
- const br = await applyHook("upstream:before-request", { url, method: "POST", body, headers });
388
- if (br?.changed && br.value) {
389
- if (typeof br.value.url === "string" && br.value.url) reqUrl = br.value.url;
390
- if (br.value.headers && typeof br.value.headers === "object") headers = br.value.headers;
231
+ // responses 模型成功态转 chat
232
+ if (isResp && res.ok) {
233
+ try {
234
+ const txt = await res.text();
235
+ const j = JSON.parse(txt);
236
+ if (j && Array.isArray(j.output)) {
237
+ const chatJson = responsesToChatJson(j);
238
+ const headers = new Headers(res.headers);
239
+ headers.set("content-type", "application/json");
240
+ const transformed = new Response(JSON.stringify(chatJson), { status: res.status, headers });
241
+ transformed._t = { ...(res._t || {}), totalMs: Math.round(performance.now() - t0) };
242
+ return transformed;
243
+ }
244
+ const fallback = new Response(txt, { status: res.status, headers: res.headers });
245
+ fallback._t = { ...(res._t || {}), totalMs: Math.round(performance.now() - t0) };
246
+ return fallback;
247
+ } catch {
248
+ res._t = { ...(res._t || {}), totalMs: Math.round(performance.now() - t0) };
249
+ return res;
391
250
  }
392
- const opts = {
393
- method: "POST",
394
- headers,
395
- body: JSON.stringify(body),
396
- signal: controller.signal,
397
- };
398
- if (dispatcher) opts.dispatcher = dispatcher;
399
- const res = await fetchImpl(reqUrl, opts);
400
- return res;
401
- } catch (err) {
402
- return err;
403
- } finally {
404
- clearTimeout(timer);
405
251
  }
252
+ res._t = { ...(res._t || {}), totalMs: res._t?.totalMs ?? Math.round(performance.now() - t0) };
253
+ return res;
406
254
  }
407
255
 
408
256
  async function preheat() {
409
257
  if (isPreheatDisabled()) return { ok: false, skipped: true, reason: "disabled" };
410
- const url = `${baseUrl}/zen/v1/models`;
411
- const controller = new AbortController();
412
- const timer = setTimeout(() => controller.abort(new Error("preheat timed out after 3000ms")), 3000);
413
258
  const t0 = performance.now();
414
259
  try {
415
- const headers = buildHeaders({ stream: false });
416
- const opts = {
260
+ const res = await transport.request({
261
+ url: `${baseUrl}/zen/v1/models`,
417
262
  method: "GET",
418
- headers,
419
- signal: controller.signal,
420
- };
421
- if (dispatcher) opts.dispatcher = dispatcher;
422
- const res = await fetchImpl(url, opts);
423
- const ms = Math.round(performance.now() - t0);
424
- // 消耗 body 以释放连接回池(即使不需要内容)
425
- try { if (res.body) await res.text().catch(() => {}); } catch {}
426
- return { ok: res.ok, status: res.status, ms };
427
- } catch (err) {
428
- return { ok: false, error: String(err?.message || err), ms: Math.round(performance.now() - t0) };
429
- } finally {
430
- clearTimeout(timer);
263
+ headers: buildHeaders({ stream: false }),
264
+ stream: false,
265
+ timeoutMs: 3000,
266
+ });
267
+ try { await res.text().catch(() => {}); } catch {}
268
+ return { ok: res.ok, status: res.status, ms: Math.round(performance.now() - t0) };
269
+ } catch (e) {
270
+ return { ok: false, error: String(e?.message || e), ms: Math.round(performance.now() - t0) };
431
271
  }
432
272
  }
433
-
434
- let closed = false;
435
273
  async function close() {
436
- if (closed) return;
437
- closed = true;
438
- if (agent && typeof agent.close === "function") {
439
- try { await agent.close(); } catch {}
440
- } else if (dispatcher && typeof dispatcher.close === "function" && dispatcher !== agent) {
441
- try { await dispatcher.close(); } catch {}
442
- }
274
+ await transport.close();
443
275
  }
444
-
445
- // 兼容旧调用:headers 为动态生成,暴露 getter 快照(用于测试/展示)
446
276
  const headers = buildHeaders({});
447
- return { chat, preheat, close, headers, buildHeaders, dispatcher, agent, [Symbol.asyncDispose]: close };
277
+ return { chat, preheat, close, headers, buildHeaders, get dispatcher() { return transport.dispatcher; }, get agent() { return transport.agent; }, [Symbol.asyncDispose]: close };
448
278
  }
449
-
450
- function sleep(ms) {
451
- return new Promise((r) => setTimeout(r, ms));
452
- }