mslxdff 0.1.39 → 0.1.41

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,66 @@
1
+ import { timingSafeEqual, createHash } from "node:crypto";
2
+
3
+ export const errMsg = (err) => String(err?.message || err);
4
+
5
+ export function clientIp(req) {
6
+ const fwd = req.headers["x-forwarded-for"];
7
+ const head = typeof fwd === "string" ? fwd.split(",")[0].trim() : "";
8
+ const raw = String(head || req.socket.remoteAddress || "");
9
+ return raw.replace(/^::ffff:/, "").replace(/^::1$/, "127.0.0.1") || null;
10
+ }
11
+
12
+ export function authorized(req, token) {
13
+ const header = req.headers["authorization"] || "";
14
+ const match = /^Bearer (.+)$/.exec(header);
15
+ if (!match) return false;
16
+ const digests = (s) => createHash("sha256").update(s).digest();
17
+ return timingSafeEqual(digests(match[1]), digests(token));
18
+ }
19
+
20
+ export function json(res, status, body) {
21
+ res.statusCode = status;
22
+ res.setHeader("Content-Type", "application/json");
23
+ res.end(JSON.stringify(body));
24
+ }
25
+
26
+ export function notFound(res) {
27
+ return json(res, 404, { error: "Not Found" });
28
+ }
29
+
30
+ export function readBody(req) {
31
+ return new Promise((resolve, reject) => {
32
+ let data = "";
33
+ req.on("data", (c) => (data += c));
34
+ req.on("end", () => {
35
+ try {
36
+ resolve(data ? JSON.parse(data) : {});
37
+ } catch (err) {
38
+ reject(err);
39
+ }
40
+ });
41
+ req.on("error", reject);
42
+ });
43
+ }
44
+
45
+ export function parseHops(header) {
46
+ const n = Number(header);
47
+ return Number.isInteger(n) && n >= 0 ? n : 0;
48
+ }
49
+
50
+ export const PROMPT_MAX_LEN = 160;
51
+
52
+ export function summarizePrompt(body) {
53
+ const msgs = body?.messages;
54
+ if (!Array.isArray(msgs) || !msgs.length) return "";
55
+ const msg = msgs[msgs.length - 1];
56
+ const c = msg?.content;
57
+ let text = "";
58
+ if (typeof c === "string") text = c;
59
+ else if (Array.isArray(c)) {
60
+ text = c
61
+ .map((p) => (typeof p === "string" ? p : p && typeof p.text === "string" ? p.text : ""))
62
+ .join(" ");
63
+ }
64
+ text = String(text || "").replace(/\s+/g, " ").trim();
65
+ return text.length > PROMPT_MAX_LEN ? text.slice(0, PROMPT_MAX_LEN) + "…" : text;
66
+ }
@@ -0,0 +1,91 @@
1
+ import { timingSafeEqual, createHash } from "node:crypto";
2
+ import { DEFAULT_MAX_HOPS } from "../peers.js";
3
+ import { json, notFound, authorized } from "./helpers.js";
4
+ import { chatHandler } from "./chat.js";
5
+ import { joinHandler, leaveHandler } from "./groups.js";
6
+ import { heartbeatHandler, pollHandler, resultHandler, forwardHandler } from "./groups-relay.js";
7
+ import { modelsHandler, modelsStatusHandler } from "./models-route.js";
8
+
9
+ export function createRouter({ token, upstream, models, auto, logs, peers, maxHops = DEFAULT_MAX_HOPS, groups, bans, bus }) {
10
+ return async function router(req, res) {
11
+ const method = req.method || "GET";
12
+ const path = (req.url || "").split("?")[0];
13
+ const route = ROUTES.find((r) => r.method === method && r.path === path);
14
+ if (!route) return notFound(res);
15
+ if (route.requiresAuth && !authorized(req, token)) {
16
+ res.statusCode = 401;
17
+ res.setHeader("WWW-Authenticate", "Bearer");
18
+ return json(res, 401, { error: "Unauthorized" });
19
+ }
20
+ await route.handler({ req, res, upstream, models, auto, logs, peers, maxHops, groups, bans, token, bus });
21
+ };
22
+ }
23
+
24
+ const ROUTES = [
25
+ {
26
+ method: "GET",
27
+ path: "/health",
28
+ handler: ({ res }) => json(res, 200, { status: "ok" }),
29
+ },
30
+ {
31
+ method: "POST",
32
+ path: "/v1/chat/completions",
33
+ requiresAuth: true,
34
+ handler: chatHandler,
35
+ },
36
+ {
37
+ method: "POST",
38
+ path: "/v1/groups/join",
39
+ requiresAuth: false,
40
+ handler: joinHandler,
41
+ },
42
+ {
43
+ method: "POST",
44
+ path: "/v1/groups/leave",
45
+ requiresAuth: false,
46
+ handler: leaveHandler,
47
+ },
48
+ {
49
+ method: "POST",
50
+ path: "/v1/groups/relay/heartbeat",
51
+ requiresAuth: false,
52
+ handler: heartbeatHandler,
53
+ },
54
+ {
55
+ method: "POST",
56
+ path: "/v1/groups/relay/poll",
57
+ requiresAuth: false,
58
+ handler: pollHandler,
59
+ },
60
+ {
61
+ method: "POST",
62
+ path: "/v1/groups/relay/result",
63
+ requiresAuth: false,
64
+ handler: resultHandler,
65
+ },
66
+ {
67
+ method: "POST",
68
+ path: "/v1/groups/relay/forward",
69
+ requiresAuth: true,
70
+ handler: forwardHandler,
71
+ },
72
+ {
73
+ method: "GET",
74
+ path: "/v1/models",
75
+ requiresAuth: true,
76
+ handler: modelsHandler,
77
+ },
78
+ {
79
+ method: "GET",
80
+ path: "/v1/models/status",
81
+ requiresAuth: true,
82
+ handler: modelsStatusHandler,
83
+ },
84
+ ];
85
+
86
+ // re-export for facade & tests
87
+ export { errMsg, PROMPT_MAX_LEN, summarizePrompt, clientIp, authorized, json, notFound, readBody, parseHops } from "./helpers.js";
88
+ export { buildFallbackInfo, applyFallbackHeaders, enrichNonStreamJson, enrichSseChunkText } from "./fallback.js";
89
+ export { relay, SLOW_TOTAL_MS, STREAM_TIMEOUT_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS, MAX_STREAM_MS } from "./stream.js";
90
+ export { peerHealthyModels, racePeerCandidates, PEER_RACE_LIMIT } from "./peers.js";
91
+ export { enqueueRelay, dequeueRelayForPoll, resolveRelay, tryBroadbandRelay } from "./relay-queue.js";
@@ -0,0 +1,31 @@
1
+ import { json, errMsg } from "./helpers.js";
2
+
3
+ export async function modelsHandler({ res, models }) {
4
+ if (!models) return json(res, 501, { error: "Models service not configured" });
5
+ try {
6
+ const data = await models.get();
7
+ json(res, 200, data);
8
+ } catch (err) {
9
+ json(res, 502, { error: errMsg(err) });
10
+ }
11
+ }
12
+
13
+ export async function modelsStatusHandler({ res, models, auto }) {
14
+ const statuses = auto?.statuses?.() || {};
15
+ let ids = [];
16
+ try {
17
+ ids = (await models?.get?.())?.data?.map((m) => m.id) || [];
18
+ } catch {}
19
+ const seen = new Set();
20
+ const data = [];
21
+ for (const id of [...ids, ...Object.keys(statuses)]) {
22
+ if (seen.has(id)) continue;
23
+ seen.add(id);
24
+ const e = statuses[id];
25
+ const entry = typeof e === "number"
26
+ ? { id, status: "error", at: e }
27
+ : { id, status: e?.status || "normal", at: e?.at ?? null, code: e?.code ?? null };
28
+ data.push(entry);
29
+ }
30
+ json(res, 200, { object: "list", data });
31
+ }
@@ -0,0 +1,125 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { isAutoModel } from "../auto.js";
3
+ import { errMsg } from "./helpers.js";
4
+
5
+ const PEER_TIMEOUT_MS = 30_000;
6
+ const PEER_STATUS_TIMEOUT_MS = 2_000;
7
+
8
+ export async function peerHealthyModels(peer, { timeoutMs = PEER_STATUS_TIMEOUT_MS, fetchImpl = fetch } = {}) {
9
+ try {
10
+ const res = await fetchImpl(`${peer.url}/v1/models/status`, {
11
+ headers: {
12
+ "Authorization": `Bearer ${peer.token || ""}`,
13
+ "Accept": "application/json",
14
+ },
15
+ signal: AbortSignal.timeout(timeoutMs),
16
+ });
17
+ if (!res.ok) return [];
18
+ const j = await res.json().catch(() => ({}));
19
+ return (j.data || [])
20
+ .filter((m) => m && typeof m.id === "string" && m.status === "normal")
21
+ .map((m) => m.id);
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
26
+
27
+ async function forwardToPeer(peer, body, model, hops) {
28
+ const controller = new AbortController();
29
+ const timer = setTimeout(() => controller.abort(), PEER_TIMEOUT_MS);
30
+ try {
31
+ return await fetch(`${peer.url}/v1/chat/completions`, {
32
+ method: "POST",
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ "Authorization": `Bearer ${peer.token}`,
36
+ "x-mslxdff-hops": String(hops + 1),
37
+ "x-mslxdff-model-lock": model,
38
+ "Accept": "text/event-stream",
39
+ },
40
+ body: JSON.stringify({ ...body, model }),
41
+ signal: controller.signal,
42
+ });
43
+ } catch (err) {
44
+ return err;
45
+ } finally {
46
+ clearTimeout(timer);
47
+ }
48
+ }
49
+
50
+ async function resolvePeerTarget(ctx, peer) {
51
+ const prevModel = ctx.peers.stat(peer.url)?.model;
52
+ const hot = ctx.peers.isHot(peer.url) && prevModel === ctx.model;
53
+ if (hot) return { peer, target: prevModel };
54
+ const isExplicit = !!ctx.model && !isAutoModel(ctx.model);
55
+ if (isExplicit) {
56
+ const healthy = await peerHealthyModels(peer);
57
+ if (!healthy.length) {
58
+ ctx.evt("peer-health", { peer: peer.url, healthy: [], count: 0, strict: true });
59
+ return { peer, target: ctx.model };
60
+ }
61
+ ctx.evt("peer-health", { peer: peer.url, healthy, count: healthy.length, strict: true });
62
+ return { peer, target: ctx.model };
63
+ }
64
+ const healthy = await peerHealthyModels(peer);
65
+ if (!healthy.length) {
66
+ await ctx.peers.recordError(peer.url);
67
+ ctx.logError(ctx.model, 0, `peer ${peer.url} has no healthy models`);
68
+ ctx.evt("peer-health", { peer: peer.url, healthy: [], count: 0 });
69
+ return null;
70
+ }
71
+ ctx.evt("peer-health", { peer: peer.url, healthy, count: healthy.length });
72
+ return { peer, target: healthy.includes(ctx.model) ? ctx.model : healthy[0] };
73
+ }
74
+
75
+ export const PEER_RACE_LIMIT = Number(process.env.MSLXDFF_PEER_RACE_LIMIT) > 0
76
+ ? Number(process.env.MSLXDFF_PEER_RACE_LIMIT)
77
+ : 3;
78
+
79
+ export async function racePeerCandidates(candidates, ctx) {
80
+ for (let i = 0; i < candidates.length; i += PEER_RACE_LIMIT) {
81
+ const batch = candidates.slice(i, i + PEER_RACE_LIMIT);
82
+ const prepared = (await Promise.all(batch.map((peer) => resolvePeerTarget(ctx, peer)))).filter(Boolean);
83
+ if (!prepared.length) continue;
84
+ const completed = await new Promise((resolve) => {
85
+ const order = [];
86
+ const total = prepared.length;
87
+ for (const { peer, target } of prepared) {
88
+ ctx.evt("peer-request", { peer: peer.url, model: target, hops: ctx.hops + 1 });
89
+ const t0 = performance.now();
90
+ forwardToPeer(peer, ctx.body, target, ctx.hops).then((res) => {
91
+ const latencyMs = Math.round(performance.now() - t0);
92
+ const failed = res instanceof Error || res.status >= 400;
93
+ ctx.evt("peer-forward", { peer: peer.url, model: target, hops: ctx.hops + 1, latencyMs, ok: !failed });
94
+ if (failed) {
95
+ const status = res instanceof Error ? 502 : res.status;
96
+ ctx.logError(ctx.model, status, res instanceof Error ? errMsg(res) : `peer ${status}`);
97
+ ctx.evt("peer-error", { peer: peer.url, model: target, status, message: res instanceof Error ? errMsg(res) : null });
98
+ order.push({ ok: false, peer, target, res, status });
99
+ } else {
100
+ order.push({ ok: true, peer, target, res, latencyMs });
101
+ }
102
+ if (order.length === total) resolve(order);
103
+ });
104
+ }
105
+ });
106
+ const winner = completed.find((o) => o.ok);
107
+ if (winner) {
108
+ for (const o of completed) {
109
+ if (o === winner) continue;
110
+ if (!o.ok) {
111
+ await ctx.peers.recordError(o.peer.url);
112
+ await ctx.peers.recordResult(o.peer.url, { ok: false });
113
+ } else {
114
+ await ctx.peers.recordResult(o.peer.url, { ok: true, latencyMs: o.latencyMs, model: o.target });
115
+ }
116
+ }
117
+ return { peer: winner.peer, target: winner.target, res: winner.res, latencyMs: winner.latencyMs };
118
+ }
119
+ for (const o of completed) {
120
+ await ctx.peers.recordError(o.peer.url);
121
+ await ctx.peers.recordResult(o.peer.url, { ok: false });
122
+ }
123
+ }
124
+ return null;
125
+ }
@@ -0,0 +1,127 @@
1
+ import { loadGroupsJoined } from "../state.js";
2
+
3
+ const relayPending = new Map();
4
+ const relayPendingByReqId = new Map();
5
+
6
+ export function enqueueRelay({ group, target, reqId, body, hops }) {
7
+ const key = `${group}::${target}`;
8
+ const list = relayPending.get(key) || [];
9
+ return new Promise((resolve, reject) => {
10
+ const timer = setTimeout(() => {
11
+ const idx = list.findIndex((e) => e.reqId === reqId);
12
+ if (idx >= 0) list.splice(idx, 1);
13
+ relayPendingByReqId.delete(reqId);
14
+ reject(new Error("relay timeout"));
15
+ }, 30_000);
16
+ timer.unref?.();
17
+ const entry = { reqId, body, hops, resolve, reject, timer };
18
+ list.push(entry);
19
+ relayPending.set(key, list);
20
+ relayPendingByReqId.set(reqId, entry);
21
+ });
22
+ }
23
+
24
+ export function dequeueRelayForPoll({ group, target, limit = 10 }) {
25
+ const key = `${group}::${target}`;
26
+ const list = relayPending.get(key) || [];
27
+ const batch = list.splice(0, limit);
28
+ if (list.length) relayPending.set(key, list);
29
+ else relayPending.delete(key);
30
+ return batch.map((e) => ({ reqId: e.reqId, body: e.body, hops: e.hops }));
31
+ }
32
+
33
+ export function resolveRelay(reqId, result) {
34
+ const entry = relayPendingByReqId.get(reqId);
35
+ if (!entry) return false;
36
+ clearTimeout(entry.timer);
37
+ relayPendingByReqId.delete(reqId);
38
+ entry.resolve(result);
39
+ return true;
40
+ }
41
+
42
+ export function getRelayPending() { return relayPending; }
43
+ export function getRelayPendingByReqId() { return relayPendingByReqId; }
44
+
45
+ export async function tryBroadbandRelay({ groups, token: myToken, model, body, hops, bus, logs, reqId, evt, res, mark, perf0, stages }) {
46
+ try {
47
+ const joined = loadGroupsJoined();
48
+ const broadbandGroups = joined.filter((g) => g.kind === "broadband" || g.myUrl?.startsWith("relay://"));
49
+ const allCandidates = [];
50
+ if (groups) {
51
+ const localGroups = groups.list();
52
+ for (const [gName, g] of Object.entries(localGroups)) {
53
+ for (const [id, m] of Object.entries(g.members || {})) {
54
+ if (id === "leader") continue;
55
+ const isBb = m?.kind === "broadband" || String(m?.url || "").startsWith("relay://");
56
+ if (!isBb) continue;
57
+ const staleMs = Number(process.env.MSLXDFF_BROADBAND_STALE_MS) > 0 ? Number(process.env.MSLXDFF_BROADBAND_STALE_MS) : 90_000;
58
+ if (typeof m.lastSeen === "number" && Date.now() - m.lastSeen > staleMs) continue;
59
+ allCandidates.push({ group: gName, target: m.url, member: m, via: "local-leader", leaderUrl: null });
60
+ }
61
+ }
62
+ }
63
+ for (const g of joined) {
64
+ if (!g.leaderUrl) continue;
65
+ try {
66
+ const controller = new AbortController();
67
+ const timer = setTimeout(() => controller.abort(), 5000);
68
+ const r = await fetch(`${g.leaderUrl}/v1/groups/join`, {
69
+ method: "POST",
70
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${myToken}` },
71
+ body: JSON.stringify({ name: g.name, memberName: g.memberName, url: g.myUrl, token: myToken }),
72
+ signal: controller.signal,
73
+ });
74
+ clearTimeout(timer);
75
+ if (!r.ok) continue;
76
+ const data = await r.json().catch(() => ({}));
77
+ const members = data.members || {};
78
+ for (const [id, m] of Object.entries(members)) {
79
+ if (id === "leader") continue;
80
+ const isBb = m?.kind === "broadband" || String(m?.url || "").startsWith("relay://");
81
+ if (!isBb) continue;
82
+ if (m.url === g.myUrl) continue;
83
+ const staleMs = Number(process.env.MSLXDFF_BROADBAND_STALE_MS) > 0 ? Number(process.env.MSLXDFF_BROADBAND_STALE_MS) : 90_000;
84
+ if (typeof m.lastSeen === "number" && Date.now() - m.lastSeen > staleMs) continue;
85
+ allCandidates.push({ group: g.name, target: m.url, member: m, via: "via-leader", leaderUrl: g.leaderUrl });
86
+ }
87
+ } catch {}
88
+ }
89
+ if (!allCandidates.length) return null;
90
+ for (const cand of allCandidates) {
91
+ try {
92
+ evt?.("relay-try", { reqId, model, via: cand.via, target: cand.target, group: cand.group });
93
+ if (!cand.leaderUrl) {
94
+ const fwdBody = { model, ...body, model };
95
+ const reqIdLocal = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
96
+ const promise = enqueueRelay({ group: cand.group, target: cand.target, reqId: reqIdLocal, body: fwdBody, hops });
97
+ const result = await promise;
98
+ if (result && result.status) {
99
+ return { via: "broadband-local", result, target: cand.target, group: cand.group };
100
+ }
101
+ } else {
102
+ const ctrl = new AbortController();
103
+ const t = setTimeout(() => ctrl.abort(), 35_000);
104
+ const r = await fetch(`${cand.leaderUrl}/v1/groups/relay/forward`, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${myToken}`, "x-mslxdff-hops": String(hops + 1) },
107
+ body: JSON.stringify({ group: cand.group, target: cand.target, body: { ...body, model }, hops: hops + 1, reqId }),
108
+ signal: ctrl.signal,
109
+ });
110
+ clearTimeout(t);
111
+ if (!r.ok) {
112
+ const txt = await r.text().catch(() => "");
113
+ evt?.("relay-fail", { reqId, model, via: cand.via, target: cand.target, status: r.status, message: txt.slice(0, 200) });
114
+ continue;
115
+ }
116
+ return { via: "broadband-via-leader", result: r, target: cand.target, group: cand.group };
117
+ }
118
+ } catch (err) {
119
+ evt?.("relay-fail", { reqId, model, via: cand.via, target: cand.target, message: String(err?.message || err).slice(0, 200) });
120
+ continue;
121
+ }
122
+ }
123
+ return null;
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
@@ -0,0 +1,194 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { applyFallbackHeaders, enrichNonStreamJson, enrichSseChunkText } from "./fallback.js";
3
+ import { json } from "./helpers.js";
4
+
5
+ export const SLOW_TOTAL_MS = (() => {
6
+ const n = Number(process.env.MSLXDFF_SLOW_TOTAL_MS);
7
+ return Number.isInteger(n) && n > 0 ? n : 20_000;
8
+ })();
9
+
10
+ export const STREAM_TIMEOUT_MS = (() => {
11
+ const n = Number(process.env.MSLXDFF_STREAM_TIMEOUT_MS);
12
+ return Number.isInteger(n) && n > 0 ? n : 25_000;
13
+ })();
14
+
15
+ export const STALL_TIMEOUT_MS = (() => {
16
+ const n = Number(process.env.MSLXDFF_STALL_TIMEOUT_MS);
17
+ return Number.isInteger(n) && n > 0 ? n : 0;
18
+ })();
19
+
20
+ export const SCORE_STALL_MS = (() => {
21
+ const raw = process.env.MSLXDFF_SCORE_STALL_MS ?? process.env.MSLXDFF_STALL_TIMEOUT_MS;
22
+ const n = Number(raw);
23
+ return Number.isInteger(n) && n > 0 ? n : 15_000;
24
+ })();
25
+
26
+ export const MAX_STREAM_MS = (() => {
27
+ const n = Number(process.env.MSLXDFF_MAX_STREAM_MS);
28
+ return Number.isInteger(n) && n > 0 ? n : 0;
29
+ })();
30
+
31
+ export async function relay(res, upRes, body, { onFirstChunk, onDownstreamAbort, streamTimeoutMs = STREAM_TIMEOUT_MS, fallback } = {}) {
32
+ const t0 = performance.now();
33
+ const contentType = upRes.headers.get("content-type") || "";
34
+ const isStream = Boolean(body?.stream) || contentType.includes("text/event-stream");
35
+ res.statusCode = upRes.status;
36
+ if (fallback) applyFallbackHeaders(res, fallback);
37
+
38
+ let ttf = null;
39
+ let interrupted = false;
40
+ let finishedNormally = false;
41
+ const detail = {
42
+ receivedChunks: 0,
43
+ receivedBytes: 0,
44
+ wroteChunks: 0,
45
+ wroteBytes: 0,
46
+ sawDone: false,
47
+ sawFinishReason: null,
48
+ lastChunkAtMs: null,
49
+ lastChunkGapMs: null,
50
+ maxGapMs: 0,
51
+ stallHits: 0,
52
+ exitReason: null,
53
+ upstreamError: null,
54
+ downstreamClosed: false,
55
+ };
56
+ let prevChunkAt = t0;
57
+ const onClose = () => {
58
+ detail.downstreamClosed = true;
59
+ if (!finishedNormally && onDownstreamAbort) onDownstreamAbort();
60
+ };
61
+ res.on("close", onClose);
62
+
63
+ if (isStream) {
64
+ res.setHeader("Content-Type", "text/event-stream");
65
+ res.setHeader("Cache-Control", "no-cache");
66
+ res.setHeader("Connection", "keep-alive");
67
+ if (fallback?.fallback) {
68
+ try {
69
+ res.write(`: mslxdff fallback ${fallback.requested_model} -> ${fallback.actual_model} (${fallback.reason})\n`);
70
+ res.write(`: notice ${fallback.notice}\n\n`);
71
+ } catch {}
72
+ }
73
+ if (upRes.body) {
74
+ let first = true;
75
+ let wroteAny = false;
76
+ let timedOut = false;
77
+ let stalled = false;
78
+ let tooLong = false;
79
+ let stallTimer = null;
80
+ const armStall = () => {
81
+ if (stallTimer) clearTimeout(stallTimer);
82
+ stallTimer = STALL_TIMEOUT_MS
83
+ ? setTimeout(() => {
84
+ stalled = true;
85
+ detail.exitReason = "stall";
86
+ if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
87
+ }, STALL_TIMEOUT_MS)
88
+ : null;
89
+ };
90
+ let firstTimer = setTimeout(() => {
91
+ timedOut = true;
92
+ detail.exitReason = "first-timeout";
93
+ if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
94
+ }, streamTimeoutMs);
95
+ const maxTimer = MAX_STREAM_MS
96
+ ? setTimeout(() => {
97
+ tooLong = true;
98
+ detail.exitReason = "max";
99
+ if (typeof upRes.body.cancel === "function") upRes.body.cancel().catch(() => {});
100
+ }, MAX_STREAM_MS)
101
+ : null;
102
+ try {
103
+ for await (const chunk of upRes.body) {
104
+ const now = performance.now();
105
+ detail.receivedChunks += 1;
106
+ const len = chunk?.length ?? chunk?.byteLength ?? 0;
107
+ detail.receivedBytes += len;
108
+ const gap = Math.round(now - prevChunkAt);
109
+ detail.lastChunkAtMs = Math.round(now - t0);
110
+ detail.lastChunkGapMs = gap;
111
+ if (gap > detail.maxGapMs) detail.maxGapMs = gap;
112
+ if (gap > SCORE_STALL_MS) detail.stallHits += 1;
113
+ prevChunkAt = now;
114
+ try {
115
+ const txt = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : typeof chunk === "string" ? chunk : "";
116
+ if (txt.includes("[DONE]")) detail.sawDone = true;
117
+ const m = txt.match(/"finish_reason"\s*:\s*"([^"]+)"/);
118
+ if (m) detail.sawFinishReason = m[1];
119
+ } catch { /* ignore */ }
120
+ if (timedOut || stalled || tooLong) break;
121
+ if (first) {
122
+ first = false;
123
+ ttf = Math.round(now - t0);
124
+ onFirstChunk?.(ttf);
125
+ if (firstTimer) { clearTimeout(firstTimer); firstTimer = null; }
126
+ }
127
+ let outChunk = chunk;
128
+ if (first === false && fallback?.fallback && wroteAny === false) {
129
+ try {
130
+ let txt = "";
131
+ if (Buffer.isBuffer(chunk)) txt = chunk.toString("utf8");
132
+ else if (chunk instanceof Uint8Array) txt = Buffer.from(chunk).toString("utf8");
133
+ else if (typeof chunk === "string") txt = chunk;
134
+ if (txt.includes("data:")) {
135
+ const enriched = enrichSseChunkText(txt, fallback);
136
+ if (enriched !== txt) outChunk = Buffer.from(enriched, "utf8");
137
+ }
138
+ } catch {}
139
+ }
140
+ wroteAny = true;
141
+ detail.wroteChunks += 1;
142
+ detail.wroteBytes += Buffer.isBuffer(outChunk) ? outChunk.length : (outChunk?.length ?? len);
143
+ res.write(outChunk);
144
+ armStall();
145
+ }
146
+ if (!detail.exitReason) detail.exitReason = "normal";
147
+ } catch (err) {
148
+ detail.upstreamError = String(err?.message || err).slice(0, 300);
149
+ detail.exitReason = "upstream-error";
150
+ if (!wroteAny) timedOut = true;
151
+ else stalled = true;
152
+ } finally {
153
+ if (firstTimer) clearTimeout(firstTimer);
154
+ if (maxTimer) clearTimeout(maxTimer);
155
+ if (stallTimer) clearTimeout(stallTimer);
156
+ }
157
+ if (timedOut && !wroteAny) {
158
+ res.removeListener("close", onClose);
159
+ return { status: STREAM_TIMEOUT_MS, ttfMs: null, totalMs: Math.round(performance.now() - t0), aborted: true, interrupted: false, detail };
160
+ }
161
+ if ((stalled || tooLong) && wroteAny) {
162
+ interrupted = true;
163
+ detail.exitReason = detail.exitReason || (stalled ? "stall" : "max");
164
+ res.removeListener("close", onClose);
165
+ try { res.end(); } catch { /* ignore */ }
166
+ return { status: 200, ttfMs: ttf, totalMs: Math.round(performance.now() - t0), aborted: false, interrupted, detail };
167
+ }
168
+ } else {
169
+ detail.exitReason = "empty-body";
170
+ }
171
+ const totalMs = Math.round(performance.now() - t0);
172
+ if (!detail.exitReason) detail.exitReason = "normal";
173
+ finishedNormally = true;
174
+ res.removeListener("close", onClose);
175
+ try { res.end(); } catch { /* ignore */ }
176
+ return { status: 200, ttfMs: ttf, totalMs, aborted: false, interrupted: false, detail };
177
+ }
178
+
179
+ finishedNormally = true;
180
+ res.removeListener("close", onClose);
181
+ const text = await upRes.text();
182
+ detail.receivedBytes = Buffer.byteLength(text);
183
+ detail.exitReason = "normal-non-stream";
184
+ try {
185
+ const parsed = JSON.parse(text);
186
+ const enriched = enrichNonStreamJson(parsed, fallback);
187
+ json(res, upRes.status, enriched);
188
+ } catch {
189
+ res.statusCode = upRes.status;
190
+ res.setHeader("Content-Type", contentType || "text/plain");
191
+ res.end(text);
192
+ }
193
+ return { status: upRes.status, ttfMs: null, totalMs: Math.round(performance.now() - t0), aborted: false, interrupted: false, detail };
194
+ }