mslxdff 0.1.122 → 0.1.123

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/routes/peers.js +77 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.122",
3
+ "version": "0.1.123",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,10 +3,53 @@ import { isAutoModel } from "../auto.js";
3
3
  import { errMsg } from "./helpers.js";
4
4
  import { runHook } from "../plugins.js";
5
5
  import { buildShareKeysHeader, SHARE_KEYS_HEADER } from "../providers/share-keys.js";
6
- import { compatFetch, timeoutSignal } from "../compat.js";
6
+ import { compatFetch, timeoutSignal, getUndici } from "../compat.js";
7
7
 
8
8
  const PEER_TIMEOUT_MS = 30_000;
9
9
  const PEER_STATUS_TIMEOUT_MS = 2_000;
10
+ const DEFAULT_PEER_CONNECT_TIMEOUT_MS = 3_000;
11
+
12
+ function peerConnectTimeoutMs() {
13
+ const n = Number(process.env.MSLXDFF_PEER_CONNECT_TIMEOUT_MS);
14
+ return Number.isInteger(n) && n > 0 ? n : DEFAULT_PEER_CONNECT_TIMEOUT_MS;
15
+ }
16
+
17
+ // 连接级超时(DNS + TCP/TLS 握手):黑洞节点(端口挂起)必须快速失败,
18
+ // 不能占用 30s 的响应超时(曾把一次请求拖到 92s)。仅 undici 可用时生效。
19
+ let peerDispatcher = null;
20
+ function getPeerDispatcher() {
21
+ if (peerDispatcher) return peerDispatcher;
22
+ const { Agent } = getUndici();
23
+ if (!Agent) return null;
24
+ try {
25
+ peerDispatcher = new Agent({
26
+ connect: { timeout: peerConnectTimeoutMs() },
27
+ headersTimeout: PEER_TIMEOUT_MS,
28
+ bodyTimeout: PEER_TIMEOUT_MS,
29
+ keepAliveTimeout: 30_000,
30
+ keepAliveMaxTimeout: 60_000,
31
+ });
32
+ } catch { peerDispatcher = null; }
33
+ return peerDispatcher;
34
+ }
35
+
36
+ // 跨 caller 中继必须剥离 reasoning 加密态:encrypted_content 由上游按 caller(出口)签发,
37
+ // 换个节点转发会被拒(400 "reasoning encrypted_content was not issued to this caller")。
38
+ // 明文 reasoning_content 保留(不绑定 caller,thinking 模式需要)。
39
+ export function stripCallerBoundReasoning(body) {
40
+ const messages = body?.messages;
41
+ if (!Array.isArray(messages)) return body;
42
+ let changed = false;
43
+ const out = messages.map((m) => {
44
+ if (!m || m.role !== "assistant") return m;
45
+ if (!Array.isArray(m.reasoning_items) || !m.reasoning_items.length) return m;
46
+ changed = true;
47
+ const copy = { ...m };
48
+ delete copy.reasoning_items;
49
+ return copy;
50
+ });
51
+ return changed ? { ...body, messages: out } : body;
52
+ }
10
53
 
11
54
  function peerHealthTtlMs() {
12
55
  const n = Number(process.env.MSLXDFF_PEER_HEALTH_TTL_MS);
@@ -75,11 +118,13 @@ async function forwardToPeer(peer, body, model, hops) {
75
118
  // ADR-0008:该模型命中的供应商若开启 share → 附带瞬时 key 给组员借用(opencode 恒排除)
76
119
  const shareHeader = buildShareKeysHeader(model);
77
120
  if (shareHeader) headers[SHARE_KEYS_HEADER] = shareHeader;
121
+ const dispatcher = getPeerDispatcher();
78
122
  return await compatFetch(`${peer.url}/v1/chat/completions`, {
79
123
  method: "POST",
80
124
  headers,
81
- body: JSON.stringify({ ...body, model }),
125
+ body: JSON.stringify({ ...stripCallerBoundReasoning(body), model }),
82
126
  signal: controller.signal,
127
+ ...(dispatcher ? { dispatcher } : {}),
83
128
  });
84
129
  } catch (err) {
85
130
  return err;
@@ -117,15 +162,30 @@ export const PEER_RACE_LIMIT = Number(process.env.MSLXDFF_PEER_RACE_LIMIT) > 0
117
162
  ? Number(process.env.MSLXDFF_PEER_RACE_LIMIT)
118
163
  : 3;
119
164
 
165
+ // 串行记账队列:失败/迟到成功的记录不阻塞赢家返回,同时避免并发写盘互相覆盖。
166
+ let peerRecordChain = Promise.resolve();
167
+ function recordLater(fn) {
168
+ peerRecordChain = peerRecordChain.then(fn).catch(() => {});
169
+ }
170
+
120
171
  export async function racePeerCandidates(candidates, ctx) {
121
- for (let i = 0; i < candidates.length; i += PEER_RACE_LIMIT) {
122
- const batch = candidates.slice(i, i + PEER_RACE_LIMIT);
172
+ const tried = (ctx.triedUrls ??= new Set());
173
+ const fresh = candidates.filter((p) => !tried.has(p.url));
174
+ for (let i = 0; i < fresh.length; i += PEER_RACE_LIMIT) {
175
+ const batch = fresh.slice(i, i + PEER_RACE_LIMIT);
123
176
  const prepared = (await Promise.all(batch.map((peer) => resolvePeerTarget(ctx, peer)))).filter(Boolean);
124
177
  if (!prepared.length) continue;
125
178
  const completed = await new Promise((resolve) => {
126
179
  const order = [];
127
180
  const total = prepared.length;
181
+ let settled = false;
182
+ const finish = (winner) => {
183
+ if (settled) return;
184
+ settled = true;
185
+ resolve({ list: order, winner });
186
+ };
128
187
  for (const { peer, target } of prepared) {
188
+ tried.add(peer.url);
129
189
  ctx.evt("peer-request", { peer: peer.url, model: target, hops: ctx.hops + 1 });
130
190
  // 插件 hook:peer:beforeForward — 转发给组员前观察
131
191
  if (ctx.plugins?.length) {
@@ -147,29 +207,29 @@ export async function racePeerCandidates(candidates, ctx) {
147
207
  ctx.logError(ctx.model, status, res instanceof Error ? `peer ${peer.url} ${errMsg(res)}` : `peer ${peer.url} ${status}`);
148
208
  ctx.evt("peer-error", { peer: peer.url, model: target, status, message: res instanceof Error ? errMsg(res) : null });
149
209
  order.push({ ok: false, peer, target, res, status });
210
+ recordLater(() => ctx.peers.recordError(peer.url, { status }));
211
+ recordLater(() => ctx.peers.recordResult(peer.url, { ok: false }));
212
+ if (order.length === total) finish(null);
150
213
  } else {
151
- order.push({ ok: true, peer, target, res, latencyMs });
214
+ const entry = { ok: true, peer, target, res, latencyMs };
215
+ order.push(entry);
216
+ if (!settled) {
217
+ // 第一个成功立即返回:不等慢/黑洞候选(迟到者的记账由各分支自理)
218
+ finish(entry);
219
+ } else {
220
+ recordLater(() => ctx.peers.recordResult(peer.url, { ok: true, latencyMs, model: target }));
221
+ }
152
222
  }
153
- if (order.length === total) resolve(order);
154
223
  });
155
224
  }
156
225
  });
157
- const winner = completed.find((o) => o.ok);
226
+ const winner = completed.winner;
158
227
  if (winner) {
159
- for (const o of completed) {
160
- if (o === winner) continue;
161
- if (!o.ok) {
162
- await ctx.peers.recordError(o.peer.url, { status: o.status });
163
- await ctx.peers.recordResult(o.peer.url, { ok: false });
164
- } else {
165
- await ctx.peers.recordResult(o.peer.url, { ok: true, latencyMs: o.latencyMs, model: o.target });
166
- }
167
- }
168
228
  return { peer: winner.peer, target: winner.target, res: winner.res, latencyMs: winner.latencyMs };
169
229
  }
170
230
  // 全失败:补读失败响应体(诊断 + 调用者详情)。仅在"本轮无 winner"时执行,
171
231
  // 不拖慢成功路径;并行读、每 peer 上限 600ms,body 里才有 400/429 的真实原因。
172
- await Promise.all(completed.map(async (o) => {
232
+ await Promise.all(completed.list.map(async (o) => {
173
233
  if (o.ok || !o.res || typeof o.res !== "object" || o.res instanceof Error) return;
174
234
  try {
175
235
  const snip = String(await Promise.race([
@@ -184,10 +244,6 @@ export async function racePeerCandidates(candidates, ctx) {
184
244
  ctx.logError(ctx.model, o.status, `peer ${o.peer.url} ${o.status} body=${snip}`);
185
245
  } catch {}
186
246
  }));
187
- for (const o of completed) {
188
- await ctx.peers.recordError(o.peer.url, { status: o.status });
189
- await ctx.peers.recordResult(o.peer.url, { ok: false });
190
- }
191
247
  }
192
248
  return null;
193
249
  }