Files
pikasTech-HWLAB/cmd/hwlab-codex-api-responses-forwarder/main.ts
T

237 lines
7.5 KiB
JavaScript

#!/usr/bin/env node
import { createServer, request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
const serviceId = "hwlab-codex-api-responses-forwarder";
const defaultPort = 49280;
const port = parsePositiveInteger(process.env.HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT || process.env.PORT, defaultPort);
const host = firstNonEmpty(process.env.HWLAB_CODE_AGENT_CODEX_API_FORWARDER_HOST, "127.0.0.1");
const upstreamBaseUrl = new URL(firstNonEmpty(process.env.HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL, "https://hyueapi.com"));
const upstreamNoProxyEntries = noProxyEntriesForHost(upstreamBaseUrl.hostname);
const maxBodyBytes = parsePositiveInteger(process.env.HWLAB_CODE_AGENT_CODEX_API_FORWARDER_MAX_BODY_BYTES, 64 * 1024 * 1024);
const hopByHopHeaders = new Set([
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade"
]);
ensureNoProxyEntries(upstreamNoProxyEntries);
const server = createServer(async (clientReq, clientRes) => {
try {
const url = new URL(clientReq.url || "/", "http://hwlab-codex-api-forwarder.local");
if (url.pathname === "/health" || url.pathname === "/health/live" || url.pathname === "/health/liveliness") {
sendJson(clientRes, 200, healthPayload("ok"));
return;
}
if (url.pathname === "/health/readiness") {
sendJson(clientRes, 200, healthPayload("ready"));
return;
}
await proxyRequest(clientReq, clientRes, url);
} catch (error) {
sendJson(clientRes, 502, {
error: {
code: "codex_api_forwarder_failed",
message: "Codex API Responses forwarder failed to proxy request",
reason: error.message
}
});
}
});
server.listen(port, host, () => {
process.stdout.write(JSON.stringify({
serviceId,
status: "listening",
host,
port,
upstreamHost: upstreamBaseUrl.hostname,
noProxyRequired: upstreamNoProxyEntries
}) + "\n");
});
async function proxyRequest(clientReq, clientRes, url) {
const startedAt = Date.now();
const targetUrl = targetUrlForRequest(url);
const headers = filterRequestHeaders(clientReq.headers, targetUrl);
const requestImpl = targetUrl.protocol === "https:" ? httpsRequest : httpRequest;
const upstreamReq = requestImpl({
protocol: targetUrl.protocol,
hostname: targetUrl.hostname,
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
method: clientReq.method,
path: `${targetUrl.pathname}${targetUrl.search}`,
headers
}, (upstreamRes) => {
const responseHeaders = filterResponseHeaders(upstreamRes.headers);
clientRes.writeHead(upstreamRes.statusCode || 502, responseHeaders);
let responseBytes = 0;
upstreamRes.on("data", (chunk) => {
responseBytes += chunk.length;
});
upstreamRes.on("end", () => {
process.stderr.write(JSON.stringify({
serviceId,
method: clientReq.method,
path: url.pathname,
upstreamPath: targetUrl.pathname,
status: upstreamRes.statusCode || null,
responseBytes,
elapsedMs: Date.now() - startedAt
}) + "\n");
});
upstreamRes.pipe(clientRes);
});
upstreamReq.on("error", (error) => {
if (!clientRes.headersSent) {
sendJson(clientRes, 502, {
error: {
code: "codex_api_forwarder_upstream_error",
message: "Codex API forwarder upstream request failed",
reason: error.message
}
});
} else {
clientRes.end();
}
});
await pipeRequestBody(clientReq, upstreamReq);
}
async function pipeRequestBody(clientReq, upstreamReq) {
let bytes = 0;
for await (const chunk of clientReq) {
bytes += chunk.length;
if (bytes > maxBodyBytes) {
upstreamReq.destroy(new Error(`request body exceeded ${maxBodyBytes} bytes`));
return;
}
if (!upstreamReq.write(chunk)) {
await new Promise((resolve) => upstreamReq.once("drain", resolve));
}
}
upstreamReq.end();
}
function targetUrlForRequest(url) {
const target = new URL(upstreamBaseUrl.toString());
target.pathname = upstreamPathFor(url.pathname);
target.search = url.search;
target.hash = "";
return target;
}
function upstreamPathFor(inboundPath) {
const basePath = upstreamBaseUrl.pathname.replace(/\/+$/u, "");
const requestPath = normalizeCodexResponsesPath(inboundPath || "/");
if (!basePath) return requestPath;
if (basePath === "/v1" && requestPath.startsWith("/v1/")) return requestPath;
if (basePath === "/v1" && requestPath === "/v1") return requestPath;
if (basePath.endsWith("/responses") && (requestPath === "/responses" || requestPath === "/v1/responses")) return basePath;
if (basePath.endsWith("/models") && (requestPath === "/models" || requestPath === "/v1/models")) return basePath;
if (requestPath.startsWith(`${basePath}/`) || requestPath === basePath) return requestPath;
return `${basePath}${requestPath}`;
}
function normalizeCodexResponsesPath(pathname) {
if (upstreamBaseUrl.pathname.replace(/\/+$/u, "") === "/v1") return pathname;
if (pathname === "/v1/responses") return "/responses";
if (pathname === "/v1/models") return "/models";
return pathname;
}
function filterRequestHeaders(headers, targetUrl) {
const filtered = {};
for (const [name, value] of Object.entries(headers)) {
const lower = name.toLowerCase();
if (hopByHopHeaders.has(lower) || lower === "host") continue;
filtered[name] = value;
}
filtered.host = targetUrl.host;
return filtered;
}
function filterResponseHeaders(headers) {
const filtered = {};
for (const [name, value] of Object.entries(headers)) {
if (hopByHopHeaders.has(name.toLowerCase())) continue;
filtered[name] = value;
}
return filtered;
}
function sendJson(res, status, payload) {
const body = JSON.stringify(payload);
res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(body) });
res.end(body);
}
function healthPayload(status) {
return {
serviceId,
status,
listen: { host, port },
upstream: {
origin: upstreamBaseUrl.origin,
host: upstreamBaseUrl.hostname,
directNoProxyRequired: true,
noProxyRequired: upstreamNoProxyEntries
}
};
}
function ensureNoProxyEntries(entries) {
for (const name of ["NO_PROXY", "no_proxy"]) {
process.env[name] = mergeNoProxyEntries(process.env[name], entries);
}
}
function noProxyEntriesForHost(hostname) {
const hostText = String(hostname || "").trim().toLowerCase();
if (!hostText) return ["127.0.0.1", "localhost", "::1"];
const entries = [hostText, "127.0.0.1", "localhost", "::1"];
if (!/^\d+(?:\.\d+){3}$/u.test(hostText) && !hostText.includes(":")) entries.push(`.${hostText}`);
return uniqueStrings(entries);
}
function mergeNoProxyEntries(current, additions = []) {
return uniqueStrings([...String(current ?? "").split(/[,;\s]+/u), ...additions]).join(",");
}
function uniqueStrings(values) {
const seen = new Set();
const result = [];
for (const value of values) {
const text = String(value ?? "").trim();
if (!text) continue;
const key = text.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
result.push(text);
}
return result;
}
function firstNonEmpty(...values) {
for (const value of values) {
const text = String(value ?? "").trim();
if (text) return text;
}
return "";
}
function parsePositiveInteger(value, fallback) {
const parsed = Number.parseInt(value || "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}