120 lines
3.5 KiB
JavaScript
120 lines
3.5 KiB
JavaScript
import { createServer } from "node:http";
|
|
import { readFile } from "node:fs/promises";
|
|
import { extname, join, normalize } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = fileURLToPath(new URL(".", import.meta.url));
|
|
const port = Number(process.env.PORT || 4173);
|
|
|
|
const mime = {
|
|
".html": "text/html; charset=utf-8",
|
|
".css": "text/css; charset=utf-8",
|
|
".js": "text/javascript; charset=utf-8",
|
|
".json": "application/json; charset=utf-8",
|
|
};
|
|
|
|
function readBody(request) {
|
|
return new Promise((resolve, reject) => {
|
|
let body = "";
|
|
request.on("data", (chunk) => {
|
|
body += chunk;
|
|
if (body.length > 2_000_000) {
|
|
reject(new Error("Request body is too large"));
|
|
request.destroy();
|
|
}
|
|
});
|
|
request.on("end", () => resolve(body));
|
|
request.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function sendJson(response, status, payload) {
|
|
response.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
response.end(JSON.stringify(payload));
|
|
}
|
|
|
|
async function handleChat(request, response) {
|
|
try {
|
|
const payload = JSON.parse(await readBody(request));
|
|
const apiBase = String(payload.apiBase || "").replace(/\/+$/, "");
|
|
const apiKey = String(payload.apiKey || "");
|
|
const model = String(payload.model || "");
|
|
|
|
if (!apiBase || !apiKey || !model) {
|
|
sendJson(response, 400, { error: "API base, API key, and model are required." });
|
|
return;
|
|
}
|
|
|
|
const upstreamBody = {
|
|
model,
|
|
messages: payload.messages || [],
|
|
temperature: payload.temperature,
|
|
max_tokens: payload.max_tokens,
|
|
};
|
|
|
|
if (payload.response_format) {
|
|
upstreamBody.response_format = payload.response_format;
|
|
}
|
|
|
|
const upstream = await fetch(`${apiBase}/chat/completions`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${apiKey}`,
|
|
},
|
|
body: JSON.stringify(upstreamBody),
|
|
});
|
|
|
|
const data = await upstream.json().catch(() => ({}));
|
|
if (!upstream.ok) {
|
|
sendJson(response, upstream.status, { error: data?.error?.message || "Model request failed." });
|
|
return;
|
|
}
|
|
|
|
sendJson(response, 200, { content: data?.choices?.[0]?.message?.content || "" });
|
|
} catch (error) {
|
|
const cause = error.cause?.message ? ` (${error.cause.message})` : "";
|
|
sendJson(response, 500, { error: `${error.message}${cause}` });
|
|
}
|
|
}
|
|
|
|
async function handleStatic(request, response) {
|
|
const url = new URL(request.url || "/", `http://${request.headers.host}`);
|
|
const cleanPath = normalize(url.pathname === "/" ? "/index.html" : url.pathname).replace(/^(\.\.[/\\])+/, "");
|
|
const filePath = join(root, cleanPath);
|
|
|
|
if (!filePath.startsWith(root)) {
|
|
response.writeHead(403);
|
|
response.end("Forbidden");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const content = await readFile(filePath);
|
|
response.writeHead(200, { "Content-Type": mime[extname(filePath)] || "application/octet-stream" });
|
|
response.end(content);
|
|
} catch {
|
|
response.writeHead(404);
|
|
response.end("Not found");
|
|
}
|
|
}
|
|
|
|
const server = createServer((request, response) => {
|
|
if (request.method === "POST" && request.url === "/api/chat") {
|
|
handleChat(request, response);
|
|
return;
|
|
}
|
|
|
|
if (request.method === "GET" || request.method === "HEAD") {
|
|
handleStatic(request, response);
|
|
return;
|
|
}
|
|
|
|
response.writeHead(405);
|
|
response.end("Method not allowed");
|
|
});
|
|
|
|
server.listen(port, () => {
|
|
console.log(`Identity Prompt Lab: http://localhost:${port}`);
|
|
});
|