@khanglvm/llm-router 1.0.5

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,294 @@
1
+ /**
2
+ * Local HTTP server wrapper around the shared fetch handler.
3
+ */
4
+
5
+ import http from "node:http";
6
+ import path from "node:path";
7
+ import { watch as fsWatch } from "node:fs";
8
+ import { Readable } from "node:stream";
9
+ import { createFetchHandler } from "../runtime/handler.js";
10
+ import { readConfigFile, getDefaultConfigPath } from "./config-store.js";
11
+
12
+ const DEFAULT_CONFIG_RELOAD_DEBOUNCE_MS = 300;
13
+ const MAX_CONFIG_RELOAD_DEBOUNCE_MS = 5000;
14
+
15
+ function resolveReloadDebounceMs(value) {
16
+ if (value === undefined || value === null || value === "") {
17
+ return DEFAULT_CONFIG_RELOAD_DEBOUNCE_MS;
18
+ }
19
+
20
+ const parsed = Number.parseInt(String(value), 10);
21
+ if (!Number.isFinite(parsed) || parsed < 0) {
22
+ return DEFAULT_CONFIG_RELOAD_DEBOUNCE_MS;
23
+ }
24
+
25
+ return Math.min(parsed, MAX_CONFIG_RELOAD_DEBOUNCE_MS);
26
+ }
27
+
28
+ function formatError(error) {
29
+ return error instanceof Error ? error.message : String(error);
30
+ }
31
+
32
+ function createLiveConfigStore({
33
+ configPath,
34
+ watchConfig = true,
35
+ reloadDebounceMs = DEFAULT_CONFIG_RELOAD_DEBOUNCE_MS,
36
+ validateConfig,
37
+ onReload,
38
+ onReloadError
39
+ }) {
40
+ let currentConfig = null;
41
+ let initialLoadPromise = null;
42
+ let inFlightReload = null;
43
+ let queuedReloadReason = "";
44
+ let watcher = null;
45
+ let reloadTimer = null;
46
+ let closed = false;
47
+
48
+ const configDir = path.dirname(configPath);
49
+ const configFile = path.basename(configPath);
50
+
51
+ const emitReloadError = (error, reason) => {
52
+ if (typeof onReloadError === "function") {
53
+ onReloadError(error, reason);
54
+ return;
55
+ }
56
+
57
+ console.error(`[llm-router] Failed reloading config (${reason}): ${formatError(error)}`);
58
+ };
59
+
60
+ async function loadAndSwap(reason) {
61
+ try {
62
+ const next = await readConfigFile(configPath);
63
+ if (typeof validateConfig === "function") {
64
+ const validationError = validateConfig(next);
65
+ if (validationError) {
66
+ throw new Error(validationError);
67
+ }
68
+ }
69
+
70
+ currentConfig = next;
71
+ if (typeof onReload === "function") {
72
+ onReload(next, reason);
73
+ }
74
+ return currentConfig;
75
+ } catch (error) {
76
+ emitReloadError(error, reason);
77
+ if (!currentConfig) throw error;
78
+ return currentConfig;
79
+ }
80
+ }
81
+
82
+ async function triggerReload(reason) {
83
+ if (closed) return currentConfig;
84
+ if (inFlightReload) {
85
+ queuedReloadReason = reason;
86
+ return inFlightReload;
87
+ }
88
+
89
+ inFlightReload = loadAndSwap(reason)
90
+ .finally(() => {
91
+ inFlightReload = null;
92
+ if (queuedReloadReason && !closed) {
93
+ const nextReason = queuedReloadReason;
94
+ queuedReloadReason = "";
95
+ void triggerReload(nextReason);
96
+ }
97
+ });
98
+
99
+ return inFlightReload;
100
+ }
101
+
102
+ function scheduleReload(reason) {
103
+ if (closed) return;
104
+ if (reloadTimer) clearTimeout(reloadTimer);
105
+ reloadTimer = setTimeout(() => {
106
+ reloadTimer = null;
107
+ void triggerReload(reason);
108
+ }, reloadDebounceMs);
109
+ }
110
+
111
+ function startWatcher() {
112
+ if (!watchConfig) return;
113
+ try {
114
+ watcher = fsWatch(configDir, (eventType, filename) => {
115
+ if (closed) return;
116
+ if (!filename) return;
117
+ if (String(filename) !== configFile) return;
118
+ scheduleReload(eventType || "change");
119
+ });
120
+ } catch (error) {
121
+ emitReloadError(error, "watch-init");
122
+ }
123
+ }
124
+
125
+ async function getConfig() {
126
+ if (currentConfig) return currentConfig;
127
+ if (!initialLoadPromise) {
128
+ initialLoadPromise = triggerReload("startup")
129
+ .finally(() => {
130
+ initialLoadPromise = null;
131
+ });
132
+ }
133
+ return initialLoadPromise;
134
+ }
135
+
136
+ function close() {
137
+ closed = true;
138
+ if (reloadTimer) {
139
+ clearTimeout(reloadTimer);
140
+ reloadTimer = null;
141
+ }
142
+ if (watcher) {
143
+ watcher.close();
144
+ watcher = null;
145
+ }
146
+ }
147
+
148
+ startWatcher();
149
+
150
+ return {
151
+ getConfig,
152
+ reloadNow: async (reason = "manual") => triggerReload(reason),
153
+ close
154
+ };
155
+ }
156
+
157
+ function formatHostForUrl(host, port) {
158
+ const value = String(host || "127.0.0.1").trim();
159
+ if (!value.includes(":")) return `${value}:${port}`;
160
+ if (value.startsWith("[") && value.endsWith("]")) return `${value}:${port}`;
161
+ return `[${value}]:${port}`;
162
+ }
163
+
164
+ function normalizeRequestPath(rawUrl) {
165
+ const value = String(rawUrl || "/").trim() || "/";
166
+ if (value.startsWith("http://") || value.startsWith("https://")) {
167
+ try {
168
+ const parsed = new URL(value);
169
+ return `${parsed.pathname}${parsed.search}` || "/";
170
+ } catch {
171
+ return "/";
172
+ }
173
+ }
174
+ if (value.startsWith("/")) return value;
175
+ return `/${value}`;
176
+ }
177
+
178
+ function buildRequestUrl(req, fallbackHost) {
179
+ const path = normalizeRequestPath(req.url);
180
+ return `http://${fallbackHost}${path}`;
181
+ }
182
+
183
+ function nodeRequestToFetchRequest(req, fallbackHost) {
184
+ const url = buildRequestUrl(req, fallbackHost);
185
+ const method = req.method || "GET";
186
+ const headers = new Headers();
187
+
188
+ for (const [name, value] of Object.entries(req.headers)) {
189
+ if (Array.isArray(value)) {
190
+ for (const item of value) headers.append(name, item);
191
+ } else if (typeof value === "string") {
192
+ headers.set(name, value);
193
+ }
194
+ }
195
+
196
+ // Use the actual socket address for local IP allowlist checks.
197
+ const socketIp = typeof req.socket?.remoteAddress === "string"
198
+ ? req.socket.remoteAddress
199
+ : "";
200
+ if (socketIp) {
201
+ headers.set("x-real-ip", socketIp);
202
+ }
203
+
204
+ const hasBody = method !== "GET" && method !== "HEAD";
205
+ if (!hasBody) {
206
+ return new Request(url, { method, headers });
207
+ }
208
+
209
+ return new Request(url, {
210
+ method,
211
+ headers,
212
+ body: Readable.toWeb(req),
213
+ duplex: "half"
214
+ });
215
+ }
216
+
217
+ async function writeFetchResponseToNode(res, response) {
218
+ res.statusCode = response.status;
219
+ response.headers.forEach((value, name) => {
220
+ res.setHeader(name, value);
221
+ });
222
+
223
+ if (!response.body) {
224
+ res.end();
225
+ return;
226
+ }
227
+
228
+ const readable = Readable.fromWeb(response.body);
229
+ readable.on("error", (error) => {
230
+ res.destroy(error);
231
+ });
232
+ readable.pipe(res);
233
+ }
234
+
235
+ export async function startLocalRouteServer({
236
+ port = 8787,
237
+ host = "127.0.0.1",
238
+ configPath = getDefaultConfigPath(),
239
+ watchConfig = true,
240
+ configReloadDebounceMs = process.env.LLM_ROUTER_CONFIG_RELOAD_DEBOUNCE_MS,
241
+ validateConfig,
242
+ onConfigReload,
243
+ onConfigReloadError,
244
+ requireAuth = false
245
+ } = {}) {
246
+ const reloadDebounceMs = resolveReloadDebounceMs(configReloadDebounceMs);
247
+ const configStore = createLiveConfigStore({
248
+ configPath,
249
+ watchConfig,
250
+ reloadDebounceMs,
251
+ validateConfig,
252
+ onReload: onConfigReload,
253
+ onReloadError: onConfigReloadError
254
+ });
255
+ await configStore.getConfig();
256
+
257
+ const fetchHandler = createFetchHandler({
258
+ ignoreAuth: !requireAuth,
259
+ getConfig: () => configStore.getConfig()
260
+ });
261
+
262
+ const fallbackHost = formatHostForUrl(host, port);
263
+
264
+ const server = http.createServer(async (req, res) => {
265
+ try {
266
+ const request = nodeRequestToFetchRequest(req, fallbackHost);
267
+ const response = await fetchHandler(request, {}, undefined);
268
+ await writeFetchResponseToNode(res, response);
269
+ } catch (error) {
270
+ res.statusCode = 500;
271
+ res.setHeader("content-type", "application/json");
272
+ res.end(JSON.stringify({
273
+ error: "Internal server error",
274
+ message: error instanceof Error ? error.message : String(error)
275
+ }));
276
+ }
277
+ });
278
+
279
+ await new Promise((resolve, reject) => {
280
+ server.once("error", reject);
281
+ server.listen(port, host, () => {
282
+ server.off("error", reject);
283
+ resolve();
284
+ });
285
+ });
286
+
287
+ const originalClose = server.close.bind(server);
288
+ server.close = (callback) => {
289
+ configStore.close();
290
+ return originalClose(callback);
291
+ };
292
+
293
+ return server;
294
+ }