mslxdff 0.1.84 → 0.1.85

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.84",
3
+ "version": "0.1.85",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,87 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import os from "node:os";
4
+ import { defaultStateFile } from "../state/store.js";
5
+ import { atomicWriteSync } from "../state/persist.js";
6
+
7
+ export function defaultViaRoutesFile() {
8
+ if (process.env.MSLXDFF_VIA_ROUTES_FILE) return String(process.env.MSLXDFF_VIA_ROUTES_FILE).trim();
9
+ const sf = defaultStateFile();
10
+ return join(dirname(sf), "via-routes.json");
11
+ }
12
+
13
+ function viaTtlMs() {
14
+ const raw = process.env.MSLXDFF_VIA_ROUTE_TTL_MS;
15
+ if (raw === undefined || raw === null || raw === "") return 0;
16
+ const s = String(raw).trim().toLowerCase();
17
+ if (s === "0" || s === "off" || s === "false" || s === "no" || s === "disable" || s === "disabled") return 0;
18
+ const n = Number(s);
19
+ if (Number.isInteger(n) && n >= 0) return n;
20
+ return 0;
21
+ }
22
+
23
+ export function loadViaRoutes(file) {
24
+ const f = file || defaultViaRoutesFile();
25
+ try {
26
+ if (!existsSync(f)) return { version: 1, at: null, routes: {}, meta: {} };
27
+ const j = JSON.parse(readFileSync(f, "utf8"));
28
+ if (j && typeof j === "object" && j.routes && typeof j.routes === "object") return j;
29
+ if (j && typeof j === "object" && !j.routes) return { version: 1, at: j.at || null, routes: j, meta: {} };
30
+ return { version: 1, at: null, routes: {}, meta: {} };
31
+ } catch {
32
+ return { version: 1, at: null, routes: {}, meta: {} };
33
+ }
34
+ }
35
+
36
+ export function getViaRoute(model, { file, ttlMs } = {}) {
37
+ const id = String(model || "").trim();
38
+ if (!id) return null;
39
+ const f = file || defaultViaRoutesFile();
40
+ const data = loadViaRoutes(f);
41
+ const entry = data.routes?.[id];
42
+ if (!entry) return null;
43
+ const t = ttlMs !== undefined ? ttlMs : viaTtlMs();
44
+ if (t > 0 && entry.at) {
45
+ const atMs = Date.parse(entry.at);
46
+ if (Number.isFinite(atMs) && Date.now() - atMs > t) return null;
47
+ }
48
+ return entry;
49
+ }
50
+
51
+ export function saveViaRoutes(results, { file, meta } = {}) {
52
+ const f = file || defaultViaRoutesFile();
53
+ const now = new Date().toISOString();
54
+ const prev = loadViaRoutes(f);
55
+ const nextRoutes = { ...(prev.routes || {}) };
56
+ for (const r of results || []) {
57
+ const id = String(r.model || r.id || "").trim();
58
+ if (!id) continue;
59
+ const best = String(r.best || "direct").trim() || "direct";
60
+ const direct = r.direct ? { ok: Boolean(r.direct.ok), ttfbMs: r.direct.ttfbMs ?? r.direct.totalMs ?? null, totalMs: r.direct.totalMs ?? null, label: r.direct.label || null, error: r.direct.error ? String(r.direct.error).slice(0, 300) : null } : null;
61
+ const via = {};
62
+ for (const [k, v] of Object.entries(r.via || {})) {
63
+ via[k] = v?.ok ? { ok: true, ttfbMs: v.ttfbMs ?? v.totalMs ?? null, totalMs: v.totalMs ?? null } : { ok: false, ttfbMs: v?.ttfbMs ?? null, totalMs: v?.totalMs ?? null, label: v?.label || v?.error || "offline" };
64
+ }
65
+ nextRoutes[id] = {
66
+ best,
67
+ direct,
68
+ via,
69
+ deltaMs: r.deltaMs ?? null,
70
+ provider: r.provider || id.split("/")[0] || "",
71
+ at: now,
72
+ };
73
+ }
74
+ const out = {
75
+ version: 1,
76
+ at: now,
77
+ routes: nextRoutes,
78
+ meta: meta || prev.meta || {},
79
+ };
80
+ atomicWriteSync(f, out);
81
+ return out;
82
+ }
83
+
84
+ export function clearViaRoutes(file) {
85
+ const f = file || defaultViaRoutesFile();
86
+ atomicWriteSync(f, { version: 1, at: new Date().toISOString(), routes: {}, meta: {} });
87
+ }
@@ -0,0 +1,70 @@
1
+ import { computeMetrics } from "../metrics.js";
2
+
3
+ function buildWorkbuddyHeaders(apiKey, auth) {
4
+ const h = {
5
+ "Content-Type": "application/json",
6
+ Accept: "text/event-stream",
7
+ "User-Agent": "CLI/2.115.0 WorkBuddy/2.115.0",
8
+ Origin: "https://www.codebuddy.cn",
9
+ Referer: "https://www.codebuddy.cn/",
10
+ "X-Product": "SaaS",
11
+ };
12
+ if (apiKey) h["Authorization"] = `Bearer ${apiKey}`;
13
+ if (auth?.uid) h["X-User-Id"] = auth.uid;
14
+ h["X-Domain"] = auth?.domain || "www.codebuddy.cn";
15
+ if (auth?.enterpriseId) { h["X-Enterprise-Id"] = auth.enterpriseId; h["X-Tenant-Id"] = auth.enterpriseId; }
16
+ return h;
17
+ }
18
+
19
+ export async function workbuddyBenchOne({ baseUrl, chatPath = "/v2/chat/completions", model, apiKey, auth, prompt = "hi", maxTokens = 5, timeoutMs = 30000, fetchImpl = globalThis.fetch }) {
20
+ const controller = new AbortController();
21
+ const timer = setTimeout(() => controller.abort(new Error(`timeout ${timeoutMs}ms`)), timeoutMs);
22
+ const t0 = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
23
+ let ttfbMs = null;
24
+ let content = "";
25
+ try {
26
+ const url = String(baseUrl).replace(/\/+$/, "") + String(chatPath || "/v2/chat/completions");
27
+ const headers = buildWorkbuddyHeaders(apiKey, auth);
28
+ const rawModel = String(model || "").trim();
29
+ const body = { model: rawModel, stream: true, messages: [{ role: "user", content: prompt }], max_tokens: maxTokens };
30
+ const res = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal: controller.signal });
31
+ if (res instanceof Error) throw res;
32
+ if (!res.ok) {
33
+ let txt = "";
34
+ try { txt = await res.text(); } catch {}
35
+ const label = res.status === 401 ? "鉴权失败" : res.status === 402 ? "余额不足" : res.status === 429 ? "限流" : res.status >= 500 ? `上游错误 ${res.status}` : `HTTP ${res.status}`;
36
+ return { id: model, ok: false, status: res.status, label, error: txt.slice(0, 300), ttfbMs, totalMs: Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - t0), tps: null, charsPerSec: null, tokens: null };
37
+ }
38
+ // SSE stream parsing — 复用 cline 逻辑
39
+ const reader = res.body.getReader();
40
+ const decoder = new TextDecoder();
41
+ let buf = "";
42
+ for (;;) {
43
+ const { done, value } = await reader.read();
44
+ if (done) break;
45
+ if (ttfbMs === null) ttfbMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - t0);
46
+ buf += decoder.decode(value, { stream: true });
47
+ let idx;
48
+ while ((idx = buf.indexOf("\n")) >= 0) {
49
+ const line = buf.slice(0, idx);
50
+ buf = buf.slice(idx + 1);
51
+ if (!line.startsWith("data:")) continue;
52
+ const payload = line.slice(5).trim();
53
+ if (!payload || payload === "[DONE]") continue;
54
+ try {
55
+ const j = JSON.parse(payload);
56
+ const c = j?.choices?.[0]?.delta?.content || j?.choices?.[0]?.message?.content || "";
57
+ if (typeof c === "string") content += c;
58
+ } catch {}
59
+ }
60
+ }
61
+ const totalMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - t0);
62
+ const chars = content.length;
63
+ const { tps, charsPerSec } = computeMetrics({ ttfbMs, totalMs, promptTokens: null, completionTokens: null, chars });
64
+ return { id: model, ok: true, status: 200, label: "成功", ttfbMs, totalMs, tps, charsPerSec, tokens: null, chars };
65
+ } catch (e) {
66
+ const msg = e?.message || String(e);
67
+ const totalMs = Math.round((typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()) - t0);
68
+ return { id: model, ok: false, label: /timeout|abort/i.test(msg) ? "超时" : "网络错误", error: msg.slice(0, 300), ttfbMs, totalMs, tps: null, charsPerSec: null, tokens: null };
69
+ } finally { clearTimeout(timer); }
70
+ }
@@ -4,6 +4,7 @@ import { refreshTokenForBase } from "../../../providers/cline/auth.js";
4
4
  import { runOne } from "../../../bench/runner.js";
5
5
  import { formatViaReport } from "../../../bench/report.js";
6
6
  import { clineBenchOne } from "../../../bench/cline-bench.js";
7
+ import { workbuddyBenchOne } from "../../../bench/workbuddy-bench.js";
7
8
 
8
9
  export function buildHeadersForProvider(providerId, apiKey, auth) {
9
10
  const h = {};
@@ -93,6 +94,9 @@ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, load
93
94
  const aIdx = Math.min(kIdx, Math.max(auths.length - 1, 0));
94
95
  const key = keys[kIdx] || keys[0] || "";
95
96
  const auth = auths[aIdx] || auths[0] || null;
97
+ if (String(p).toLowerCase() === "workbuddy") {
98
+ return workbuddyBenchOne({ baseUrl, chatPath, model, apiKey: key, auth, prompt: "hi", maxTokens: 5, timeoutMs: opts.timeoutMs, fetchImpl });
99
+ }
96
100
  const cKeys = keys.filter((k) => isRefreshToken(k, p));
97
101
  if (cKeys.length) {
98
102
  const normBase = String(baseUrl).replace(/\/+$/, "");
@@ -114,6 +118,16 @@ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, load
114
118
  const aIdx = Math.min(kIdx, Math.max(auths.length - 1, 0));
115
119
  const key = keys[kIdx] || keys[0] || "";
116
120
  const auth = auths[aIdx] || auths[0] || null;
121
+ // workbuddy 强制 stream:true SSE 中继(与直连一致)
122
+ if (String(p).toLowerCase() === "workbuddy") {
123
+ const rawModel = String(model).startsWith(`${p}/`) ? String(model).slice(p.length + 1) : String(model);
124
+ const targetUrl = `${String(baseUrl).replace(/\/+$/, "")}${chatPath}`;
125
+ const headers = buildHeadersForProvider(p, key, auth);
126
+ const body = { model: rawModel, stream: true, messages: [{ role: "user", content: "hi" }], max_tokens: 5 };
127
+ const peer = peers.find((pe) => pe.url === peerUrl || (pe.name || pe.id) === peerUrl);
128
+ const peerToken = peer?.token || token;
129
+ return viaProbe({ peerUrl, token: peerToken, relayTarget: targetUrl, relayHeaders: headers, relayBody: body, timeoutMs: opts.timeoutMs, fetchImpl });
130
+ }
117
131
  const cKeys = keys.filter((k) => isRefreshToken(k, p));
118
132
  if (cKeys.length) {
119
133
  const normBase = String(baseUrl).replace(/\/+$/, "");
@@ -154,6 +168,15 @@ export async function handleVia({ providerId, opts, fetchImpl, loadConfigs, load
154
168
  }
155
169
  const meta = { at: new Date().toISOString(), samples: opts.samples, timeout: opts.timeoutMs, includeOpencode, peers: peers.map((p) => p.id), opencodeSkipped: !includeOpencode };
156
170
  const report = formatViaReport(allResults, { peers, meta, json: opts.json });
171
+ if (opts.apply) {
172
+ const { saveViaRoutes } = await import("../../../bench/via-routes.js");
173
+ const saved = saveViaRoutes(allResults, { meta });
174
+ const viaLog2 = (s) => (opts.json ? console.error(s) : console.log(s));
175
+ viaLog2(`\nvia-routes 已落盘: ${saved.at} 共 ${Object.keys(saved.routes).length} 条 → ${saved.routes[Object.keys(saved.routes)[0]] ? "" : ""}${(await import("../../../bench/via-routes.js")).defaultViaRoutesFile()}`);
176
+ for (const [m, e] of Object.entries(saved.routes)) {
177
+ if (allResults.some((r) => r.model === m)) viaLog2(` ${m} → ${e.best}${e.deltaMs ? ` (${e.deltaMs}ms)` : ""}`);
178
+ }
179
+ }
157
180
  if (opts.json) console.log(report.text);
158
181
  else console.log("\n" + report.text);
159
182
  process.exit(0);
@@ -5,14 +5,16 @@ import { defaultModelsPath, defaultChatPath } from "../../../state/provider-conf
5
5
  import { isRefreshToken } from "../../../providers/cline/headers.js";
6
6
  import { refreshTokenForBase } from "../../../providers/cline/auth.js";
7
7
  import { clineBenchOne } from "../../../bench/cline-bench.js";
8
+ import { workbuddyBenchOne } from "../../../bench/workbuddy-bench.js";
8
9
  import { buildHeadersForProvider, handleVia } from "./bench-via.js";
9
10
 
10
11
  function parseBenchArgs(rest) {
11
- const opts = { json: false, prompt: "hi", maxTokens: 32, timeoutMs: 30000, via: false, includeOpencode: false, samples: 1 };
12
+ const opts = { json: false, prompt: "hi", maxTokens: 32, timeoutMs: 30000, via: false, includeOpencode: false, samples: 1, apply: false };
12
13
  for (let i = 0; i < rest.length; i++) {
13
14
  const a = rest[i];
14
15
  if (a === "--json" || a === "-json") opts.json = true;
15
16
  else if (a === "--via" || a === "--bench-via") opts.via = true;
17
+ else if (a === "--apply" || a === "--write" || a === "--save") opts.apply = true;
16
18
  else if (a === "--include-opencode") opts.includeOpencode = true;
17
19
  else if (a === "--samples" && rest[i + 1]) opts.samples = Number(rest[++i]) || 1;
18
20
  else if (a.startsWith("--samples=")) opts.samples = Number(a.split("=")[1]) || 1;
@@ -95,6 +97,12 @@ export async function handleProviderBench(id, sub, rest, args, deps = {}) {
95
97
  const aIdx = Math.min(kIdx, Math.max(auths.length - 1, 0));
96
98
  const key = keys[kIdx];
97
99
  const auth = auths[aIdx] || auths[0] || null;
100
+ if (String(providerId).toLowerCase() === "workbuddy") {
101
+ const r = await workbuddyBenchOne({ baseUrl, chatPath, model, apiKey: key, auth, prompt: opts.prompt, maxTokens: opts.maxTokens, timeoutMs: opts.timeoutMs, fetchImpl });
102
+ results.push(r);
103
+ if (!opts.json) { if (r.ok) console.log(`OK TTFB ${r.ttfbMs}ms 总 ${r.totalMs}ms ${r.tps != null ? `${r.tps} t/s` : r.charsPerSec != null ? `${r.charsPerSec} 字/秒` : "—"}`); else console.log(`FAIL ${r.label} ${r.error ? `(${r.error.slice(0, 60)})` : ""}`); }
104
+ continue;
105
+ }
98
106
  if (rtKeys.length) {
99
107
  const rt = rtKeys[kIdx % rtKeys.length];
100
108
  const at = await refreshTokenForBase({ refreshToken: rt, baseUrl: normBase, fetchImpl });
@@ -10,6 +10,7 @@ import { handleHedge } from "./hedge-handler.js";
10
10
  import { handleLocalRelay } from "./local-handler.js";
11
11
  import { handlePeerRelay } from "./peer-handler.js";
12
12
  import { handleBroadbandRelay } from "./broadband-handler.js";
13
+ import { handleViaRoute } from "./via-route-handler.js";
13
14
  import { handleExhaustedLocal, handleExhaustedAll } from "./exhausted-handler.js";
14
15
  import { normalizeFullId, getModelAlias } from "../../providers/model-id.js";
15
16
 
@@ -111,7 +112,7 @@ export function createChatGateway({ upstream, auto, logs, peers, maxHops, groups
111
112
  for (const e of sel.errors) evt("plugin-hook-error", { reqId, hook: "model:select", plugin: e.plugin, error: e.error });
112
113
  }
113
114
 
114
- const handlerCtx = { reqId, model: null, body, hops, peers, plugins, evt, logError, logCall, logs };
115
+ const handlerCtx = { reqId, model: null, body, hops, peers, plugins, evt, logError, logCall, logs, workbuddyUid };
115
116
 
116
117
  // ===== Selector: 首次 auto 并发择优 =====
117
118
  if (useAuto && order.length > 1 && auto && !lockModel) {
@@ -176,8 +177,38 @@ export function createChatGateway({ upstream, auto, logs, peers, maxHops, groups
176
177
  }
177
178
  }
178
179
 
180
+ // ===== VIA-ROUTE 单路径择路(显式锁模型,不并发) =====
181
+ let viaRouteLastErr = null;
182
+ if (!useAuto && requested && requested.includes("/") && canForwardPeers && !lockModel && peers) {
183
+ try {
184
+ const vr = await handleViaRoute({
185
+ model: requested,
186
+ body,
187
+ peers,
188
+ handlerCtx,
189
+ evt,
190
+ logCall,
191
+ logError,
192
+ mark,
193
+ perf0,
194
+ stages,
195
+ startedAt,
196
+ plugins,
197
+ res,
198
+ requested,
199
+ useAuto,
200
+ lockModel,
201
+ auto,
202
+ });
203
+ if (vr.handled) return;
204
+ if (vr.lastErr) viaRouteLastErr = vr.lastErr;
205
+ } catch (e) {
206
+ evt("via-route-exception", { reqId, model: requested, error: errMsg(e) });
207
+ }
208
+ }
209
+
179
210
  // ===== Executor: 串行 trial =====
180
- let lastErr = null;
211
+ let lastErr = viaRouteLastErr;
181
212
  for (let idx = 0; idx < order.length; idx++) {
182
213
  const model = order[idx];
183
214
  handlerCtx.model = model;
@@ -0,0 +1,144 @@
1
+ import { createRelayPipeline } from "./relay-pipeline.js";
2
+ import { relay, SLOW_TOTAL_MS, STREAM_TIMEOUT_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS } from "../stream.js";
3
+ import { buildFallbackInfo } from "../fallback.js";
4
+ import { getViaRoute } from "../../bench/via-routes.js";
5
+ import { loadProviderKeys } from "../../state.js";
6
+ import { SHARE_KEYS_HEADER } from "../../providers/share-keys.js";
7
+ import { errMsg } from "../helpers.js";
8
+
9
+ function shortLabel(p) {
10
+ const raw = String(p?.name || p?.id || p?.url || "").trim();
11
+ if (!raw) return "";
12
+ if (raw.includes("://")) {
13
+ try { const u = new URL(raw); return `${u.hostname}${u.port ? `:${u.port}` : ""}`; } catch { return raw.slice(-16); }
14
+ }
15
+ return raw;
16
+ }
17
+
18
+ export async function handleViaRoute({
19
+ model,
20
+ body,
21
+ peers,
22
+ handlerCtx,
23
+ evt,
24
+ logCall,
25
+ logError,
26
+ mark,
27
+ perf0,
28
+ stages,
29
+ startedAt,
30
+ plugins,
31
+ res,
32
+ requested,
33
+ useAuto,
34
+ lockModel,
35
+ auto,
36
+ }) {
37
+ const route = getViaRoute(model);
38
+ if (!route || !route.best || route.best === "direct" || !String(route.best).startsWith("via:")) return { handled: false };
39
+ const peerLabel = String(route.best).slice(4).trim();
40
+ if (!peerLabel) return { handled: false };
41
+ // 找到对应该 label 的 peer(仅走 best 单路径,不并发)
42
+ const ordered = (() => { try { return peers.ordered(); } catch { return []; } })();
43
+ const byErr = (() => { try { return peers.orderedByLastError(); } catch { return []; } })();
44
+ const all = [...ordered, ...byErr];
45
+ const uniq = [];
46
+ const seen = new Set();
47
+ for (const p of all) { const k = p.url; if (!seen.has(k)) { seen.add(k); uniq.push(p); } }
48
+ let peer = uniq.find((p) => shortLabel(p) === peerLabel || String(p.url || "").includes(peerLabel) || String(p.id || "") === peerLabel || String(p.name || "") === peerLabel);
49
+ if (!peer) {
50
+ try {
51
+ const { loadPeers } = await import("../../state.js");
52
+ const disk = loadPeers() || [];
53
+ peer = disk.find((p) => shortLabel(p) === peerLabel || String(p.url || "").includes(peerLabel));
54
+ } catch {}
55
+ }
56
+ if (!peer) {
57
+ evt("via-route-miss", { reqId: handlerCtx.reqId, model, peerLabel, reason: "peer not found" });
58
+ return { handled: false };
59
+ }
60
+ evt("via-route-hit", { reqId: handlerCtx.reqId, model, peer: peer.url, peerLabel, routeBest: route.best, at: route.at });
61
+ // 单路径转发,不并发:直接打 best peer
62
+ const hops = handlerCtx.hops || 0;
63
+ const providerId = String(model).split("/")[0] || "";
64
+ let shareHeader = null;
65
+ try {
66
+ const keys = loadProviderKeys(providerId) || [];
67
+ if (keys.length) shareHeader = `${providerId}=${keys.join(",")}`;
68
+ } catch {}
69
+ const controller = new AbortController();
70
+ const timer = setTimeout(() => controller.abort(new Error("via-route timeout 30000ms")), 30000);
71
+ let upRes;
72
+ try {
73
+ const headers = {
74
+ "Content-Type": "application/json",
75
+ "Authorization": `Bearer ${peer.token || ""}`,
76
+ "x-mslxdff-hops": String(hops + 1),
77
+ "x-mslxdff-model-lock": model,
78
+ "Accept": "text/event-stream",
79
+ };
80
+ if (shareHeader) headers[SHARE_KEYS_HEADER] = shareHeader;
81
+ // workbuddyUid 透传
82
+ if (handlerCtx.workbuddyUid) headers["x-mslxdff-workbuddy-uid"] = handlerCtx.workbuddyUid;
83
+ evt("via-route-request", { reqId: handlerCtx.reqId, peer: peer.url, model, hops: hops + 1, hasShare: Boolean(shareHeader) });
84
+ upRes = await fetch(`${String(peer.url).replace(/\/+$/, "")}/v1/chat/completions`, {
85
+ method: "POST",
86
+ headers,
87
+ body: JSON.stringify({ ...body, model }),
88
+ signal: controller.signal,
89
+ });
90
+ } catch (e) {
91
+ clearTimeout(timer);
92
+ const msg = errMsg(e);
93
+ evt("via-route-error", { reqId: handlerCtx.reqId, peer: peer.url, model, error: msg });
94
+ try { await peers.recordError(peer.url); } catch {}
95
+ try { await peers.recordResult(peer.url, { ok: false }); } catch {}
96
+ return { handled: false, lastErr: { model, status: 502, message: msg } };
97
+ }
98
+ clearTimeout(timer);
99
+ const failed = upRes instanceof Error || upRes.status >= 400;
100
+ if (failed) {
101
+ const status = upRes instanceof Error ? 502 : upRes.status;
102
+ let bodyText = "";
103
+ try { bodyText = await upRes.clone().text(); } catch {}
104
+ const msg = bodyText.slice(0, 300) || errMsg(upRes) || `peer ${status}`;
105
+ evt("via-route-peer-error", { reqId: handlerCtx.reqId, peer: peer.url, model, status, message: msg.slice(0, 200) });
106
+ try { await peers.recordError(peer.url); } catch {}
107
+ try { await peers.recordResult(peer.url, { ok: false }); } catch {}
108
+ // 502/429 等可 fallback 到 direct
109
+ return { handled: false, lastErr: { model, upstream: upRes, status, message: msg } };
110
+ }
111
+ // 成功:记录并走 pipeline 中继(复用 peer 的 streaming 逻辑)
112
+ try { await peers.recordResult(peer.url, { ok: true, latencyMs: 0, model }); } catch {}
113
+ evt("via-route-win", { reqId: handlerCtx.reqId, peer: peer.url, model });
114
+ const pipeline = createRelayPipeline({
115
+ relay,
116
+ buildFallbackInfo,
117
+ auto,
118
+ plugins,
119
+ evt,
120
+ mark,
121
+ logCall,
122
+ logError: logError || (() => {}),
123
+ constants: { STREAM_TIMEOUT_MS, SLOW_TOTAL_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS },
124
+ startedAt,
125
+ stages,
126
+ });
127
+ await pipeline.execute({
128
+ res,
129
+ upRes,
130
+ body,
131
+ requested: requested ?? model,
132
+ actual: model,
133
+ lastErr: null,
134
+ via: "peer",
135
+ lockModel: lockModel || model,
136
+ useAuto: Boolean(useAuto),
137
+ handlerCtx: { ...handlerCtx, model },
138
+ mark,
139
+ perf0,
140
+ stages,
141
+ startedAt,
142
+ });
143
+ return { handled: true };
144
+ }