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.
- package/package.json +1 -1
- package/src/peers.js +42 -5
- package/src/routes.js +113 -55
package/package.json
CHANGED
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
|
|
95
|
-
//
|
|
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) =>
|
|
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
|
|
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 {
|
|
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:
|
|
243
|
-
//
|
|
244
|
-
//
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|