// --------------------------------------------------------------------------- // Tiny API proxy for interactive dashboard features. // Runs inside the container on localhost; nginx proxies /api/chat here. // --------------------------------------------------------------------------- const http = require("http"); const https = require("https"); const { URL } = require("url"); const PORT = process.env.API_PORT || 3001; const OPENWEBUI_URL = process.env.OPENWEBUI_URL || "http://100.103.83.12:3000"; const OPENWEBUI_TOKEN = process.env.OPENWEBUI_TOKEN || ""; const GPU_STATS_URL = process.env.GPU_STATS_URL || "http://gpu-stats:9101/stats"; const OLLAMA_URL = process.env.OLLAMA_URL || "http://100.103.83.12:11435"; function request(url, options = {}) { return new Promise((resolve, reject) => { const u = new URL(url); const client = u.protocol === "https:" ? https : http; const req = client.request( u, { method: options.method || "GET", headers: options.headers || {}, timeout: options.timeout || 120000, }, (res) => { let body = ""; res.setEncoding("utf8"); res.on("data", (chunk) => (body += chunk)); res.on("end", () => resolve({ status: res.statusCode, headers: res.headers, body })); } ); req.on("error", reject); req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); }); if (options.body) req.write(options.body); req.end(); }); } function jsonResponse(res, status, data) { res.writeHead(status, { "Content-Type": "application/json" }); res.end(JSON.stringify(data)); } function readBody(req) { return new Promise((resolve, reject) => { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", () => resolve(body)); req.on("error", reject); }); } const server = http.createServer(async (req, res) => { // Very small CORS allowlist: only dashboard origin, but since this sits behind // nginx on the same origin we can keep it strict. res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type"); if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; } const url = new URL(req.url, `http://127.0.0.1:${PORT}`); if (url.pathname === "/health" && req.method === "GET") { return jsonResponse(res, 200, { ok: true }); } // Always drain the request body before responding, otherwise leftover bytes // corrupt keep-alive connections (e.g. nginx upstream keepalive). req.resume(); if (url.pathname === "/api/gpu-stats" && req.method === "GET") { try { const upstream = await request(GPU_STATS_URL, { timeout: 8000 }); res.writeHead(upstream.status, { "Content-Type": "application/json", "Cache-Control": "no-store", }); res.end(upstream.body); } catch (e) { return jsonResponse(res, 502, { error: `GPU stats unreachable: ${e.message}` }); } } if (url.pathname === "/api/ollama" && req.method === "GET") { try { const upstream = await request(`${OLLAMA_URL}/api/ps`, { timeout: 8000 }); res.writeHead(upstream.status, { "Content-Type": "application/json", "Cache-Control": "no-store", }); res.end(upstream.body); } catch (e) { return jsonResponse(res, 502, { error: `Ollama unreachable: ${e.message}` }); } } if (url.pathname === "/api/chat" && req.method === "POST") { if (!OPENWEBUI_TOKEN) { return jsonResponse(res, 503, { error: "OPENWEBUI_TOKEN not configured" }); } try { const raw = await readBody(req); const body = JSON.parse(raw || "{}"); const { model, prompt } = body; if (!model || !prompt) { return jsonResponse(res, 400, { error: "model and prompt are required" }); } const payload = JSON.stringify({ model, messages: [{ role: "user", content: prompt }], stream: false, }); const upstream = await request(`${OPENWEBUI_URL}/api/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${OPENWEBUI_TOKEN}`, }, body: payload, }); if (upstream.status < 200 || upstream.status >= 300) { return jsonResponse(res, upstream.status, { error: `Open WebUI ${upstream.status}`, body: upstream.body }); } const data = JSON.parse(upstream.body); const message = data.choices?.[0]?.message?.content || data.response || "(no response)"; return jsonResponse(res, 200, { message }); } catch (e) { return jsonResponse(res, 500, { error: e.message }); } } res.writeHead(404); res.end("Not found"); }); // This is a tiny internal proxy: never let a per-request error kill the // process (that would take down the GPU stats proxy too). process.on("uncaughtException", (err) => { console.error("uncaughtException:", err); }); process.on("unhandledRejection", (err) => { console.error("unhandledRejection:", err); }); server.listen(PORT, "127.0.0.1", () => { console.log(`Dashboard API proxy listening on 127.0.0.1:${PORT}`); });