mslxdff 0.1.35 → 0.1.37
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/bin/mslxdff.js +32 -5
- package/package.json +1 -1
- package/src/auto.js +2 -3
- package/src/routes.js +172 -17
package/bin/mslxdff.js
CHANGED
|
@@ -1179,17 +1179,42 @@ function fmtEvent(e) {
|
|
|
1179
1179
|
const t = e?.ts ? new Date(e.ts).toISOString().slice(11, 19) : "--:--:--";
|
|
1180
1180
|
const head = `[${t}]`;
|
|
1181
1181
|
const m = (x) => x || "-";
|
|
1182
|
+
const fallbackTag = e.fallback ? ` fallback=${e.fallback.requested_model || e.requested}->${e.fallback.actual_model || e.actual}(${e.fallback.reason || e.reason})` : "";
|
|
1182
1183
|
switch (e?.type) {
|
|
1183
1184
|
case "request":
|
|
1184
|
-
return `${head} request ${m(e.model)}${e.auto ? " (auto)" : ""} hops=${e.hops} from ${e.ip || "?"}${e.stream ? " stream" : ""}${e.prompt ? ` content="${e.prompt}"` : ""}`;
|
|
1185
|
+
return `${head} request client-> ${m(e.requested || e.model)}${e.auto ? " (auto)" : ""} raw=${m(e.rawModel)} lock=${m(e.lockModel)} hops=${e.hops} from ${e.ip || "?"}${e.stream ? " stream" : ""}${e.prompt ? ` content="${e.prompt}"` : ""} reqId=${e.reqId || ""}`;
|
|
1186
|
+
case "client-request":
|
|
1187
|
+
return `${head} client req 客户端请求 model=${m(e.requested)} raw=${m(e.rawModel)} lock=${m(e.lockModel)} ip=${e.ip || "?"}${e.stream ? " stream" : ""}`;
|
|
1188
|
+
case "ordered":
|
|
1189
|
+
return `${head} ordered 尝试顺序 [${(e.order||[]).join(" -> ")}] canFallback=${e.canFallback} useAuto=${e.useAuto}`;
|
|
1190
|
+
case "model-try":
|
|
1191
|
+
return `${head} model-try 尝试本地 model=${m(e.model)} idx=${e.idx} reqId=${e.reqId || ""}`;
|
|
1192
|
+
case "upstream-try":
|
|
1193
|
+
return `${head} upstream try 上游请求 model=${m(e.model)} attempt=${e.attempt || 1}`;
|
|
1194
|
+
case "upstream-done":
|
|
1195
|
+
return `${head} upstream ok ${m(e.model)} HTTP ${e.status}${e.timing ? ` total=${e.timing.totalMs}ms` : ""}`;
|
|
1185
1196
|
case "upstream-error":
|
|
1186
1197
|
return `${head} upstream err ${m(e.model)} ${e.status ? `HTTP ${e.status}` : "network"}: ${m(e.message)}`;
|
|
1198
|
+
case "peer-race-start":
|
|
1199
|
+
return `${head} peer race 开始并发给组员 model=${m(e.model)} peers=${e.peers}`;
|
|
1187
1200
|
case "peer-health":
|
|
1188
|
-
return `${head} peer check ${e.peer} -> ${e.count ? e.healthy.join(", ") : "no healthy models"}`;
|
|
1201
|
+
return `${head} peer check ${e.peer} -> ${e.count ? e.healthy.join(", ") : "no healthy models"}${e.strict ? " (strict)" : ""}`;
|
|
1189
1202
|
case "peer-forward":
|
|
1190
|
-
return `${head}
|
|
1203
|
+
return `${head} peer req 给组员请求 peer=${e.peer} model=${m(e.model)} hops=${e.hops}${e.retry ? " (retry)" : ""}`;
|
|
1191
1204
|
case "peer-error":
|
|
1192
|
-
return `${head} peer err ${e.peer} ${e.status ? `HTTP ${e.status}` : "network"}: ${m(e.message)}`;
|
|
1205
|
+
return `${head} peer err ${e.peer} ${e.status ? `HTTP ${e.status}` : "network"}: ${m(e.message)} model=${m(e.model)}`;
|
|
1206
|
+
case "peer-race-win":
|
|
1207
|
+
return `${head} peer win 选中 peer=${e.winPeer} model=${m(e.winTarget)} latency=${e.latencyMs}ms`;
|
|
1208
|
+
case "peer-race-lose":
|
|
1209
|
+
return `${head} peer lose 组员全部失败 model=${m(e.model)}`;
|
|
1210
|
+
case "fallback":
|
|
1211
|
+
return `${head} fallback ${m(e.from)} -> ${m(e.to)} reason=${m(e.reason)}`;
|
|
1212
|
+
case "fallback-notice":
|
|
1213
|
+
return `${head} fallback! 客户端请求 ${m(e.requested)} 实际返回 ${m(e.actual)} 原因=${m(e.reason)} via=${m(e.via)} notice=${m(e.notice)}`;
|
|
1214
|
+
case "relay-try":
|
|
1215
|
+
return `${head} relay try 给宽带中继请求 target=${e.target} model=${m(e.model)} via=${e.via}`;
|
|
1216
|
+
case "relay-fail":
|
|
1217
|
+
return `${head} relay fail ${e.target} ${e.status ? `HTTP ${e.status}` : ""} ${m(e.message)}`;
|
|
1193
1218
|
case "relay-ip-change":
|
|
1194
1219
|
return `${head} relay ip ${e.member || e.id || "?"} ${e.oldIp || "?"} -> ${e.newIp || e.publicIp || "?"} via leader`;
|
|
1195
1220
|
case "relay-forward":
|
|
@@ -1199,7 +1224,9 @@ function fmtEvent(e) {
|
|
|
1199
1224
|
case "client-abort":
|
|
1200
1225
|
return `${head} client abort ${m(e.model)} total=${fmtDur(e.totalMs)}`;
|
|
1201
1226
|
case "result":
|
|
1202
|
-
return `${head} result
|
|
1227
|
+
return `${head} result 返回客户端 status=${e.status} model=${m(e.model)} via=${e.via} 响应耗时 ${fmtDur(e.durationMs)}${fallbackTag}`;
|
|
1228
|
+
case "client-response":
|
|
1229
|
+
return `${head} client res 返回客户端 客户端请求 ${m(e.requested)} 实际返回 ${m(e.actual)} via=${m(e.via)}${e.fallback ? ` fallback=${e.fallback.requested_model}->${e.fallback.actual_model}(${e.fallback.reason})` : " 无fallback"} status=${e.status}`;
|
|
1203
1230
|
case "auto-update-enabled":
|
|
1204
1231
|
return `${head} auto-update enabled every ${Math.round((e.intervalMs||0)/60000)}m current=${e.current}`;
|
|
1205
1232
|
case "auto-update-disabled":
|
package/package.json
CHANGED
package/src/auto.js
CHANGED
|
@@ -132,9 +132,8 @@ export function createAutoSelector({
|
|
|
132
132
|
slowCooldownMs,
|
|
133
133
|
latencies,
|
|
134
134
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
135
|
+
// 显式指定模型:严格优先,永不因冷却被挤到最后(原设计:A deepseek 失败 → B/D deepseek 并发 → 都失败才 fallback)
|
|
136
|
+
// 冷却仅影响 auto 的择优,不影响指定模型的“很难被更改”语义
|
|
138
137
|
return [requested, ...others];
|
|
139
138
|
}
|
|
140
139
|
|
package/src/routes.js
CHANGED
|
@@ -193,6 +193,102 @@ function notFound(res) {
|
|
|
193
193
|
return json(res, 404, { error: "Not Found" });
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// --- fallback 显式提示(巧妙不破兼容)---
|
|
197
|
+
// 机器可读:x-mslxdff-* headers;人类可读:mslxdff 字段 + SSE comment
|
|
198
|
+
function fallbackReason(lastErr) {
|
|
199
|
+
if (!lastErr) return "cooldown";
|
|
200
|
+
const s = Number(lastErr.status);
|
|
201
|
+
if (s === 429) return "rate_limited";
|
|
202
|
+
if (lastErr.message && /timeout/i.test(String(lastErr.message))) return "timeout";
|
|
203
|
+
if (s === 502 || s === 503 || s === 504) return "upstream_error";
|
|
204
|
+
if (s >= 400) return "upstream_error";
|
|
205
|
+
return "fallback";
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildFallbackInfo({ requested, actual, lastErr, via, useAuto, lockModel }) {
|
|
209
|
+
if (!requested || !actual) return null;
|
|
210
|
+
const alwaysHeaders = {
|
|
211
|
+
requested_model: requested,
|
|
212
|
+
actual_model: actual,
|
|
213
|
+
via: via || "local",
|
|
214
|
+
};
|
|
215
|
+
// auto / lock 仍告知 actual,但不算 fallback
|
|
216
|
+
if (useAuto || lockModel) {
|
|
217
|
+
return { ...alwaysHeaders, fallback: false, reason: null, notice: null };
|
|
218
|
+
}
|
|
219
|
+
const isFallback = requested !== actual;
|
|
220
|
+
if (!isFallback) {
|
|
221
|
+
return { ...alwaysHeaders, fallback: false, reason: null, notice: null };
|
|
222
|
+
}
|
|
223
|
+
const reason = fallbackReason(lastErr);
|
|
224
|
+
const reasonZh = reason === "rate_limited" ? "限流" : reason === "timeout" ? "超时" : reason === "cooldown" ? "冷却中" : "不可用";
|
|
225
|
+
const notice = `${requested} ${reasonZh},已由 ${actual} 代答`;
|
|
226
|
+
return { ...alwaysHeaders, fallback: true, reason, notice };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function applyFallbackHeaders(res, info) {
|
|
230
|
+
if (!info) return;
|
|
231
|
+
// 始终告知实际与请求,客户端对比即知
|
|
232
|
+
if (info.requested_model) res.setHeader("x-mslxdff-requested-model", info.requested_model);
|
|
233
|
+
if (info.actual_model) res.setHeader("x-mslxdff-actual-model", info.actual_model);
|
|
234
|
+
if (info.via) res.setHeader("x-mslxdff-via", info.via);
|
|
235
|
+
if (info.fallback) {
|
|
236
|
+
res.setHeader("x-mslxdff-fallback", "1");
|
|
237
|
+
if (info.reason) res.setHeader("x-mslxdff-fallback-reason", info.reason);
|
|
238
|
+
// 人类 curl 可见
|
|
239
|
+
if (info.notice) res.setHeader("x-mslxdff-notice", encodeURIComponent(info.notice));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function enrichNonStreamJson(obj, info) {
|
|
244
|
+
if (!info || typeof obj !== "object" || obj === null) return obj;
|
|
245
|
+
// 仅当 fallback 时才注入顶层 mslxdff,避免噪音;但始终可通过 header 拿到 actual
|
|
246
|
+
if (!info.fallback) return obj;
|
|
247
|
+
if (obj.mslxdff) return obj;
|
|
248
|
+
return {
|
|
249
|
+
...obj,
|
|
250
|
+
mslxdff: {
|
|
251
|
+
fallback: true,
|
|
252
|
+
requested_model: info.requested_model,
|
|
253
|
+
actual_model: info.actual_model,
|
|
254
|
+
reason: info.reason,
|
|
255
|
+
via: info.via,
|
|
256
|
+
notice: info.notice,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function enrichSseChunkText(text, info) {
|
|
262
|
+
if (!info?.fallback) return text;
|
|
263
|
+
// 行级注入:对每行 data: {json} 尝试注入 mslxdff
|
|
264
|
+
const lines = text.split("\n");
|
|
265
|
+
let changed = false;
|
|
266
|
+
for (let i = 0; i < lines.length; i++) {
|
|
267
|
+
const line = lines[i];
|
|
268
|
+
const m = /^data:\s*(\{.*\})\s*$/.exec(line);
|
|
269
|
+
if (!m) continue;
|
|
270
|
+
try {
|
|
271
|
+
const obj = JSON.parse(m[1]);
|
|
272
|
+
if (obj && typeof obj === "object" && !obj.mslxdff) {
|
|
273
|
+
obj.mslxdff = {
|
|
274
|
+
fallback: true,
|
|
275
|
+
requested_model: info.requested_model,
|
|
276
|
+
actual_model: info.actual_model,
|
|
277
|
+
reason: info.reason,
|
|
278
|
+
via: info.via,
|
|
279
|
+
notice: info.notice,
|
|
280
|
+
};
|
|
281
|
+
lines[i] = `data: ${JSON.stringify(obj)}`;
|
|
282
|
+
changed = true;
|
|
283
|
+
break; // 仅注入首个 JSON 行
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return changed ? lines.join("\n") : text;
|
|
290
|
+
}
|
|
291
|
+
|
|
196
292
|
function readBody(req) {
|
|
197
293
|
return new Promise((resolve, reject) => {
|
|
198
294
|
let data = "";
|
|
@@ -211,11 +307,12 @@ function readBody(req) {
|
|
|
211
307
|
// Relay an upstream response to the client. Returns { status, ttfMs, aborted, interrupted, detail }
|
|
212
308
|
// detail carries byte/chunk/sawDone diagnostics so a truncated deep-think
|
|
213
309
|
// stream can be told apart from a clean EOF vs our stall/max vs client abort.
|
|
214
|
-
async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort, streamTimeoutMs = STREAM_TIMEOUT_MS } = {}) {
|
|
310
|
+
async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort, streamTimeoutMs = STREAM_TIMEOUT_MS, fallback } = {}) {
|
|
215
311
|
const t0 = performance.now();
|
|
216
312
|
const contentType = upRes.headers.get("content-type") || "";
|
|
217
313
|
const isStream = Boolean(body?.stream) || contentType.includes("text/event-stream");
|
|
218
314
|
res.statusCode = upRes.status;
|
|
315
|
+
if (fallback) applyFallbackHeaders(res, fallback);
|
|
219
316
|
|
|
220
317
|
let ttf = null;
|
|
221
318
|
let interrupted = false;
|
|
@@ -246,6 +343,13 @@ async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort, stream
|
|
|
246
343
|
res.setHeader("Content-Type", "text/event-stream");
|
|
247
344
|
res.setHeader("Cache-Control", "no-cache");
|
|
248
345
|
res.setHeader("Connection", "keep-alive");
|
|
346
|
+
// SSE 注释:curl -N 可见,EventSource/SDK 自动忽略,不污染 content
|
|
347
|
+
if (fallback?.fallback) {
|
|
348
|
+
try {
|
|
349
|
+
res.write(`: mslxdff fallback ${fallback.requested_model} -> ${fallback.actual_model} (${fallback.reason})\n`);
|
|
350
|
+
res.write(`: notice ${fallback.notice}\n\n`);
|
|
351
|
+
} catch {}
|
|
352
|
+
}
|
|
249
353
|
if (upRes.body) {
|
|
250
354
|
let first = true;
|
|
251
355
|
let wroteAny = false;
|
|
@@ -301,10 +405,24 @@ async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort, stream
|
|
|
301
405
|
onFirstChunk?.(ttf);
|
|
302
406
|
if (firstTimer) { clearTimeout(firstTimer); firstTimer = null; }
|
|
303
407
|
}
|
|
408
|
+
// 首块注入 mslxdff 字段(仅 fallback 时),SDK 解析 JSON 可直接发现
|
|
409
|
+
let outChunk = chunk;
|
|
410
|
+
if (first === false && fallback?.fallback && wroteAny === false) {
|
|
411
|
+
try {
|
|
412
|
+
let txt = "";
|
|
413
|
+
if (Buffer.isBuffer(chunk)) txt = chunk.toString("utf8");
|
|
414
|
+
else if (chunk instanceof Uint8Array) txt = Buffer.from(chunk).toString("utf8");
|
|
415
|
+
else if (typeof chunk === "string") txt = chunk;
|
|
416
|
+
if (txt.includes("data:")) {
|
|
417
|
+
const enriched = enrichSseChunkText(txt, fallback);
|
|
418
|
+
if (enriched !== txt) outChunk = Buffer.from(enriched, "utf8");
|
|
419
|
+
}
|
|
420
|
+
} catch {}
|
|
421
|
+
}
|
|
304
422
|
wroteAny = true;
|
|
305
423
|
detail.wroteChunks += 1;
|
|
306
|
-
detail.wroteBytes += len;
|
|
307
|
-
res.write(
|
|
424
|
+
detail.wroteBytes += Buffer.isBuffer(outChunk) ? outChunk.length : (outChunk?.length ?? len);
|
|
425
|
+
res.write(outChunk);
|
|
308
426
|
armStall(); // no-op when STALL_TIMEOUT_MS=0; scoring uses SCORE_STALL_MS gap above
|
|
309
427
|
}
|
|
310
428
|
if (!detail.exitReason) detail.exitReason = "normal";
|
|
@@ -346,7 +464,9 @@ async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort, stream
|
|
|
346
464
|
detail.receivedBytes = Buffer.byteLength(text);
|
|
347
465
|
detail.exitReason = "normal-non-stream";
|
|
348
466
|
try {
|
|
349
|
-
|
|
467
|
+
const parsed = JSON.parse(text);
|
|
468
|
+
const enriched = enrichNonStreamJson(parsed, fallback);
|
|
469
|
+
json(res, upRes.status, enriched);
|
|
350
470
|
} catch {
|
|
351
471
|
res.statusCode = upRes.status;
|
|
352
472
|
res.setHeader("Content-Type", contentType || "text/plain");
|
|
@@ -428,17 +548,35 @@ async function forwardToPeer(peer, body, model, hops) {
|
|
|
428
548
|
}
|
|
429
549
|
}
|
|
430
550
|
|
|
431
|
-
// Resolve the model a peer should serve for this request
|
|
432
|
-
//
|
|
433
|
-
//
|
|
551
|
+
// Resolve the model a peer should serve for this request.
|
|
552
|
+
// 原设计严格语义:显式指定模型时,永远用该模型去试 peer,不因 peer 的本地 healthy 状态而偷换成 hy3。
|
|
553
|
+
// 只有 auto 模式才走 healthy 探测与择优。
|
|
434
554
|
// Returns { peer, target } or null when the peer is unusable.
|
|
435
555
|
async function resolvePeerTarget(ctx, peer) {
|
|
436
556
|
const prevModel = ctx.peers.stat(peer.url)?.model;
|
|
437
557
|
const hot = ctx.peers.isHot(peer.url) && prevModel === ctx.model;
|
|
438
558
|
if (hot) return { peer, target: prevModel };
|
|
559
|
+
// 显式模型:严格用请求模型,不做 healthy 偷换(B/D 必须以 deepseek 去试,失败才算该模型在该 peer 不可用)
|
|
560
|
+
const isExplicit = !!ctx.model && !isAutoModel(ctx.model);
|
|
561
|
+
if (isExplicit) {
|
|
562
|
+
// 仅做可达性探测:轻量 ping /v1/models/status 判断 peer 是否活着,不因模型状态过滤
|
|
563
|
+
const healthy = await peerHealthyModels(peer);
|
|
564
|
+
if (!healthy.length) {
|
|
565
|
+
// 无法探活也仍尝试:让 forward 去试,失败会由 race 逻辑记错;但为保持原有“全不健康则跳过”行为,仍标记
|
|
566
|
+
// 这里改为:即使 healthy 为空,也返回 target=ctx.model,让上游去判 429,而不是直接丢弃 peer
|
|
567
|
+
// 只有当 fetch 本身异常(healthy=[] 来自网络错)才视为 peer 不可用,需区分
|
|
568
|
+
// peerHealthyModels 在网络错时返回 [],此时应视为 peer 不可用
|
|
569
|
+
// 我们通过再次轻量探测区分:若 peer 完全不可达,healthy=[] 且 peer 曾无成功记录,则跳过
|
|
570
|
+
// 简化:若 healthy 为空,直接尝试目标模型,失败再记错(更符合“严格”)
|
|
571
|
+
ctx.evt("peer-health", { peer: peer.url, healthy: [], count: 0, strict: true });
|
|
572
|
+
return { peer, target: ctx.model };
|
|
573
|
+
}
|
|
574
|
+
ctx.evt("peer-health", { peer: peer.url, healthy, count: healthy.length, strict: true });
|
|
575
|
+
return { peer, target: ctx.model };
|
|
576
|
+
}
|
|
577
|
+
// auto 模式:走原有择优逻辑
|
|
439
578
|
const healthy = await peerHealthyModels(peer);
|
|
440
579
|
if (!healthy.length) {
|
|
441
|
-
// peer unreachable or every model unhealthy — mark it and move on
|
|
442
580
|
await ctx.peers.recordError(peer.url);
|
|
443
581
|
ctx.logError(ctx.model, 0, `peer ${peer.url} has no healthy models`);
|
|
444
582
|
ctx.evt("peer-health", { peer: peer.url, healthy: [], count: 0 });
|
|
@@ -647,8 +785,11 @@ const ROUTES = [
|
|
|
647
785
|
}
|
|
648
786
|
if (upRes) {
|
|
649
787
|
logCall(model, upRes.status);
|
|
650
|
-
|
|
788
|
+
const fallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "local", useAuto, lockModel });
|
|
789
|
+
if (fallback?.fallback) evt("fallback-notice", { reqId, requested, actual: model, reason: fallback.reason, notice: fallback.notice, via: "local" });
|
|
790
|
+
evt("relay-start", { reqId, model, via: "local", isStream: Boolean(body.stream), fallback });
|
|
651
791
|
const out = await relay(res, upRes, body, {
|
|
792
|
+
fallback,
|
|
652
793
|
onFirstChunk: (delta) => {
|
|
653
794
|
mark(`ttf-${model}`);
|
|
654
795
|
evt("relay-first-chunk", { reqId, model, ttfMs: delta });
|
|
@@ -674,7 +815,8 @@ const ROUTES = [
|
|
|
674
815
|
}
|
|
675
816
|
evt("slow-model", { model, elapsedMs: out.totalMs ?? (Date.now() - startedAt), threshold: STALL_TIMEOUT_MS, interrupted: true, detail: out.detail ?? null });
|
|
676
817
|
logCall(model, 200);
|
|
677
|
-
evt("result", { model, status: out.status, via: "local", timing: upRes._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, interrupted: true, detail: out.detail ?? null });
|
|
818
|
+
evt("result", { model, status: out.status, via: "local", timing: upRes._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, interrupted: true, detail: out.detail ?? null, fallback, requested, actual: model });
|
|
819
|
+
evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, interrupted: true, reqId });
|
|
678
820
|
return;
|
|
679
821
|
}
|
|
680
822
|
const elapsed = Date.now() - startedAt;
|
|
@@ -700,7 +842,8 @@ const ROUTES = [
|
|
|
700
842
|
} else if (scoredSlow && out.detail) {
|
|
701
843
|
// already recorded slow+latency above, still ensure latency EMA is updated for slow case (done)
|
|
702
844
|
}
|
|
703
|
-
evt("result", { model, status: out.status, via: "local", timing: upRes._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null });
|
|
845
|
+
evt("result", { model, status: out.status, via: "local", timing: upRes._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null, fallback, requested, actual: model });
|
|
846
|
+
evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, reqId });
|
|
704
847
|
return;
|
|
705
848
|
}
|
|
706
849
|
|
|
@@ -718,8 +861,11 @@ const ROUTES = [
|
|
|
718
861
|
evt("peer-race-win", { reqId, model, winPeer: win.peer.url, winTarget: win.target, latencyMs: win.latencyMs });
|
|
719
862
|
await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
|
|
720
863
|
logCall(win.target, win.res.status);
|
|
721
|
-
|
|
864
|
+
const peerFallback = buildFallbackInfo({ requested, actual: win.target, lastErr, via: "peer", useAuto, lockModel });
|
|
865
|
+
if (peerFallback?.fallback) evt("fallback-notice", { reqId, requested, actual: win.target, reason: peerFallback.reason, notice: peerFallback.notice, via: "peer" });
|
|
866
|
+
evt("relay-start", { reqId, model: win.target, via: "peer", isStream: Boolean(body.stream), fallback: peerFallback });
|
|
722
867
|
const out = await relay(res, win.res, body, {
|
|
868
|
+
fallback: peerFallback,
|
|
723
869
|
onFirstChunk: (d) => mark(`ttf-peer-${win.target}`),
|
|
724
870
|
onDownstreamAbort: () => evt("client-abort", { reqId, model: win.target, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
|
|
725
871
|
});
|
|
@@ -733,7 +879,8 @@ const ROUTES = [
|
|
|
733
879
|
await auto.recordOk(win.target, { latencyMs });
|
|
734
880
|
}
|
|
735
881
|
}
|
|
736
|
-
evt("result", { model: win.target, status: out.status, via: "peer", timing: win.res._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null });
|
|
882
|
+
evt("result", { model: win.target, status: out.status, via: "peer", timing: win.res._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null, fallback: peerFallback, requested, actual: win.target });
|
|
883
|
+
evt("client-response", { requested, actual: win.target, via: "peer", fallback: peerFallback, status: out.status, reqId });
|
|
737
884
|
return;
|
|
738
885
|
}
|
|
739
886
|
evt("peer-race-lose", { reqId, model });
|
|
@@ -746,8 +893,11 @@ const ROUTES = [
|
|
|
746
893
|
const isResponse = bb.result && typeof bb.result.status === "number" && typeof bb.result.headers?.get === "function";
|
|
747
894
|
if (isResponse) {
|
|
748
895
|
// streaming response from leader's forward (which waited for broadband)
|
|
749
|
-
|
|
896
|
+
const bbFallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "broadband", useAuto, lockModel });
|
|
897
|
+
if (bbFallback?.fallback) evt("fallback-notice", { reqId, requested, actual: model, reason: bbFallback.reason, notice: bbFallback.notice, via: "broadband" });
|
|
898
|
+
evt("relay-start", { reqId, model, via: "broadband", target: bb.target, group: bb.group, fallback: bbFallback });
|
|
750
899
|
const out = await relay(res, bb.result, body, {
|
|
900
|
+
fallback: bbFallback,
|
|
751
901
|
onFirstChunk: (d) => mark(`ttf-bb-${model}`),
|
|
752
902
|
onDownstreamAbort: () => evt("client-abort", { reqId, model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
|
|
753
903
|
});
|
|
@@ -761,7 +911,8 @@ const ROUTES = [
|
|
|
761
911
|
await auto.recordOk(model, { latencyMs });
|
|
762
912
|
}
|
|
763
913
|
}
|
|
764
|
-
evt("result", { model, status: out.status, via: "broadband", timing: bb.result._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null });
|
|
914
|
+
evt("result", { model, status: out.status, via: "broadband", timing: bb.result._t ?? null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null, fallback: bbFallback, requested, actual: model });
|
|
915
|
+
evt("client-response", { requested, actual: model, via: "broadband", fallback: bbFallback, status: out.status, reqId });
|
|
765
916
|
return;
|
|
766
917
|
} else if (bb.result && typeof bb.result.status === "number") {
|
|
767
918
|
// buffered result from local leader enqueue
|
|
@@ -779,13 +930,17 @@ const ROUTES = [
|
|
|
779
930
|
return null;
|
|
780
931
|
})(),
|
|
781
932
|
};
|
|
782
|
-
|
|
933
|
+
const bbLocalFallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "broadband", useAuto, lockModel });
|
|
934
|
+
if (bbLocalFallback?.fallback) evt("fallback-notice", { reqId, requested, actual: model, reason: bbLocalFallback.reason, notice: bbLocalFallback.notice, via: "broadband" });
|
|
935
|
+
evt("relay-start", { reqId, model, via: "broadband-local", target: bb.target, group: bb.group, fallback: bbLocalFallback });
|
|
783
936
|
const out = await relay(res, fakeRes, body, {
|
|
937
|
+
fallback: bbLocalFallback,
|
|
784
938
|
onFirstChunk: (d) => mark(`ttf-bb-${model}`),
|
|
785
939
|
onDownstreamAbort: () => evt("client-abort", { reqId, model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] }),
|
|
786
940
|
});
|
|
787
941
|
evt("relay-done", { reqId, model, via: "broadband-local", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
|
|
788
|
-
evt("result", { model, status: out.status, via: "broadband", timing: null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null });
|
|
942
|
+
evt("result", { model, status: out.status, via: "broadband", timing: null, ttfMs: out.ttfMs, totalMs: out.totalMs, detail: out.detail ?? null, fallback: bbLocalFallback, requested, actual: model });
|
|
943
|
+
evt("client-response", { requested, actual: model, via: "broadband", fallback: bbLocalFallback, status: out.status, reqId });
|
|
789
944
|
return;
|
|
790
945
|
}
|
|
791
946
|
}
|