mslxdff 0.1.89 → 0.1.91
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/chat/engine.js +160 -0
- package/src/chat/repl.js +32 -299
- package/src/chat/terminal.js +95 -0
- package/src/chat/tool-handlers.js +67 -0
- package/src/chat/upstream.js +1 -4
- package/src/chat-pipeline/auto-race.js +124 -0
- package/src/chat-pipeline/engine.js +10 -234
- package/src/chat-pipeline/serial-trial.js +144 -0
- package/src/cli/commands/model/list-providers.js +75 -0
- package/src/cli/commands/model/list-render.js +79 -0
- package/src/cli/commands/model/list.js +208 -0
- package/src/cli/commands/model/picks.js +45 -0
- package/src/cli/commands/model/stats.js +43 -0
- package/src/cli/commands/model/status.js +47 -0
- package/src/cli/commands/model.js +17 -371
- package/src/cli/help.js +2 -1
- package/src/providers/cline/chat.js +15 -2
- package/src/routes/chat/relay-pipeline.js +26 -0
- package/src/runtime/bootstrap.js +14 -469
- package/src/runtime/broadband-stream.js +76 -0
- package/src/runtime/broadband.js +97 -0
- package/src/runtime/group-sync.js +28 -0
- package/src/runtime/providers-setup.js +147 -0
- package/src/runtime/server-lifecycle.js +156 -0
- package/src/state/facade.js +1 -0
- package/src/state/schemas/model.js +41 -0
- package/src/upstream-responses.js +64 -0
- package/src/upstream.js +5 -59
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具执行层 — 从 engine.js 抽出的工具段。
|
|
3
|
+
* buildDedupKey 纯函数(去重键归一);runTool 执行单工具调用并返回结果文本。
|
|
4
|
+
* SKIPPED_DUP/forceNoTools 状态机仍在 engine runTurn 侧。
|
|
5
|
+
*/
|
|
6
|
+
export function buildDedupKey(name, args = {}) {
|
|
7
|
+
if (name === "run_command") {
|
|
8
|
+
const cmd = String(args.command || "").trim().toLowerCase().replace(/\s+/g, " ");
|
|
9
|
+
const norm = cmd.replace(/^-+providers\b/, "-provider").replace(/\s+/g, " ").trim();
|
|
10
|
+
return `run_command:${norm}`;
|
|
11
|
+
}
|
|
12
|
+
if (name === "curl") {
|
|
13
|
+
const u = String(args.url || "").trim().toLowerCase();
|
|
14
|
+
const m = String(args.method || "GET").toUpperCase();
|
|
15
|
+
return `curl:${m}:${u}:${String(args.body || "").slice(0, 200)}`;
|
|
16
|
+
}
|
|
17
|
+
if (name === "read_file") {
|
|
18
|
+
return `read_file:${String(args.path || "").trim().toLowerCase()}`;
|
|
19
|
+
}
|
|
20
|
+
return `${name}:${JSON.stringify(args)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runTool({
|
|
24
|
+
name,
|
|
25
|
+
args = {},
|
|
26
|
+
userText = "",
|
|
27
|
+
execCommand = async () => ({ ok: false, output: "no exec" }),
|
|
28
|
+
readFileTool = async () => ({ ok: false, output: "no read" }),
|
|
29
|
+
curlTool = async () => ({ ok: false, output: "no curl" }),
|
|
30
|
+
onTrace = () => {},
|
|
31
|
+
} = {}) {
|
|
32
|
+
const t1 = performance.now();
|
|
33
|
+
if (name === "run_command") {
|
|
34
|
+
const cmd = String(args.command || "").trim();
|
|
35
|
+
const r = await execCommand(cmd);
|
|
36
|
+
let result = `${r.ok ? "OK" : "FAIL"}: ${r.output}`;
|
|
37
|
+
const lowCmd = cmd.toLowerCase().replace(/\s+/g, " ").trim();
|
|
38
|
+
const asksModel = String(userText || "").toLowerCase().includes("模型");
|
|
39
|
+
const isOnceAndDone =
|
|
40
|
+
/^-+(showtoken|status|s|providers?\b|model\b|group\b|log\b|workbuddy\b|free\b|autostart\b|plugins\b)/.test(lowCmd) ||
|
|
41
|
+
lowCmd === "-provider list" || lowCmd === "-providers list";
|
|
42
|
+
if (isOnceAndDone && r.ok) {
|
|
43
|
+
if (asksModel && lowCmd.includes("-provider")) {
|
|
44
|
+
result += `\n\n[提示:此命令仅显示供应商配置,不包含模型列表。用户问的是“有哪些模型”,请用系统提示中的“可用模型”按前缀过滤回答,或调 curl local/models,不要再调 provider list]`;
|
|
45
|
+
} else {
|
|
46
|
+
result += `\n\n[系统提示:此查询已完成,结果即答案,请直接用中文回答用户,禁止再调用相同或同类查询工具]`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
onTrace(`[tool] run_command "${cmd.slice(0, 40)}" · ${Math.round(performance.now() - t1)}ms · ${r.ok ? "OK" : "FAIL"} ${r.output.length}字`);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
if (name === "read_file") {
|
|
53
|
+
const r = await readFileTool(args);
|
|
54
|
+
let result = `${r.ok ? "OK" : "FAIL"}: ${r.output.slice(0, 6000)}`;
|
|
55
|
+
if (r.ok) result += `\n\n[系统提示:文件已读取,请直接基于内容回答,禁止重复读取同一文件]`;
|
|
56
|
+
onTrace(`[tool] read_file ${args.path} · ${Math.round(performance.now() - t1)}ms · ${r.ok ? "OK" : "FAIL"} ${r.output.length}字`);
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
if (name === "curl") {
|
|
60
|
+
const u = String(args.url || "").trim();
|
|
61
|
+
const r = await curlTool(args);
|
|
62
|
+
const result = `${r.ok ? "OK" : "FAIL"}: ${r.output.slice(0, 6000)}`;
|
|
63
|
+
onTrace(`[tool] curl ${u} · ${Math.round(performance.now() - t1)}ms`);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
return `unknown tool ${name}`;
|
|
67
|
+
}
|
package/src/chat/upstream.js
CHANGED
|
@@ -55,7 +55,4 @@ const orch = createOrchestrator({
|
|
|
55
55
|
export const chatOnce = direct.chatOnce;
|
|
56
56
|
export const chatWithFallback = orch.chatWithFallback;
|
|
57
57
|
export const summarizeHistory = orch.summarizeHistory;
|
|
58
|
-
|
|
59
|
-
// 兼容旧 chat-repl.test.js 的文件内容断言(保留关键字)
|
|
60
|
-
// chatViaGateway gateway auto safeChatOnce
|
|
61
|
-
export const __compat = "chatViaGateway gateway auto safeChatOnce";
|
|
58
|
+
// keywords for legacy file-content check: chatViaGateway gateway auto safeChatOnce
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { injectReasoningContent } from "../reasoning.js";
|
|
3
|
+
import { runHook } from "../plugins.js";
|
|
4
|
+
import { errMsg } from "../routes/helpers.js";
|
|
5
|
+
import { handleLocalRelay } from "../routes/chat/local-handler.js";
|
|
6
|
+
import { handleExhaustedAll } from "../routes/chat/exhausted-handler.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* auto 首次并发择优 — 从 engine.js 抽出的第一段。
|
|
10
|
+
* 无 prior-success 且多非冷却候选时并发竞速,胜者走 local relay。
|
|
11
|
+
* 返回 { done:true } 表示已终结(handled/exhausted),调用方直接 return;
|
|
12
|
+
* 返回 { done:false, order } 表示未终结,调用方用过滤后的 order 继续。
|
|
13
|
+
*/
|
|
14
|
+
export async function runAutoRace(ctx, deps = {}) {
|
|
15
|
+
const { localRelay = handleLocalRelay, exhaustedAll = handleExhaustedAll } = deps;
|
|
16
|
+
const {
|
|
17
|
+
reqId, requested, body, hops, useAuto, lockModel, plugins,
|
|
18
|
+
auto, upstream, perf0, stages, mark, evt, logCall, logError,
|
|
19
|
+
startedAt, handlerCtx, res,
|
|
20
|
+
} = ctx;
|
|
21
|
+
let order = ctx.order;
|
|
22
|
+
const shareKeys = ctx.shareKeys ?? ctx.policy?.shareKeys ?? {};
|
|
23
|
+
const workbuddyUid = ctx.workbuddyUid ?? ctx.policy?.workbuddyUid ?? null;
|
|
24
|
+
|
|
25
|
+
if (!(useAuto && order.length > 1 && auto && !lockModel)) return { done: false, order };
|
|
26
|
+
const statuses = auto.statuses?.() ?? {};
|
|
27
|
+
const hasPriorSuccess = Object.values(statuses).some((e) => e && typeof e === "object" && e.status === "normal");
|
|
28
|
+
const nonCoolingOrder = order.filter((m) => { try { return !auto.isCooling(m); } catch { return true; } });
|
|
29
|
+
if (hasPriorSuccess || nonCoolingOrder.length <= 1) return { done: false, order };
|
|
30
|
+
|
|
31
|
+
const concLimit = (() => {
|
|
32
|
+
const v = Number(process.env.MSLXDFF_AUTO_CONCURRENT);
|
|
33
|
+
if (Number.isInteger(v) && v > 0) return Math.min(v, nonCoolingOrder.length);
|
|
34
|
+
return Math.min(nonCoolingOrder.length, 5);
|
|
35
|
+
})();
|
|
36
|
+
let raceModels = nonCoolingOrder.slice(0, concLimit);
|
|
37
|
+
if (plugins?.length) {
|
|
38
|
+
const k = [];
|
|
39
|
+
for (const m of raceModels) {
|
|
40
|
+
const b = await runHook(plugins, "model:beforeTry", { reqId, requested, model: m, hops });
|
|
41
|
+
if (b.value === false || b.value?.skip) continue;
|
|
42
|
+
k.push(m);
|
|
43
|
+
}
|
|
44
|
+
raceModels = k;
|
|
45
|
+
}
|
|
46
|
+
if (!raceModels.length) {
|
|
47
|
+
order = order.filter((m) => !new Set(nonCoolingOrder.slice(0, concLimit)).has(m));
|
|
48
|
+
if (!order.length) {
|
|
49
|
+
await exhaustedAll({ res, body, lastErr: { model: requested, status: 502, message: "all concurrent candidates skipped by plugin" }, order: nonCoolingOrder.slice(0, concLimit), requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
|
|
50
|
+
return { done: true };
|
|
51
|
+
}
|
|
52
|
+
return { done: false, order };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
evt("auto-concurrent-race", { reqId, models: raceModels, skippedFaulty: order.length - nonCoolingOrder.length, limit: concLimit });
|
|
56
|
+
const raceStart = performance.now();
|
|
57
|
+
const attempts = raceModels.map(async (m) => {
|
|
58
|
+
let f = { ...injectReasoningContent(m, body), model: m };
|
|
59
|
+
if (plugins?.length) {
|
|
60
|
+
const u = await runHook(plugins, "upstream:request", { reqId, requested, model: m, payload: f, stream: Boolean(body.stream) });
|
|
61
|
+
if (u.changed && u.value?.payload) f = u.value.payload;
|
|
62
|
+
}
|
|
63
|
+
let r = null;
|
|
64
|
+
try {
|
|
65
|
+
const o = {};
|
|
66
|
+
if (Object.keys(shareKeys).length) o.shareKeys = shareKeys;
|
|
67
|
+
if (workbuddyUid) o.workbuddyUid = workbuddyUid;
|
|
68
|
+
r = await upstream.chat(f, Object.keys(o).length ? o : undefined);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
if (plugins?.length) runHook(plugins, "upstream:response", { reqId, requested, model: m, status: null, ok: false, error: errMsg(e), timing: e?._t ?? null }).catch(() => {});
|
|
71
|
+
return { model: m, ok: false, error: errMsg(e), status: 502, timing: e?._t ?? null };
|
|
72
|
+
}
|
|
73
|
+
if (plugins?.length) runHook(plugins, "upstream:response", { reqId, requested, model: m, status: r instanceof Error ? null : r?.status ?? null, ok: !(r instanceof Error) && r ? r.status < 400 : false, error: r instanceof Error ? errMsg(r) : null, timing: r?._t ?? null }).catch(() => {});
|
|
74
|
+
if (r && r.status >= 400) {
|
|
75
|
+
const a = r.status === 403 && r.headers?.get?.("x-mslxdff-allowlist") === "1";
|
|
76
|
+
if (a) return { model: m, ok: false, error: "allowlist", status: 403, allowlist: true };
|
|
77
|
+
return { model: m, ok: false, error: `upstream ${r.status}`, status: r.status, res: r, timing: r._t ?? null };
|
|
78
|
+
}
|
|
79
|
+
if (r instanceof Error) return { model: m, ok: false, error: errMsg(r), status: 502 };
|
|
80
|
+
return { model: m, ok: true, res: r, status: r.status, timing: r._t ?? null };
|
|
81
|
+
});
|
|
82
|
+
const results = await Promise.allSettled(attempts);
|
|
83
|
+
const okList = results.map((r, i) => ({ r, i, model: raceModels[i] }))
|
|
84
|
+
.filter(({ r }) => r.status === "fulfilled" && r.value?.ok)
|
|
85
|
+
.map(({ r, i, model }) => ({ model, idx: i, val: r.value, t: r.value.timing?.totalMs ?? r.value.timing?.ms ?? Number.MAX_SAFE_INTEGER }));
|
|
86
|
+
if (okList.length) {
|
|
87
|
+
okList.sort((a, b) => a.t - b.t);
|
|
88
|
+
const best = okList[0];
|
|
89
|
+
const winModel = best.model;
|
|
90
|
+
evt("auto-concurrent-win", { reqId, model: winModel, timing: best.val.timing, totalMs: Math.round(performance.now() - raceStart), tried: raceModels.length });
|
|
91
|
+
for (const { r, i } of results.map((r, i) => ({ r, i }))) {
|
|
92
|
+
const m = raceModels[i];
|
|
93
|
+
if (r.status === "fulfilled" && r.value?.ok) {
|
|
94
|
+
if (m === winModel) {
|
|
95
|
+
const latencyMs = r.value.timing?.totalMs ?? Math.round(performance.now() - raceStart);
|
|
96
|
+
await auto.recordOk(m, { latencyMs });
|
|
97
|
+
try { const { savePreferredModel } = await import("../state.js"); savePreferredModel(m); evt("auto-concurrent-preferred", { reqId, model: m }); } catch {}
|
|
98
|
+
}
|
|
99
|
+
} else if (r.status === "fulfilled" && !r.value?.ok && !r.value?.allowlist) await auto.recordError(m, { status: r.value.status || 502 });
|
|
100
|
+
else if (r.status === "rejected") await auto.recordError(m, { status: 502 });
|
|
101
|
+
}
|
|
102
|
+
handlerCtx.model = winModel;
|
|
103
|
+
const lr = await localRelay({ upRes: best.val.res, model: winModel, body, order: raceModels, idx: best.idx, lastErr: null, requested, useAuto, lockModel, auto, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res });
|
|
104
|
+
if (lr.handled) return { done: true };
|
|
105
|
+
if (!lr.lastErr) return { done: true };
|
|
106
|
+
} else {
|
|
107
|
+
evt("auto-concurrent-all-fail", { reqId, tried: raceModels.length, totalMs: Math.round(performance.now() - raceStart) });
|
|
108
|
+
for (const { r, i } of results.map((r, i) => ({ r, i }))) {
|
|
109
|
+
const m = raceModels[i];
|
|
110
|
+
if (r.status === "fulfilled" && !r.value?.ok && !r.value?.allowlist) await auto.recordError(m, { status: r.value.status || 502 });
|
|
111
|
+
else if (r.status === "rejected") await auto.recordError(m, { status: 502 });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const triedSet = new Set(raceModels);
|
|
115
|
+
order = order.filter((m) => !triedSet.has(m));
|
|
116
|
+
if (!order.length) {
|
|
117
|
+
const failedStatuses = results.map((r) => (r.status === "fulfilled" ? r.value?.status : null)).filter((s) => Number.isInteger(s));
|
|
118
|
+
const lastStatus = failedStatuses[failedStatuses.length - 1] || failedStatuses[0] || 502;
|
|
119
|
+
const last = { model: raceModels[0] || requested, status: lastStatus, message: "all concurrent candidates failed" };
|
|
120
|
+
await exhaustedAll({ res, body, lastErr: last, order: raceModels, requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
|
|
121
|
+
return { done: true };
|
|
122
|
+
}
|
|
123
|
+
return { done: false, order };
|
|
124
|
+
}
|
|
@@ -1,241 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { runHook } from "../plugins.js";
|
|
4
|
-
import { errMsg, json } from "../routes/helpers.js";
|
|
5
|
-
import { hedgeDelayMs, shouldHedge } from "../routes/hedge.js";
|
|
6
|
-
import { handleHedge } from "../routes/chat/hedge-handler.js";
|
|
7
|
-
import { handleLocalRelay } from "../routes/chat/local-handler.js";
|
|
8
|
-
import { handlePeerRelay } from "../routes/chat/peer-handler.js";
|
|
9
|
-
import { handleBroadbandRelay } from "../routes/chat/broadband-handler.js";
|
|
10
|
-
import { handleViaRoute } from "../routes/chat/via-route-handler.js";
|
|
11
|
-
import { handleExhaustedLocal, handleExhaustedAll } from "../routes/chat/exhausted-handler.js";
|
|
1
|
+
import { runAutoRace } from "./auto-race.js";
|
|
2
|
+
import { runSerialTrial } from "./serial-trial.js";
|
|
12
3
|
|
|
13
4
|
/**
|
|
14
|
-
* ExecutionEngine
|
|
15
|
-
*
|
|
5
|
+
* ExecutionEngine(薄编排)— auto 并发择优 → 串行 trial。
|
|
6
|
+
* 重活下沉 auto-race.js / serial-trial.js,本文件仅做顺序编排。
|
|
16
7
|
*/
|
|
17
|
-
export function createEngine(
|
|
8
|
+
export function createEngine(deps = {}) {
|
|
9
|
+
const { raceDeps, serialDeps } = deps;
|
|
18
10
|
async function run(_plan, state) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
useAuto, lockModel, requested, hops,
|
|
23
|
-
canFallback, canForwardPeers,
|
|
24
|
-
perf0, stages, mark, evt, logCall, logError, done, handlerCtx,
|
|
25
|
-
auto, upstream, peers, groups, bus, token, plugins, logs,
|
|
26
|
-
} = state;
|
|
27
|
-
const { shareKeys, workbuddyUid } = policy;
|
|
28
|
-
|
|
29
|
-
// ===== auto 首次并发择优 =====
|
|
30
|
-
if (useAuto && order.length > 1 && auto && !lockModel) {
|
|
31
|
-
const statuses = auto.statuses?.() ?? {};
|
|
32
|
-
const hasPriorSuccess = Object.values(statuses).some((e) => e && typeof e === "object" && e.status === "normal");
|
|
33
|
-
const nonCoolingOrder = order.filter((m) => { try { return !auto.isCooling(m); } catch { return true; } });
|
|
34
|
-
if (!hasPriorSuccess && nonCoolingOrder.length > 1) {
|
|
35
|
-
const concLimit = (() => {
|
|
36
|
-
const v = Number(process.env.MSLXDFF_AUTO_CONCURRENT);
|
|
37
|
-
if (Number.isInteger(v) && v > 0) return Math.min(v, nonCoolingOrder.length);
|
|
38
|
-
return Math.min(nonCoolingOrder.length, 5);
|
|
39
|
-
})();
|
|
40
|
-
let raceModels = nonCoolingOrder.slice(0, concLimit);
|
|
41
|
-
if (plugins?.length) {
|
|
42
|
-
const k = [];
|
|
43
|
-
for (const m of raceModels) {
|
|
44
|
-
const b = await runHook(plugins, "model:beforeTry", { reqId, requested, model: m, hops });
|
|
45
|
-
if (b.value === false || b.value?.skip) continue;
|
|
46
|
-
k.push(m);
|
|
47
|
-
}
|
|
48
|
-
raceModels = k;
|
|
49
|
-
}
|
|
50
|
-
if (!raceModels.length) {
|
|
51
|
-
order = order.filter((m) => !new Set(nonCoolingOrder.slice(0, concLimit)).has(m));
|
|
52
|
-
if (!order.length) {
|
|
53
|
-
await handleExhaustedAll({ res, body, lastErr: { model: requested, status: 502, message: "all concurrent candidates skipped by plugin" }, order: nonCoolingOrder.slice(0, concLimit), requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
} else {
|
|
57
|
-
evt("auto-concurrent-race", { reqId, models: raceModels, skippedFaulty: order.length - nonCoolingOrder.length, limit: concLimit });
|
|
58
|
-
const raceStart = performance.now();
|
|
59
|
-
const attempts = raceModels.map(async (m) => {
|
|
60
|
-
let f = { ...injectReasoningContent(m, body), model: m };
|
|
61
|
-
if (plugins?.length) {
|
|
62
|
-
const u = await runHook(plugins, "upstream:request", { reqId, requested, model: m, payload: f, stream: Boolean(body.stream) });
|
|
63
|
-
if (u.changed && u.value?.payload) f = u.value.payload;
|
|
64
|
-
}
|
|
65
|
-
let r = null;
|
|
66
|
-
try {
|
|
67
|
-
const o = {};
|
|
68
|
-
if (Object.keys(shareKeys).length) o.shareKeys = shareKeys;
|
|
69
|
-
if (workbuddyUid) o.workbuddyUid = workbuddyUid;
|
|
70
|
-
r = await upstream.chat(f, Object.keys(o).length ? o : undefined);
|
|
71
|
-
} catch (e) {
|
|
72
|
-
if (plugins?.length) runHook(plugins, "upstream:response", { reqId, requested, model: m, status: null, ok: false, error: errMsg(e), timing: e?._t ?? null }).catch(() => {});
|
|
73
|
-
return { model: m, ok: false, error: errMsg(e), status: 502, timing: e?._t ?? null };
|
|
74
|
-
}
|
|
75
|
-
if (plugins?.length) runHook(plugins, "upstream:response", { reqId, requested, model: m, status: r instanceof Error ? null : r?.status ?? null, ok: !(r instanceof Error) && r ? r.status < 400 : false, error: r instanceof Error ? errMsg(r) : null, timing: r?._t ?? null }).catch(() => {});
|
|
76
|
-
if (r && r.status >= 400) {
|
|
77
|
-
const a = r.status === 403 && r.headers?.get?.("x-mslxdff-allowlist") === "1";
|
|
78
|
-
if (a) return { model: m, ok: false, error: "allowlist", status: 403, allowlist: true };
|
|
79
|
-
return { model: m, ok: false, error: `upstream ${r.status}`, status: r.status, res: r, timing: r._t ?? null };
|
|
80
|
-
}
|
|
81
|
-
if (r instanceof Error) return { model: m, ok: false, error: errMsg(r), status: 502 };
|
|
82
|
-
return { model: m, ok: true, res: r, status: r.status, timing: r._t ?? null };
|
|
83
|
-
});
|
|
84
|
-
const results = await Promise.allSettled(attempts);
|
|
85
|
-
const okList = results.map((r, i) => ({ r, i, model: raceModels[i] }))
|
|
86
|
-
.filter(({ r }) => r.status === "fulfilled" && r.value?.ok)
|
|
87
|
-
.map(({ r, i, model }) => ({ model, idx: i, val: r.value, t: r.value.timing?.totalMs ?? r.value.timing?.ms ?? Number.MAX_SAFE_INTEGER }));
|
|
88
|
-
if (okList.length) {
|
|
89
|
-
okList.sort((a, b) => a.t - b.t);
|
|
90
|
-
const best = okList[0];
|
|
91
|
-
const winModel = best.model;
|
|
92
|
-
evt("auto-concurrent-win", { reqId, model: winModel, timing: best.val.timing, totalMs: Math.round(performance.now() - raceStart), tried: raceModels.length });
|
|
93
|
-
for (const { r, i } of results.map((r, i) => ({ r, i }))) {
|
|
94
|
-
const m = raceModels[i];
|
|
95
|
-
if (r.status === "fulfilled" && r.value?.ok) {
|
|
96
|
-
if (m === winModel) {
|
|
97
|
-
const latencyMs = r.value.timing?.totalMs ?? Math.round(performance.now() - raceStart);
|
|
98
|
-
await auto.recordOk(m, { latencyMs });
|
|
99
|
-
try { const { savePreferredModel } = await import("../state.js"); savePreferredModel(m); evt("auto-concurrent-preferred", { reqId, model: m }); } catch {}
|
|
100
|
-
}
|
|
101
|
-
} else if (r.status === "fulfilled" && !r.value?.ok && !r.value?.allowlist) await auto.recordError(m, { status: r.value.status || 502 });
|
|
102
|
-
else if (r.status === "rejected") await auto.recordError(m, { status: 502 });
|
|
103
|
-
}
|
|
104
|
-
handlerCtx.model = winModel;
|
|
105
|
-
const lr = await handleLocalRelay({ upRes: best.val.res, model: winModel, body, order: raceModels, idx: best.idx, lastErr: null, requested, useAuto, lockModel, auto, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res });
|
|
106
|
-
if (lr.handled) return;
|
|
107
|
-
if (!lr.lastErr) return;
|
|
108
|
-
} else {
|
|
109
|
-
evt("auto-concurrent-all-fail", { reqId, tried: raceModels.length, totalMs: Math.round(performance.now() - raceStart) });
|
|
110
|
-
for (const { r, i } of results.map((r, i) => ({ r, i }))) {
|
|
111
|
-
const m = raceModels[i];
|
|
112
|
-
if (r.status === "fulfilled" && !r.value?.ok && !r.value?.allowlist) await auto.recordError(m, { status: r.value.status || 502 });
|
|
113
|
-
else if (r.status === "rejected") await auto.recordError(m, { status: 502 });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const triedSet = new Set(raceModels);
|
|
117
|
-
order = order.filter((m) => !triedSet.has(m));
|
|
118
|
-
if (!order.length) {
|
|
119
|
-
const failedStatuses = results.map((r) => (r.status === "fulfilled" ? r.value?.status : null)).filter((s) => Number.isInteger(s));
|
|
120
|
-
const lastStatus = failedStatuses[failedStatuses.length - 1] || failedStatuses[0] || 502;
|
|
121
|
-
const last = { model: raceModels[0] || requested, status: lastStatus, message: "all concurrent candidates failed" };
|
|
122
|
-
await handleExhaustedAll({ res, body, lastErr: last, order: raceModels, requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ===== VIA-ROUTE 单路径择路(显式锁模型,不并发) =====
|
|
130
|
-
let viaRouteLastErr = null;
|
|
131
|
-
if (!useAuto && requested && requested.includes("/") && canForwardPeers && !lockModel && peers) {
|
|
132
|
-
try {
|
|
133
|
-
const vr = await handleViaRoute({ model: requested, body, peers, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res, requested, useAuto, lockModel, auto });
|
|
134
|
-
if (vr.handled) return;
|
|
135
|
-
if (vr.lastErr) viaRouteLastErr = vr.lastErr;
|
|
136
|
-
} catch (e) {
|
|
137
|
-
evt("via-route-exception", { reqId, model: requested, error: errMsg(e) });
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ===== 串行 trial =====
|
|
142
|
-
let lastErr = viaRouteLastErr;
|
|
143
|
-
for (let idx = 0; idx < order.length; idx++) {
|
|
144
|
-
const model = order[idx];
|
|
145
|
-
handlerCtx.model = model;
|
|
146
|
-
evt("model-try", { reqId, model, idx, remaining: order.length - idx });
|
|
147
|
-
if (plugins?.length) {
|
|
148
|
-
const bt = await runHook(plugins, "model:beforeTry", { reqId, requested, model, idx, hops });
|
|
149
|
-
for (const e of bt.errors) evt("plugin-hook-error", { reqId, hook: "model:beforeTry", plugin: e.plugin, error: e.error });
|
|
150
|
-
if (bt.value === false || bt.value?.skip === true) { evt("plugin-hook", { reqId, hook: "model:beforeTry", applied: true, skipped: model }); continue; }
|
|
151
|
-
}
|
|
152
|
-
let upRes = null;
|
|
153
|
-
let forwarded = { ...injectReasoningContent(model, body), model };
|
|
154
|
-
if (plugins?.length) {
|
|
155
|
-
const ur = await runHook(plugins, "upstream:request", { reqId, requested, model, payload: forwarded, stream: Boolean(body.stream) });
|
|
156
|
-
for (const e of ur.errors) evt("plugin-hook-error", { reqId, hook: "upstream:request", plugin: e.plugin, error: e.error });
|
|
157
|
-
if (ur.changed && ur.value?.payload && typeof ur.value.payload === "object") { forwarded = ur.value.payload; evt("plugin-hook", { reqId, hook: "upstream:request", applied: true, model, rewrittenModel: forwarded.model ?? null }); }
|
|
158
|
-
}
|
|
159
|
-
const tUp = performance.now();
|
|
160
|
-
evt("upstream-try", { reqId, model, attempt: idx + 1 });
|
|
161
|
-
try {
|
|
162
|
-
const chatOpts = {};
|
|
163
|
-
if (Object.keys(shareKeys).length) chatOpts.shareKeys = shareKeys;
|
|
164
|
-
if (workbuddyUid) chatOpts.workbuddyUid = workbuddyUid;
|
|
165
|
-
upRes = await upstream.chat(forwarded, Object.keys(chatOpts).length ? chatOpts : undefined);
|
|
166
|
-
evt("upstream-done", { reqId, model, ok: !(upRes instanceof Error) && upRes.status < 400, status: upRes instanceof Error ? null : upRes.status, timing: upRes._t ?? null, error: null });
|
|
167
|
-
} catch (err) {
|
|
168
|
-
if (auto) await auto.recordError(model, { message: errMsg(err) });
|
|
169
|
-
lastErr = { model, upstream: null, status: 502, message: errMsg(err) };
|
|
170
|
-
logError(model, 502, errMsg(err));
|
|
171
|
-
evt("upstream-error", { reqId, model, status: 502, message: errMsg(err), timing: err._t ?? { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - tUp) } });
|
|
172
|
-
}
|
|
173
|
-
if (plugins?.length) {
|
|
174
|
-
runHook(plugins, "upstream:response", {
|
|
175
|
-
reqId, requested, model,
|
|
176
|
-
status: upRes instanceof Error ? null : upRes instanceof Object ? (upRes.status ?? null) : null,
|
|
177
|
-
ok: !(upRes instanceof Error) && upRes ? upRes.status < 400 : false,
|
|
178
|
-
error: upRes instanceof Error ? errMsg(upRes) : null,
|
|
179
|
-
timing: upRes?._t ?? null,
|
|
180
|
-
}).catch(() => {});
|
|
181
|
-
}
|
|
182
|
-
mark(`up-${model}`);
|
|
183
|
-
if (upRes && upRes.status >= 400) {
|
|
184
|
-
const isAllowlistBlock = upRes.status === 403 && (upRes.headers?.get?.("x-mslxdff-allowlist") === "1");
|
|
185
|
-
if (isAllowlistBlock) {
|
|
186
|
-
let bodyText = null; try { bodyText = await upRes.clone().text(); } catch {}
|
|
187
|
-
let errBody = { error: `model not allowed for provider` };
|
|
188
|
-
try { errBody = bodyText ? JSON.parse(bodyText) : errBody; } catch { errBody = { error: bodyText || "model not allowed" }; }
|
|
189
|
-
if (useAuto) {
|
|
190
|
-
logError(model, 403, errBody.error || "model not allowed");
|
|
191
|
-
evt("upstream-error", { reqId, model, status: 403, message: errBody.error, timing: upRes._t ?? null, allowlist: true, skipped: true });
|
|
192
|
-
lastErr = { model, upstream: upRes, status: 403, message: errBody.error || "model not allowed" };
|
|
193
|
-
if (canFallback && idx < order.length - 1) { evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: `allowlist skip ${errBody.error || "blocked"}` }); continue; }
|
|
194
|
-
return json(res, 403, errBody);
|
|
195
|
-
}
|
|
196
|
-
logError(model, 403, errBody.error || "model not allowed");
|
|
197
|
-
evt("upstream-error", { reqId, model, status: 403, message: errBody.error, timing: upRes._t ?? null, allowlist: true });
|
|
198
|
-
return json(res, 403, errBody);
|
|
199
|
-
}
|
|
200
|
-
if (auto) await auto.recordError(model, { status: upRes.status });
|
|
201
|
-
lastErr = { model, upstream: upRes, status: upRes.status, message: null };
|
|
202
|
-
logError(model, upRes.status, `upstream ${upRes.status}`);
|
|
203
|
-
evt("upstream-error", { reqId, model, status: upRes.status, message: null, timing: upRes._t ?? null });
|
|
204
|
-
upRes = null;
|
|
205
|
-
}
|
|
206
|
-
if (upRes) {
|
|
207
|
-
const isStream = Boolean(body.stream);
|
|
208
|
-
const d = hedgeDelayMs();
|
|
209
|
-
const hasPeers = Boolean(peers) && peers.ordered().length > 0;
|
|
210
|
-
const doHedge = shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers }) && upRes.status === 200 && upRes.body;
|
|
211
|
-
if (doHedge) {
|
|
212
|
-
const hr = await handleHedge({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res, hedgeDelayMs: d });
|
|
213
|
-
if (hr.handled) return;
|
|
214
|
-
if (hr.lastErr) lastErr = hr.lastErr;
|
|
215
|
-
if (hr.upRes === null) upRes = null;
|
|
216
|
-
else if (hr.upRes) upRes = hr.upRes;
|
|
217
|
-
}
|
|
218
|
-
if (upRes) {
|
|
219
|
-
const lr = await handleLocalRelay({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res });
|
|
220
|
-
if (lr.handled) return;
|
|
221
|
-
if (lr.lastErr) { lastErr = lr.lastErr; continue; }
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (canForwardPeers) {
|
|
226
|
-
const pr = await handlePeerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, stages, startedAt, plugins, res });
|
|
227
|
-
if (pr.handled) return;
|
|
228
|
-
}
|
|
229
|
-
if (groups) {
|
|
230
|
-
const br = await handleBroadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, stages, res, startedAt, plugins });
|
|
231
|
-
if (br.handled) return;
|
|
232
|
-
}
|
|
233
|
-
if (canFallback) { evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: lastErr?.message || `upstream ${lastErr?.status ?? 502}` }); continue; }
|
|
234
|
-
await handleExhaustedLocal({ res, body, lastErr, order, handlerCtx: { ...handlerCtx, model, reqId }, evt, logCall, mark, perf0, stages, done, requested, useAuto });
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
await handleExhaustedAll({ res, body, lastErr, order, requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
|
|
11
|
+
const race = await runAutoRace(state, raceDeps);
|
|
12
|
+
if (race.done) return;
|
|
13
|
+
await runSerialTrial({ ...state, order: race.order }, serialDeps);
|
|
238
14
|
}
|
|
239
15
|
|
|
240
16
|
return { run };
|
|
241
|
-
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { injectReasoningContent } from "../reasoning.js";
|
|
3
|
+
import { runHook } from "../plugins.js";
|
|
4
|
+
import { errMsg, json } from "../routes/helpers.js";
|
|
5
|
+
import { hedgeDelayMs, shouldHedge } from "../routes/hedge.js";
|
|
6
|
+
import { handleHedge } from "../routes/chat/hedge-handler.js";
|
|
7
|
+
import { handleLocalRelay } from "../routes/chat/local-handler.js";
|
|
8
|
+
import { handlePeerRelay } from "../routes/chat/peer-handler.js";
|
|
9
|
+
import { handleBroadbandRelay } from "../routes/chat/broadband-handler.js";
|
|
10
|
+
import { handleViaRoute } from "../routes/chat/via-route-handler.js";
|
|
11
|
+
import { handleExhaustedLocal, handleExhaustedAll } from "../routes/chat/exhausted-handler.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 串行 trial — 从 engine.js 抽出的第二段:via-route 单路径 → 串行 trial →
|
|
15
|
+
* hedge/local/peer/broadband/exhausted。恒终结,返回 { done:true }。
|
|
16
|
+
*/
|
|
17
|
+
export async function runSerialTrial(ctx, deps = {}) {
|
|
18
|
+
const {
|
|
19
|
+
viaRoute = handleViaRoute,
|
|
20
|
+
hedge = handleHedge,
|
|
21
|
+
localRelay = handleLocalRelay,
|
|
22
|
+
peerRelay = handlePeerRelay,
|
|
23
|
+
broadbandRelay = handleBroadbandRelay,
|
|
24
|
+
exhaustedLocal = handleExhaustedLocal,
|
|
25
|
+
exhaustedAll = handleExhaustedAll,
|
|
26
|
+
} = deps;
|
|
27
|
+
const {
|
|
28
|
+
order, reqId, requested, body, hops, useAuto, lockModel, plugins,
|
|
29
|
+
auto, upstream, peers, groups, bus, token, canFallback, canForwardPeers,
|
|
30
|
+
perf0, stages, mark, evt, logCall, logError, done, handlerCtx,
|
|
31
|
+
res, startedAt, logs,
|
|
32
|
+
} = ctx;
|
|
33
|
+
const shareKeys = ctx.shareKeys ?? ctx.policy?.shareKeys ?? {};
|
|
34
|
+
const workbuddyUid = ctx.workbuddyUid ?? ctx.policy?.workbuddyUid ?? null;
|
|
35
|
+
|
|
36
|
+
let viaRouteLastErr = null;
|
|
37
|
+
if (!useAuto && requested && requested.includes("/") && canForwardPeers && !lockModel && peers) {
|
|
38
|
+
try {
|
|
39
|
+
const vr = await viaRoute({ model: requested, body, peers, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res, requested, useAuto, lockModel, auto });
|
|
40
|
+
if (vr.handled) return { done: true };
|
|
41
|
+
if (vr.lastErr) viaRouteLastErr = vr.lastErr;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
evt("via-route-exception", { reqId, model: requested, error: errMsg(e) });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let lastErr = viaRouteLastErr;
|
|
48
|
+
for (let idx = 0; idx < order.length; idx++) {
|
|
49
|
+
const model = order[idx];
|
|
50
|
+
handlerCtx.model = model;
|
|
51
|
+
evt("model-try", { reqId, model, idx, remaining: order.length - idx });
|
|
52
|
+
if (plugins?.length) {
|
|
53
|
+
const bt = await runHook(plugins, "model:beforeTry", { reqId, requested, model, idx, hops });
|
|
54
|
+
for (const e of bt.errors) evt("plugin-hook-error", { reqId, hook: "model:beforeTry", plugin: e.plugin, error: e.error });
|
|
55
|
+
if (bt.value === false || bt.value?.skip === true) { evt("plugin-hook", { reqId, hook: "model:beforeTry", applied: true, skipped: model }); continue; }
|
|
56
|
+
}
|
|
57
|
+
let upRes = null;
|
|
58
|
+
let forwarded = { ...injectReasoningContent(model, body), model };
|
|
59
|
+
if (plugins?.length) {
|
|
60
|
+
const ur = await runHook(plugins, "upstream:request", { reqId, requested, model, payload: forwarded, stream: Boolean(body.stream) });
|
|
61
|
+
for (const e of ur.errors) evt("plugin-hook-error", { reqId, hook: "upstream:request", plugin: e.plugin, error: e.error });
|
|
62
|
+
if (ur.changed && ur.value?.payload && typeof ur.value.payload === "object") { forwarded = ur.value.payload; evt("plugin-hook", { reqId, hook: "upstream:request", applied: true, model, rewrittenModel: forwarded.model ?? null }); }
|
|
63
|
+
}
|
|
64
|
+
const tUp = performance.now();
|
|
65
|
+
evt("upstream-try", { reqId, model, attempt: idx + 1 });
|
|
66
|
+
try {
|
|
67
|
+
const chatOpts = {};
|
|
68
|
+
if (Object.keys(shareKeys).length) chatOpts.shareKeys = shareKeys;
|
|
69
|
+
if (workbuddyUid) chatOpts.workbuddyUid = workbuddyUid;
|
|
70
|
+
upRes = await upstream.chat(forwarded, Object.keys(chatOpts).length ? chatOpts : undefined);
|
|
71
|
+
evt("upstream-done", { reqId, model, ok: !(upRes instanceof Error) && upRes.status < 400, status: upRes instanceof Error ? null : upRes.status, timing: upRes._t ?? null, error: null });
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (auto) await auto.recordError(model, { message: errMsg(err) });
|
|
74
|
+
lastErr = { model, upstream: null, status: 502, message: errMsg(err) };
|
|
75
|
+
logError(model, 502, errMsg(err));
|
|
76
|
+
evt("upstream-error", { reqId, model, status: 502, message: errMsg(err), timing: err._t ?? { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - tUp) } });
|
|
77
|
+
}
|
|
78
|
+
if (plugins?.length) {
|
|
79
|
+
runHook(plugins, "upstream:response", {
|
|
80
|
+
reqId, requested, model,
|
|
81
|
+
status: upRes instanceof Error ? null : upRes instanceof Object ? (upRes.status ?? null) : null,
|
|
82
|
+
ok: !(upRes instanceof Error) && upRes ? upRes.status < 400 : false,
|
|
83
|
+
error: upRes instanceof Error ? errMsg(upRes) : null,
|
|
84
|
+
timing: upRes?._t ?? null,
|
|
85
|
+
}).catch(() => {});
|
|
86
|
+
}
|
|
87
|
+
mark(`up-${model}`);
|
|
88
|
+
if (upRes && upRes.status >= 400) {
|
|
89
|
+
const isAllowlistBlock = upRes.status === 403 && (upRes.headers?.get?.("x-mslxdff-allowlist") === "1");
|
|
90
|
+
if (isAllowlistBlock) {
|
|
91
|
+
let bodyText = null; try { bodyText = await upRes.clone().text(); } catch {}
|
|
92
|
+
let errBody = { error: `model not allowed for provider` };
|
|
93
|
+
try { errBody = bodyText ? JSON.parse(bodyText) : errBody; } catch { errBody = { error: bodyText || "model not allowed" }; }
|
|
94
|
+
if (useAuto) {
|
|
95
|
+
logError(model, 403, errBody.error || "model not allowed");
|
|
96
|
+
evt("upstream-error", { reqId, model, status: 403, message: errBody.error, timing: upRes._t ?? null, allowlist: true, skipped: true });
|
|
97
|
+
lastErr = { model, upstream: upRes, status: 403, message: errBody.error || "model not allowed" };
|
|
98
|
+
if (canFallback && idx < order.length - 1) { evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: `allowlist skip ${errBody.error || "blocked"}` }); continue; }
|
|
99
|
+
return json(res, 403, errBody);
|
|
100
|
+
}
|
|
101
|
+
logError(model, 403, errBody.error || "model not allowed");
|
|
102
|
+
evt("upstream-error", { reqId, model, status: 403, message: errBody.error, timing: upRes._t ?? null, allowlist: true });
|
|
103
|
+
return json(res, 403, errBody);
|
|
104
|
+
}
|
|
105
|
+
if (auto) await auto.recordError(model, { status: upRes.status });
|
|
106
|
+
lastErr = { model, upstream: upRes, status: upRes.status, message: null };
|
|
107
|
+
logError(model, upRes.status, `upstream ${upRes.status}`);
|
|
108
|
+
evt("upstream-error", { reqId, model, status: upRes.status, message: null, timing: upRes._t ?? null });
|
|
109
|
+
upRes = null;
|
|
110
|
+
}
|
|
111
|
+
if (upRes) {
|
|
112
|
+
const isStream = Boolean(body.stream);
|
|
113
|
+
const d = hedgeDelayMs();
|
|
114
|
+
const hasPeers = Boolean(peers) && peers.ordered().length > 0;
|
|
115
|
+
const doHedge = shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers }) && upRes.status === 200 && upRes.body;
|
|
116
|
+
if (doHedge) {
|
|
117
|
+
const hr = await hedge({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res, hedgeDelayMs: d });
|
|
118
|
+
if (hr.handled) return { done: true };
|
|
119
|
+
if (hr.lastErr) lastErr = hr.lastErr;
|
|
120
|
+
if (hr.upRes === null) upRes = null;
|
|
121
|
+
else if (hr.upRes) upRes = hr.upRes;
|
|
122
|
+
}
|
|
123
|
+
if (upRes) {
|
|
124
|
+
const lr = await localRelay({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res });
|
|
125
|
+
if (lr.handled) return { done: true };
|
|
126
|
+
if (lr.lastErr) { lastErr = lr.lastErr; continue; }
|
|
127
|
+
return { done: true };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (canForwardPeers) {
|
|
131
|
+
const pr = await peerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, stages, startedAt, plugins, res });
|
|
132
|
+
if (pr.handled) return { done: true };
|
|
133
|
+
}
|
|
134
|
+
if (groups) {
|
|
135
|
+
const br = await broadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, stages, res, startedAt, plugins });
|
|
136
|
+
if (br.handled) return { done: true };
|
|
137
|
+
}
|
|
138
|
+
if (canFallback) { evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: lastErr?.message || `upstream ${lastErr?.status ?? 502}` }); continue; }
|
|
139
|
+
await exhaustedLocal({ res, body, lastErr, order, handlerCtx: { ...handlerCtx, model, reqId }, evt, logCall, mark, perf0, stages, done, requested, useAuto });
|
|
140
|
+
return { done: true };
|
|
141
|
+
}
|
|
142
|
+
await exhaustedAll({ res, body, lastErr, order, requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
|
|
143
|
+
return { done: true };
|
|
144
|
+
}
|