mslxdff 0.1.13 → 0.1.15

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 (3) hide show
  1. package/package.json +1 -1
  2. package/src/peers.js +42 -5
  3. package/src/routes.js +113 -55
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
package/src/peers.js CHANGED
@@ -91,10 +91,36 @@ export function createPeersService({
91
91
  return hot * 1_000_000_000 + latency * 1_000 + fails;
92
92
  }
93
93
 
94
- // Available peers ordered for failover: hot (recent success, low latency,
95
- // few failures) first, cold/unused peers last.
94
+ // Available peers ordered for failover: hot peers (recent success) first
95
+ // the winner of the last race is reused next. Everyone else is ordered by
96
+ // when they last failed, earliest first: a peer that failed longer ago has
97
+ // had more time to recover, so it is tried before a more recent failure.
98
+ // Peers with no recorded error go after any that failed.
96
99
  function ordered(t = now()) {
97
- return available().sort((a, b) => rankScore(a, t) - rankScore(b, t));
100
+ return available().sort((a, b) => {
101
+ const hotA = stats[a.url]?.okAt && t - stats[a.url].okAt < heatMs ? 0 : 1;
102
+ const hotB = stats[b.url]?.okAt && t - stats[b.url].okAt < heatMs ? 0 : 1;
103
+ if (hotA !== hotB) return hotA - hotB;
104
+ const ea = lastErrorAt[a.url];
105
+ const eb = lastErrorAt[b.url];
106
+ if (ea == null && eb == null) return 0;
107
+ if (ea == null) return 1; // never-failed peers go after any that failed
108
+ if (eb == null) return -1;
109
+ return ea - eb; // earliest failure first
110
+ });
111
+ }
112
+
113
+ // Available peers ordered specifically for recovery-time retry: earliest
114
+ // failure first (it has had the longest to come back), never-failed last.
115
+ function orderedByLastError(t = now()) {
116
+ return available().sort((a, b) => {
117
+ const ea = lastErrorAt[a.url];
118
+ const eb = lastErrorAt[b.url];
119
+ if (ea == null && eb == null) return 0;
120
+ if (ea == null) return 1; // never-failed peers go after any that failed
121
+ if (eb == null) return -1;
122
+ return ea - eb; // earliest failure first
123
+ });
98
124
  }
99
125
 
100
126
  let cursor = 0;
@@ -106,6 +132,9 @@ export function createPeersService({
106
132
  return avail[cursor++];
107
133
  }
108
134
 
135
+ // Long-lived error memory: a peer keeps its last-error timestamp until a
136
+ // subsequent success resets it (success clears the failure record) or the
137
+ // error is no longer in the persist store on next load.
109
138
  async function recordError(url) {
110
139
  if (!url) return;
111
140
  lastErrorAt[url] = now();
@@ -113,10 +142,15 @@ export function createPeersService({
113
142
  }
114
143
 
115
144
  // Outcome of a forwarded request: ok updates the hot-cache (EMA latency,
116
- // last successful model); failures bump the consecutive-failure counter.
145
+ // last successful model) and clears the error memory; failures keep the
146
+ // error timestamp so the retry pass can order by recovery time.
117
147
  async function recordResult(url, { ok, latencyMs, model } = {}) {
118
148
  if (!url) return;
119
149
  if (ok) {
150
+ if (lastErrorAt[url] != null) {
151
+ delete lastErrorAt[url];
152
+ await persistErrors({ ...lastErrorAt });
153
+ }
120
154
  const prev = stats[url] || {};
121
155
  stats[url] = {
122
156
  okAt: now(),
@@ -133,5 +167,8 @@ export function createPeersService({
133
167
  await persistStats({ ...stats });
134
168
  }
135
169
 
136
- return { all, add, remove, removeByGroup, isCooling, isHot, stat, ordered, available, next, recordError, recordResult, errors: () => ({ ...lastErrorAt }), stats: () => ({ ...stats }) };
170
+ return {
171
+ all, add, remove, removeByGroup, isCooling, isHot, stat, ordered, orderedByLastError, available, next,
172
+ recordError, recordResult, errors: () => ({ ...lastErrorAt }), stats: () => ({ ...stats }),
173
+ };
137
174
  }
package/src/routes.js CHANGED
@@ -166,6 +166,93 @@ async function forwardToPeer(peer, body, model, hops) {
166
166
  }
167
167
  }
168
168
 
169
+ // Resolve the model a peer should serve for this request: reuse its hot-cache
170
+ // model only when it matches the requested one; otherwise probe /v1/models/status
171
+ // and prefer the requested model, falling back to the peer's first healthy one.
172
+ // Returns { peer, target } or null when the peer is unusable.
173
+ async function resolvePeerTarget(ctx, peer) {
174
+ const prevModel = ctx.peers.stat(peer.url)?.model;
175
+ const hot = ctx.peers.isHot(peer.url) && prevModel === ctx.model;
176
+ if (hot) return { peer, target: prevModel };
177
+ const healthy = await peerHealthyModels(peer);
178
+ if (!healthy.length) {
179
+ // peer unreachable or every model unhealthy — mark it and move on
180
+ await ctx.peers.recordError(peer.url);
181
+ ctx.logError(ctx.model, 0, `peer ${peer.url} has no healthy models`);
182
+ ctx.evt("peer-health", { peer: peer.url, healthy: [], count: 0 });
183
+ return null;
184
+ }
185
+ ctx.evt("peer-health", { peer: peer.url, healthy, count: healthy.length });
186
+ return { peer, target: healthy.includes(ctx.model) ? ctx.model : healthy[0] };
187
+ }
188
+
189
+ // Race a batch of candidates: up to PEER_RACE_LIMIT at a time, first success
190
+ // wins. Uses ctx.model/body/hops/peers to resolve targets and forward. Retries
191
+ // remaining candidates in subsequent batches. Returns the winning
192
+ // { peer, target, res, latencyMs } or null when everyone failed.
193
+ export const PEER_RACE_LIMIT = Number(process.env.MSLXDFF_PEER_RACE_LIMIT) > 0
194
+ ? Number(process.env.MSLXDFF_PEER_RACE_LIMIT)
195
+ : 3;
196
+
197
+ async function racePeerCandidates(candidates, ctx) {
198
+ for (let i = 0; i < candidates.length; i += PEER_RACE_LIMIT) {
199
+ const batch = candidates.slice(i, i + PEER_RACE_LIMIT);
200
+ // resolve targets for this batch first (may probe), then fire them together
201
+ const prepared = (await Promise.all(batch.map((peer) => resolvePeerTarget(ctx, peer)))).filter(Boolean);
202
+ if (!prepared.length) continue;
203
+ // fire every peer in the batch concurrently and record completion order —
204
+ // the first one to succeed wins the race
205
+ const completed = await new Promise((resolve) => {
206
+ const order = [];
207
+ const total = prepared.length;
208
+ for (const { peer, target } of prepared) {
209
+ const t0 = performance.now();
210
+ forwardToPeer(peer, ctx.body, target, ctx.hops).then((res) => {
211
+ const latencyMs = Math.round(performance.now() - t0);
212
+ const failed = res instanceof Error || res.status >= 400;
213
+ ctx.evt("peer-forward", { peer: peer.url, model: target, hops: ctx.hops + 1 });
214
+ if (failed) {
215
+ const status = res instanceof Error ? 502 : res.status;
216
+ ctx.logError(ctx.model,
217
+ status,
218
+ res instanceof Error ? errMsg(res) : `peer ${status}`);
219
+ ctx.evt("peer-error", {
220
+ peer: peer.url,
221
+ model: target,
222
+ status,
223
+ message: res instanceof Error ? errMsg(res) : null,
224
+ });
225
+ order.push({ ok: false, peer, target, res, status });
226
+ } else {
227
+ order.push({ ok: true, peer, target, res, latencyMs });
228
+ }
229
+ if (order.length === total) resolve(order);
230
+ });
231
+ }
232
+ });
233
+ const winner = completed.find((o) => o.ok);
234
+ if (winner) {
235
+ // every other responder also gets its memory cleared and its stats warmed
236
+ // so it becomes a candidate next time too
237
+ for (const o of completed) {
238
+ if (o === winner) continue;
239
+ if (!o.ok) {
240
+ await ctx.peers.recordError(o.peer.url);
241
+ await ctx.peers.recordResult(o.peer.url, { ok: false });
242
+ } else {
243
+ await ctx.peers.recordResult(o.peer.url, { ok: true, latencyMs: o.latencyMs, model: o.target });
244
+ }
245
+ }
246
+ return { peer: winner.peer, target: winner.target, res: winner.res, latencyMs: winner.latencyMs };
247
+ }
248
+ for (const o of completed) {
249
+ await ctx.peers.recordError(o.peer.url);
250
+ await ctx.peers.recordResult(o.peer.url, { ok: false });
251
+ }
252
+ }
253
+ return null;
254
+ }
255
+
169
256
  const ROUTES = [
170
257
  {
171
258
  method: "GET",
@@ -213,8 +300,21 @@ const ROUTES = [
213
300
  };
214
301
  evt("request", { hops, ip: clientIp(req), stream: Boolean(body.stream), prompt: summarizePrompt(body) });
215
302
 
303
+ // Shared context for the peer race helpers below (each model iteration
304
+ // reuses it; `model` is bound per iteration call).
305
+ const handlerCtx = {
306
+ model: null,
307
+ body,
308
+ hops,
309
+ peers,
310
+ evt,
311
+ logError,
312
+ logCall,
313
+ };
314
+
216
315
  let lastErr = null;
217
316
  for (const model of order) {
317
+ handlerCtx.model = model;
218
318
  let upRes = null;
219
319
  const forwarded = { ...injectReasoningContent(model, body), model };
220
320
  try {
@@ -239,62 +339,20 @@ const ROUTES = [
239
339
  return relay(res, upRes, body);
240
340
  }
241
341
 
242
- // local failed for this model: try peers ordered hot-first (reuse
243
- // their last successful model without probing), cold peers get a
244
- // health probe before the forward
342
+ // local failed for this model: race the peers send the request to
343
+ // up to N of them in parallel, first success wins (the winner's model
344
+ // is remembered for the next call). If every candidate fails, retry
345
+ // once ordered by recovery time (earliest failure first), which
346
+ // favours the peer that has had the longest to come back.
245
347
  if (canForwardPeers) {
246
- for (const peer of peers.ordered()) {
247
- let target = null;
248
- const hot = peers.isHot(peer.url);
249
- if (hot && peers.stat(peer.url)?.model) {
250
- target = peers.stat(peer.url).model;
251
- } else {
252
- const healthy = await peerHealthyModels(peer);
253
- if (!healthy.length) {
254
- // peer unreachable or every model unhealthy — mark it and move on
255
- await peers.recordError(peer.url);
256
- logError(model, 0, `peer ${peer.url} has no healthy models`);
257
- evt("peer-health", { peer: peer.url, healthy: [], count: 0 });
258
- continue;
259
- }
260
- evt("peer-health", { peer: peer.url, healthy, count: healthy.length });
261
- target = healthy[0];
262
- }
263
-
264
- const t0 = performance.now();
265
- let peerRes = await forwardToPeer(peer, body, target, hops);
266
- let latencyMs = Math.round(performance.now() - t0);
267
- evt("peer-forward", { peer: peer.url, model: target, hops: hops + 1 });
268
- if (peerRes instanceof Error || peerRes.status >= 400) {
269
- // hot-cache miss: probe this peer once for a healthy model
270
- if (hot) {
271
- const healthy = await peerHealthyModels(peer);
272
- if (healthy.length) {
273
- target = healthy[0];
274
- const t1 = performance.now();
275
- peerRes = await forwardToPeer(peer, body, target, hops);
276
- latencyMs = Math.round(performance.now() - t1);
277
- evt("peer-forward", { peer: peer.url, model: target, hops: hops + 1, retry: true });
278
- }
279
- }
280
- }
281
- if (peerRes instanceof Error || peerRes.status >= 400) {
282
- await peers.recordError(peer.url);
283
- await peers.recordResult(peer.url, { ok: false });
284
- logError(model, peerRes instanceof Error ? 502 : peerRes.status,
285
- peerRes instanceof Error ? errMsg(peerRes) : `peer ${peerRes.status}`);
286
- evt("peer-error", {
287
- peer: peer.url,
288
- model: target,
289
- status: peerRes instanceof Error ? 502 : peerRes.status,
290
- message: peerRes instanceof Error ? errMsg(peerRes) : null,
291
- });
292
- continue;
293
- }
294
- await peers.recordResult(peer.url, { ok: true, latencyMs, model: target });
295
- logCall(target, peerRes.status);
296
- evt("result", { model: target, status: peerRes.status, via: "peer" });
297
- return relay(res, peerRes, body);
348
+ const win =
349
+ (await racePeerCandidates(peers.ordered(), handlerCtx)) ||
350
+ (await racePeerCandidates(peers.orderedByLastError(), handlerCtx));
351
+ if (win) {
352
+ await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
353
+ logCall(win.target, win.res.status);
354
+ evt("result", { model: win.target, status: win.res.status, via: "peer" });
355
+ return relay(res, win.res, body);
298
356
  }
299
357
  }
300
358