mslxdff 0.1.43 → 0.1.54

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.
@@ -0,0 +1,142 @@
1
+ import { buildFallbackInfo } from "../fallback.js";
2
+ import { relay, SLOW_TOTAL_MS, STREAM_TIMEOUT_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS } from "../stream.js";
3
+ import { hedgedFirstChunkRace } from "../hedge.js";
4
+ import { runHook } from "../../plugins.js";
5
+
6
+ export async function handleHedge({
7
+ upRes,
8
+ model,
9
+ body,
10
+ order,
11
+ idx,
12
+ lastErr,
13
+ requested,
14
+ useAuto,
15
+ lockModel,
16
+ auto,
17
+ peers,
18
+ handlerCtx,
19
+ evt,
20
+ logCall,
21
+ logError,
22
+ mark,
23
+ perf0,
24
+ stages,
25
+ startedAt,
26
+ plugins,
27
+ res,
28
+ hedgeDelayMs,
29
+ }) {
30
+ const isStream = Boolean(body.stream);
31
+ const d = hedgeDelayMs;
32
+ // hedge 已在外层判断 doHedge,这里直接执行赛跑
33
+ try {
34
+ const hedged = await hedgedFirstChunkRace({ localUpRes: upRes, peers, handlerCtx, hedgeDelayMs: d, evt });
35
+ if (hedged && hedged.winner) {
36
+ if (hedged.winner === "local") {
37
+ const fallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "local", useAuto, lockModel });
38
+ if (fallback?.fallback) evt("fallback-notice", { reqId: handlerCtx.reqId, requested, actual: model, reason: fallback.reason, notice: fallback.notice, via: "local" });
39
+ evt("relay-start", { reqId: handlerCtx.reqId, model, via: "local", isStream, fallback, hedged: true });
40
+ const bufferedUpRes = { ...upRes, body: hedged.bufferedBody, headers: upRes.headers, status: upRes.status, _t: upRes._t };
41
+ logCall(model, bufferedUpRes.status);
42
+ const out = await relay(res, bufferedUpRes, body, {
43
+ fallback,
44
+ onFirstChunk: (delta) => {
45
+ mark(`ttf-${model}`);
46
+ evt("relay-first-chunk", { reqId: handlerCtx.reqId, model, ttfMs: delta, hedged: true, via: "local" });
47
+ if (plugins?.length) runHook(plugins, "relay:first-chunk", { reqId: handlerCtx.reqId, requested, model, via: "local", ttfMs: delta }).catch(() => {});
48
+ },
49
+ onDownstreamAbort: () => {
50
+ evt("client-abort", { reqId: handlerCtx.reqId, model, totalMs: Math.round(perf0 ? (Date.now() - startedAt) : 0), stages: [...stages] });
51
+ },
52
+ });
53
+ evt("relay-done", { reqId: handlerCtx.reqId, model, via: "local", status: out.status, ttfMs: out.ttfMs ?? hedged.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null, hedged: true });
54
+ if (out.status === STREAM_TIMEOUT_MS) {
55
+ if (auto) await auto.recordError(model, { status: 502, slow: true, note: `stream timeout ${STREAM_TIMEOUT_MS}ms` });
56
+ const err = { model, upstream: null, status: 502, message: `stream timed out after ${STREAM_TIMEOUT_MS}ms` };
57
+ logError(model, 502, `stream timeout ${STREAM_TIMEOUT_MS}ms`);
58
+ evt("upstream-error", { reqId: handlerCtx.reqId, model, status: 502, message: "stream timeout", timing: null });
59
+ evt("fallback", { reqId: handlerCtx.reqId, from: model, to: order[idx + 1] ?? null, reason: "stream timeout" });
60
+ return { handled: false, upRes: null, lastErr: err };
61
+ }
62
+ if (out.interrupted) {
63
+ if (auto) {
64
+ await auto.recordError(model, { status: 200, slow: true, note: `stall ${STALL_TIMEOUT_MS}ms` });
65
+ await auto.recordLatency(model, out.totalMs ?? (Date.now() - startedAt));
66
+ }
67
+ evt("slow-model", { model, elapsedMs: out.totalMs ?? (Date.now() - startedAt), threshold: STALL_TIMEOUT_MS, interrupted: true, detail: out.detail ?? null });
68
+ logCall(model, 200);
69
+ 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 });
70
+ evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, reqId: handlerCtx.reqId });
71
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId: handlerCtx.reqId, requested, useAuto, hops: handlerCtx.hops, stream: isStream, durationMs: Date.now() - startedAt, via: "local", status: out.status, actual: model, interrupted: true, fallback }).catch(() => {});
72
+ return { handled: true };
73
+ }
74
+ const elapsed = Date.now() - startedAt;
75
+ const latencyMs = out.totalMs ?? elapsed;
76
+ let scoredSlow = false;
77
+ if (SLOW_TOTAL_MS && auto && elapsed > SLOW_TOTAL_MS && out.status === 200) {
78
+ void auto.recordError(model, { status: 200, slow: true, note: `slow ${elapsed}ms` });
79
+ void auto.recordLatency(model, latencyMs);
80
+ evt("slow-model", { model, elapsedMs: elapsed, threshold: SLOW_TOTAL_MS, reason: "total", detail: out.detail ?? null });
81
+ scoredSlow = true;
82
+ }
83
+ if (out.detail?.stallHits > 0 && auto && out.status === 200) {
84
+ void auto.recordError(model, { status: 200, slow: true, note: `stall ${out.detail.stallHits}x gap>${SCORE_STALL_MS}ms maxGap ${out.detail.maxGapMs}ms` });
85
+ void auto.recordLatency(model, latencyMs);
86
+ evt("slow-model", { model, elapsedMs: elapsed, threshold: SCORE_STALL_MS, reason: "stall", stallHits: out.detail.stallHits, maxGapMs: out.detail.maxGapMs, detail: out.detail ?? null });
87
+ scoredSlow = true;
88
+ }
89
+ if (!scoredSlow && auto && out.status === 200) await auto.recordOk(model, { latencyMs });
90
+ else if (!scoredSlow && auto) await auto.recordLatency(model, latencyMs);
91
+ 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, hedged: true });
92
+ evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, reqId: handlerCtx.reqId });
93
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId: handlerCtx.reqId, requested, useAuto, hops: handlerCtx.hops, stream: isStream, durationMs: Date.now() - startedAt, via: "local", status: out.status, actual: model, fallback }).catch(() => {});
94
+ return { handled: true };
95
+ } else if (hedged.winner === "peer" && hedged.peerInfo) {
96
+ const win = hedged.peerInfo;
97
+ evt("peer-race-win", { reqId: handlerCtx.reqId, model, winPeer: win.peer.url, winTarget: win.target, latencyMs: win.latencyMs, hedged: true, ttfMs: hedged.ttfMs });
98
+ await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
99
+ logCall(win.target, win.res.status);
100
+ const peerFallback = buildFallbackInfo({ requested, actual: win.target, lastErr, via: "peer", useAuto, lockModel });
101
+ if (peerFallback?.fallback) evt("fallback-notice", { reqId: handlerCtx.reqId, requested, actual: win.target, reason: peerFallback.reason, notice: peerFallback.notice, via: "peer" });
102
+ evt("relay-start", { reqId: handlerCtx.reqId, model: win.target, via: "peer", isStream, fallback: peerFallback, hedged: true });
103
+ const bufferedPeerRes = { ...win.res, body: hedged.bufferedBody, headers: win.res.headers, status: win.res.status, _t: win.res._t };
104
+ const out = await relay(res, bufferedPeerRes, body, {
105
+ fallback: peerFallback,
106
+ onFirstChunk: (d) => mark(`ttf-peer-${win.target}`),
107
+ onDownstreamAbort: () => evt("client-abort", { reqId: handlerCtx.reqId, model: win.target, totalMs: Math.round(perf0 ? (Date.now() - startedAt) : 0), stages: [...stages] }),
108
+ });
109
+ evt("relay-done", { reqId: handlerCtx.reqId, model: win.target, via: "peer", status: out.status, ttfMs: out.ttfMs ?? hedged.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null, hedged: true });
110
+ if (auto && out.status === 200) {
111
+ const latencyMs = out.totalMs ?? win.latencyMs;
112
+ if (out.detail?.stallHits > 0 || (latencyMs && latencyMs > SLOW_TOTAL_MS)) {
113
+ void auto.recordError(win.target, { status: 200, slow: true, note: `peer slow ${latencyMs}ms` });
114
+ void auto.recordLatency(win.target, latencyMs);
115
+ } else {
116
+ await auto.recordOk(win.target, { latencyMs });
117
+ }
118
+ }
119
+ 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, hedged: true });
120
+ evt("client-response", { requested, actual: win.target, via: "peer", fallback: peerFallback, status: out.status, reqId: handlerCtx.reqId });
121
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId: handlerCtx.reqId, requested, useAuto, hops: handlerCtx.hops, stream: isStream, durationMs: Date.now() - startedAt, via: "peer", status: out.status, actual: win.target, fallback: peerFallback }).catch(() => {});
122
+ return { handled: true };
123
+ }
124
+ }
125
+ if (hedged && hedged.needsPeer) {
126
+ return { handled: false, upRes: null, lastErr, needsPeer: true };
127
+ } else if (!hedged || !hedged.winner) {
128
+ evt("hedge-both-fail", { reqId: handlerCtx.reqId, model });
129
+ if (!hedged) {
130
+ if (auto) await auto.recordError(model, { status: 502, slow: false, note: "hedge both fail" });
131
+ const err = { model, upstream: null, status: 502, message: "hedge both failed" };
132
+ return { handled: false, upRes: null, lastErr: err };
133
+ }
134
+ return { handled: false, upRes: null, lastErr };
135
+ }
136
+ return { handled: false, upRes: null, lastErr };
137
+ } catch (hedgeErr) {
138
+ evt("hedge-error", { reqId: handlerCtx.reqId, model, error: String(hedgeErr?.message || hedgeErr).slice(0, 300) });
139
+ // 回退到串行
140
+ return { handled: false, upRes, lastErr };
141
+ }
142
+ }
@@ -0,0 +1,178 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { injectReasoningContent, normalizeModel } from "../../reasoning.js";
3
+ import { isAutoModel } from "../../auto.js";
4
+ import { clientIp, json, readBody, parseHops, summarizePrompt, errMsg } from "../helpers.js";
5
+ import { hedgeDelayMs, shouldHedge } from "../hedge.js";
6
+ import { runHook } from "../../plugins.js";
7
+ import { handleHedge } from "./hedge-handler.js";
8
+ import { handleLocalRelay } from "./local-handler.js";
9
+ import { handlePeerRelay } from "./peer-handler.js";
10
+ import { handleBroadbandRelay } from "./broadband-handler.js";
11
+ import { handleExhaustedLocal, handleExhaustedAll } from "./exhausted-handler.js";
12
+
13
+ export async function chatHandler({ req, res, upstream, auto, logs, peers, maxHops, groups, bus, token, plugins }) {
14
+ let body;
15
+ try {
16
+ body = await readBody(req);
17
+ } catch {
18
+ return json(res, 400, { error: "Invalid JSON body" });
19
+ }
20
+
21
+ if (plugins?.length) {
22
+ const rc = await runHook(plugins, "request:received", { ip: clientIp(req), hops: parseHops(req.headers["x-mslxdff-hops"]), headers: { "content-type": req.headers["content-type"] }, body });
23
+ for (const e of rc.errors) logs?.appendEvent?.({ ts: Date.now(), type: "plugin-hook-error", hook: "request:received", plugin: e.plugin, error: e.error });
24
+ const respond = rc.value?.respond;
25
+ if (respond && typeof respond === "object") {
26
+ return json(res, respond.status || 200, respond.body ?? {});
27
+ }
28
+ }
29
+
30
+ const startedAt = Date.now();
31
+ const reqId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
32
+ const perf0 = performance.now();
33
+ const stages = [];
34
+ const mark = (name) => stages.push([name, Math.round(performance.now() - perf0)]);
35
+ const hops = parseHops(req.headers["x-mslxdff-hops"]);
36
+ const lockModel = req.headers["x-mslxdff-model-lock"] || "";
37
+ const rawModel = body.model || "";
38
+ const requested = normalizeModel(lockModel || rawModel || "");
39
+ const useAuto = isAutoModel(requested);
40
+ mark("parsed");
41
+
42
+ let order;
43
+ if (lockModel) {
44
+ order = [requested];
45
+ } else if (useAuto) {
46
+ order = auto ? await auto.candidates() : [""];
47
+ } else {
48
+ order = auto ? await auto.candidatesFor(requested) : [requested];
49
+ }
50
+ if (!order.length) order = [""];
51
+ const canFallback = order.length > 1;
52
+ const canForwardPeers = Boolean(peers) && hops < maxHops;
53
+ mark("ordered");
54
+
55
+ const logCall = (model, status) =>
56
+ logs?.appendCall({ reqId, model, auto: useAuto, status, durationMs: Date.now() - startedAt, stream: Boolean(body.stream), stages });
57
+ const logError = (model, status, message) =>
58
+ logs?.appendError({ reqId, model, auto: useAuto, status, message, stages });
59
+ const evt = (type, data) => {
60
+ const entry = { ts: Date.now(), reqId, type, ...data, model: data.model ?? requested, auto: useAuto, durationMs: Date.now() - startedAt, stages: [...stages] };
61
+ if (bus) bus.emit(entry);
62
+ logs?.appendEvent?.(entry);
63
+ };
64
+ const done = (info) => {
65
+ if (!plugins?.length) return;
66
+ runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(body.stream), durationMs: Date.now() - startedAt, ...info }).catch(() => {});
67
+ };
68
+ evt("request", { reqId, hops, ip: clientIp(req), stream: Boolean(body.stream), prompt: summarizePrompt(body), rawModel, requested, lockModel: lockModel || null });
69
+ evt("ordered", { reqId, order, canFallback, canForwardPeers, useAuto, statuses: auto?.statuses?.() ?? null });
70
+
71
+ if (plugins?.length && !lockModel) {
72
+ const sel = await runHook(plugins, "model:select", { reqId, requested, useAuto, order: [...order], hops, stream: Boolean(body.stream) });
73
+ if (sel.changed && Array.isArray(sel.value) && sel.value.length) {
74
+ order = sel.value.filter(Boolean);
75
+ if (!order.length) order = [requested];
76
+ evt("plugin-hook", { reqId, hook: "model:select", applied: true, order: [...order] });
77
+ }
78
+ for (const e of sel.errors) evt("plugin-hook-error", { reqId, hook: "model:select", plugin: e.plugin, error: e.error });
79
+ }
80
+
81
+ const handlerCtx = { reqId, model: null, body, hops, peers, plugins, evt, logError, logCall };
82
+
83
+ let lastErr = null;
84
+ for (let idx = 0; idx < order.length; idx++) {
85
+ const model = order[idx];
86
+ handlerCtx.model = model;
87
+ evt("model-try", { reqId, model, idx, remaining: order.length - idx });
88
+ if (plugins?.length) {
89
+ const bt = await runHook(plugins, "model:beforeTry", { reqId, requested, model, idx, hops });
90
+ for (const e of bt.errors) evt("plugin-hook-error", { reqId, hook: "model:beforeTry", plugin: e.plugin, error: e.error });
91
+ if (bt.value === false || bt.value?.skip === true) {
92
+ evt("plugin-hook", { reqId, hook: "model:beforeTry", applied: true, skipped: model });
93
+ continue;
94
+ }
95
+ }
96
+ let upRes = null;
97
+ let forwarded = { ...injectReasoningContent(model, body), model };
98
+ if (plugins?.length) {
99
+ const ur = await runHook(plugins, "upstream:request", { reqId, requested, model, payload: forwarded, stream: Boolean(body.stream) });
100
+ for (const e of ur.errors) evt("plugin-hook-error", { reqId, hook: "upstream:request", plugin: e.plugin, error: e.error });
101
+ if (ur.changed && ur.value?.payload && typeof ur.value.payload === "object") {
102
+ forwarded = ur.value.payload;
103
+ evt("plugin-hook", { reqId, hook: "upstream:request", applied: true, model, rewrittenModel: forwarded.model ?? null });
104
+ }
105
+ }
106
+ const tUp = performance.now();
107
+ evt("upstream-try", { reqId, model, attempt: idx + 1 });
108
+ try {
109
+ upRes = await upstream.chat(forwarded);
110
+ 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 });
111
+ } catch (err) {
112
+ if (auto) await auto.recordError(model, { message: errMsg(err) });
113
+ lastErr = { model, upstream: null, status: 502, message: errMsg(err) };
114
+ logError(model, 502, errMsg(err));
115
+ evt("upstream-error", { reqId, model, status: 502, message: errMsg(err), timing: err._t ?? { attempts: [], waitMs: 0, totalMs: Math.round(performance.now() - tUp) } });
116
+ }
117
+ if (plugins?.length) {
118
+ runHook(plugins, "upstream:response", {
119
+ reqId, requested, model,
120
+ status: upRes instanceof Error ? null : upRes instanceof Object ? (upRes.status ?? null) : null,
121
+ ok: !(upRes instanceof Error) && upRes ? upRes.status < 400 : false,
122
+ error: upRes instanceof Error ? errMsg(upRes) : null,
123
+ timing: upRes?._t ?? null,
124
+ }).catch(() => {});
125
+ }
126
+ mark(`up-${model}`);
127
+ if (upRes && upRes.status >= 400) {
128
+ if (auto) await auto.recordError(model, { status: upRes.status });
129
+ lastErr = { model, upstream: upRes, status: upRes.status, message: null };
130
+ logError(model, upRes.status, `upstream ${upRes.status}`);
131
+ evt("upstream-error", { reqId, model, status: upRes.status, message: null, timing: upRes._t ?? null });
132
+ upRes = null;
133
+ }
134
+ if (upRes) {
135
+ const isStream = Boolean(body.stream);
136
+ const d = hedgeDelayMs();
137
+ const hasPeers = Boolean(peers) && peers.ordered().length > 0;
138
+ const doHedge = shouldHedge({ isStream, canForwardPeers, hedgeDelayMs: d, hasPeers }) && upRes.status === 200 && upRes.body;
139
+ if (doHedge) {
140
+ 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 });
141
+ if (hr.handled) return;
142
+ if (hr.lastErr) lastErr = hr.lastErr;
143
+ if (hr.upRes === null) upRes = null;
144
+ else if (hr.upRes) upRes = hr.upRes;
145
+ if (upRes === null) {
146
+ // hedge 触发后走下面的 peer/broadband 分支,不再走本地串行
147
+ } else {
148
+ // hedge 未决出胜负,回退到串行本地
149
+ }
150
+ }
151
+ if (upRes) {
152
+ const lr = await handleLocalRelay({ upRes, model, body, order, idx, lastErr, requested, useAuto, lockModel, auto, handlerCtx, evt, logCall, logError, mark, perf0, stages, startedAt, plugins, res });
153
+ if (lr.handled) return;
154
+ if (lr.lastErr) { lastErr = lr.lastErr; continue; }
155
+ return;
156
+ }
157
+ }
158
+
159
+ if (canForwardPeers) {
160
+ const pr = await handlePeerRelay({ model, body, lastErr, requested, useAuto, lockModel, auto, peers, handlerCtx, evt, logCall, mark, perf0, stages, startedAt, plugins, res });
161
+ if (pr.handled) return;
162
+ }
163
+
164
+ if (groups) {
165
+ const br = await handleBroadbandRelay({ model, body, hops, lastErr, requested, useAuto, lockModel, auto, groups, token, bus, logs, handlerCtx, evt, mark, perf0, stages, res, startedAt, plugins });
166
+ if (br.handled) return;
167
+ }
168
+
169
+ if (canFallback) {
170
+ evt("fallback", { reqId, from: model, to: order[idx + 1] ?? null, reason: lastErr?.message || `upstream ${lastErr?.status ?? 502}` });
171
+ continue;
172
+ }
173
+ await handleExhaustedLocal({ res, body, lastErr, order, handlerCtx: { ...handlerCtx, model, reqId }, evt, logCall, mark, perf0, stages, done, requested, useAuto });
174
+ return;
175
+ }
176
+
177
+ await handleExhaustedAll({ res, body, lastErr, order, requested, handlerCtx: { ...handlerCtx, reqId, startedAt }, evt, logCall, mark, perf0, stages });
178
+ }
@@ -0,0 +1,88 @@
1
+ import { buildFallbackInfo } from "../fallback.js";
2
+ import { relay, SLOW_TOTAL_MS, STREAM_TIMEOUT_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS } from "../stream.js";
3
+ import { runHook } from "../../plugins.js";
4
+ import { performance } from "node:perf_hooks";
5
+
6
+ export async function handleLocalRelay({
7
+ upRes,
8
+ model,
9
+ body,
10
+ order,
11
+ idx,
12
+ lastErr,
13
+ requested,
14
+ useAuto,
15
+ lockModel,
16
+ auto,
17
+ handlerCtx,
18
+ evt,
19
+ logCall,
20
+ logError,
21
+ mark,
22
+ perf0,
23
+ stages,
24
+ startedAt,
25
+ plugins,
26
+ res,
27
+ }) {
28
+ logCall(model, upRes.status);
29
+ const fallback = buildFallbackInfo({ requested, actual: model, lastErr, via: "local", useAuto, lockModel });
30
+ if (fallback?.fallback) evt("fallback-notice", { reqId: handlerCtx.reqId, requested, actual: model, reason: fallback.reason, notice: fallback.notice, via: "local" });
31
+ evt("relay-start", { reqId: handlerCtx.reqId, model, via: "local", isStream: Boolean(body.stream), fallback });
32
+ const out = await relay(res, upRes, body, {
33
+ fallback,
34
+ onFirstChunk: (delta) => {
35
+ mark(`ttf-${model}`);
36
+ evt("relay-first-chunk", { reqId: handlerCtx.reqId, model, ttfMs: delta });
37
+ if (plugins?.length) runHook(plugins, "relay:first-chunk", { reqId: handlerCtx.reqId, requested, model, via: "local", ttfMs: delta }).catch(() => {});
38
+ },
39
+ onDownstreamAbort: () => {
40
+ evt("client-abort", { reqId: handlerCtx.reqId, model, totalMs: Math.round(performance.now() - perf0), stages: [...stages] });
41
+ },
42
+ });
43
+ evt("relay-done", { reqId: handlerCtx.reqId, model, via: "local", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
44
+ if (out.status === STREAM_TIMEOUT_MS) {
45
+ if (auto) await auto.recordError(model, { status: 502, slow: true, note: `stream timeout ${STREAM_TIMEOUT_MS}ms` });
46
+ const err = { model, upstream: null, status: 502, message: `stream timed out after ${STREAM_TIMEOUT_MS}ms` };
47
+ logError(model, 502, `stream timeout ${STREAM_TIMEOUT_MS}ms`);
48
+ evt("upstream-error", { reqId: handlerCtx.reqId, model, status: 502, message: "stream timeout", timing: null });
49
+ evt("fallback", { reqId: handlerCtx.reqId, from: model, to: order[idx + 1] ?? null, reason: "stream timeout" });
50
+ return { handled: false, upRes: null, lastErr: err };
51
+ }
52
+ if (out.interrupted) {
53
+ if (auto) {
54
+ await auto.recordError(model, { status: 200, slow: true, note: `stall ${STALL_TIMEOUT_MS}ms` });
55
+ await auto.recordLatency(model, out.totalMs ?? (Date.now() - startedAt));
56
+ }
57
+ evt("slow-model", { model, elapsedMs: out.totalMs ?? (Date.now() - startedAt), threshold: STALL_TIMEOUT_MS, interrupted: true, detail: out.detail ?? null });
58
+ logCall(model, 200);
59
+ 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 });
60
+ evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, interrupted: true, reqId: handlerCtx.reqId });
61
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId: handlerCtx.reqId, requested, useAuto, hops: handlerCtx.hops, stream: Boolean(body.stream), durationMs: Date.now() - startedAt, via: "local", status: out.status, actual: model, interrupted: true, fallback }).catch(() => {});
62
+ return { handled: true };
63
+ }
64
+ const elapsed = Date.now() - startedAt;
65
+ const latencyMs = out.totalMs ?? elapsed;
66
+ let scoredSlow = false;
67
+ if (SLOW_TOTAL_MS && auto && elapsed > SLOW_TOTAL_MS && out.status === 200) {
68
+ void auto.recordError(model, { status: 200, slow: true, note: `slow ${elapsed}ms` });
69
+ void auto.recordLatency(model, latencyMs);
70
+ evt("slow-model", { model, elapsedMs: elapsed, threshold: SLOW_TOTAL_MS, reason: "total", detail: out.detail ?? null });
71
+ scoredSlow = true;
72
+ }
73
+ if (out.detail?.stallHits > 0 && auto && out.status === 200) {
74
+ void auto.recordError(model, { status: 200, slow: true, note: `stall ${out.detail.stallHits}x gap>${SCORE_STALL_MS}ms maxGap ${out.detail.maxGapMs}ms` });
75
+ void auto.recordLatency(model, latencyMs);
76
+ evt("slow-model", { model, elapsedMs: elapsed, threshold: SCORE_STALL_MS, reason: "stall", stallHits: out.detail.stallHits, maxGapMs: out.detail.maxGapMs, detail: out.detail ?? null });
77
+ scoredSlow = true;
78
+ }
79
+ if (!scoredSlow && auto && out.status === 200) {
80
+ await auto.recordOk(model, { latencyMs });
81
+ } else if (!scoredSlow && auto) {
82
+ await auto.recordLatency(model, latencyMs);
83
+ }
84
+ 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 });
85
+ evt("client-response", { requested, actual: model, via: "local", fallback, status: out.status, reqId: handlerCtx.reqId });
86
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId: handlerCtx.reqId, requested, useAuto, hops: handlerCtx.hops, stream: Boolean(body.stream), durationMs: Date.now() - startedAt, via: "local", status: out.status, actual: model, fallback }).catch(() => {});
87
+ return { handled: true };
88
+ }
@@ -0,0 +1,58 @@
1
+ import { buildFallbackInfo } from "../fallback.js";
2
+ import { relay, SLOW_TOTAL_MS } from "../stream.js";
3
+ import { racePeerCandidates } from "../peers.js";
4
+ import { runHook } from "../../plugins.js";
5
+
6
+ export async function handlePeerRelay({
7
+ model,
8
+ body,
9
+ lastErr,
10
+ requested,
11
+ useAuto,
12
+ lockModel,
13
+ auto,
14
+ peers,
15
+ handlerCtx,
16
+ evt,
17
+ logCall,
18
+ mark,
19
+ perf0,
20
+ stages,
21
+ startedAt,
22
+ plugins,
23
+ res,
24
+ }) {
25
+ evt("peer-race-start", { reqId: handlerCtx.reqId, model, peers: peers.ordered().length });
26
+ const win =
27
+ (await racePeerCandidates(peers.ordered(), handlerCtx)) ||
28
+ (await racePeerCandidates(peers.orderedByLastError(), handlerCtx));
29
+ if (!win) {
30
+ evt("peer-race-lose", { reqId: handlerCtx.reqId, model });
31
+ return { handled: false };
32
+ }
33
+ evt("peer-race-win", { reqId: handlerCtx.reqId, model, winPeer: win.peer.url, winTarget: win.target, latencyMs: win.latencyMs });
34
+ await peers.recordResult(win.peer.url, { ok: true, latencyMs: win.latencyMs, model: win.target });
35
+ logCall(win.target, win.res.status);
36
+ const peerFallback = buildFallbackInfo({ requested, actual: win.target, lastErr, via: "peer", useAuto, lockModel });
37
+ if (peerFallback?.fallback) evt("fallback-notice", { reqId: handlerCtx.reqId, requested, actual: win.target, reason: peerFallback.reason, notice: peerFallback.notice, via: "peer" });
38
+ evt("relay-start", { reqId: handlerCtx.reqId, model: win.target, via: "peer", isStream: Boolean(body.stream), fallback: peerFallback });
39
+ const out = await relay(res, win.res, body, {
40
+ fallback: peerFallback,
41
+ onFirstChunk: (d) => mark(`ttf-peer-${win.target}`),
42
+ onDownstreamAbort: () => evt("client-abort", { reqId: handlerCtx.reqId, model: win.target, totalMs: Math.round(Date.now() - startedAt), stages: [...stages] }),
43
+ });
44
+ evt("relay-done", { reqId: handlerCtx.reqId, model: win.target, via: "peer", status: out.status, ttfMs: out.ttfMs, totalMs: out.totalMs, aborted: out.aborted, interrupted: out.interrupted ?? false, detail: out.detail ?? null });
45
+ if (auto && out.status === 200) {
46
+ const latencyMs = out.totalMs ?? win.latencyMs;
47
+ if (out.detail?.stallHits > 0 || (latencyMs && latencyMs > SLOW_TOTAL_MS)) {
48
+ void auto.recordError(win.target, { status: 200, slow: true, note: `peer slow ${latencyMs}ms` });
49
+ void auto.recordLatency(win.target, latencyMs);
50
+ } else {
51
+ await auto.recordOk(win.target, { latencyMs });
52
+ }
53
+ }
54
+ 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 });
55
+ evt("client-response", { requested, actual: win.target, via: "peer", fallback: peerFallback, status: out.status, reqId: handlerCtx.reqId });
56
+ if (plugins?.length) runHook(plugins, "request:completed", { reqId: handlerCtx.reqId, requested, useAuto, hops: handlerCtx.hops, stream: Boolean(body.stream), durationMs: Date.now() - startedAt, via: "peer", status: out.status, actual: win.target, fallback: peerFallback }).catch(() => {});
57
+ return { handled: true };
58
+ }