mslxdff 0.1.39 → 0.1.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/mslxdff.js +35 -9
- package/package.json +5 -2
- package/src/routes/chat.js +278 -0
- package/src/routes/fallback.js +88 -0
- package/src/routes/groups-relay.js +129 -0
- package/src/routes/groups.js +121 -0
- package/src/routes/helpers.js +66 -0
- package/src/routes/index.js +91 -0
- package/src/routes/models-route.js +31 -0
- package/src/routes/peers.js +125 -0
- package/src/routes/relay-queue.js +127 -0
- package/src/routes/stream.js +194 -0
- package/src/routes.js +3 -1316
- package/src/upstream.js +88 -4
|
@@ -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
|
+
}
|