mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:23:33 +00:00
- Fix typos: parellel→parallel, neccessary→necessary, mulitple→multiple, responible→responsible, geniune→genuine, yeild→yield - Add ignore words: Nd, nd, edn, lightening (valid terms) - Run prettier --write to fix all formatting issues
276 lines
9.8 KiB
JavaScript
Executable File
276 lines
9.8 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
// Local OpenAI proxy replicating the upstream-merge workflow behavior.
|
||
//
|
||
// Features:
|
||
// - Injects real OPENAI_API_KEY server-side; clients can use a dummy key
|
||
// - Allows only /v1/chat/completions and /v1/responses
|
||
// - Adds OpenAI-Beta: responses=v1 for Responses API
|
||
// - Sets Accept: text/event-stream by default to stabilize streaming
|
||
// - Scrubs sensitive headers from logs; logs are structured JSON
|
||
// - Reuses connections and sets generous timeouts for SSE
|
||
//
|
||
// Usage (local):
|
||
// # 1) Export your real API key for the proxy process
|
||
// export OPENAI_API_KEY="sk-..."
|
||
// # 2) Start the proxy (default port 5055)
|
||
// node scripts/openai-proxy.js
|
||
// # or choose a port / upstream base
|
||
// PORT=5055 OPENAI_BASE_URL=https://api.openai.com/v1 node scripts/openai-proxy.js
|
||
//
|
||
// # 3) Run the local Codex binary with the proxy (dummy key to client)
|
||
// # Build the binary first: ./build-fast.sh
|
||
// OPENAI_API_KEY="x" OPENAI_BASE_URL="http://127.0.0.1:${PORT:-5055}/v1" \
|
||
// ./code-rs/target/dev-fast/code llm request \
|
||
// --developer "Say 'pong' as plain text" \
|
||
// --message "ping" \
|
||
// --format-type json_schema \
|
||
// --schema-json '{"type":"object","properties":{},"additionalProperties":false}' \
|
||
// --model gpt-4o-mini
|
||
//
|
||
// # Or run your usual `code` subcommand (Exec/TUI) with the same env vars.
|
||
|
||
const http = require("http");
|
||
const https = require("https");
|
||
const crypto = require("crypto");
|
||
const { URL } = require("url");
|
||
|
||
const PORT = Number(process.env.PORT || 5055);
|
||
const API_KEY = process.env.OPENAI_API_KEY || "";
|
||
const UPSTREAM = new URL(
|
||
process.env.OPENAI_BASE_URL || "https://api.openai.com/v1",
|
||
);
|
||
const ALLOWED = ["/v1/chat/completions", "/v1/responses"];
|
||
|
||
// CI options:
|
||
// EXIT_ON_5XX=1 -> Exit process non‑zero if a 5xx response head is seen
|
||
// READY_FILE=path -> Touch this file when the server is listening
|
||
// LOG_DEST=stdout|stderr (default stdout)
|
||
// Debug toggles:
|
||
// DISABLE_KEEPALIVE=1 -> Do not reuse upstream connections
|
||
// FORCE_CLOSE=1 -> Send 'Connection: close' to upstream
|
||
// NO_GZIP=1 -> Set 'accept-encoding: identity'
|
||
// STRIP_SESSION_ID=1 -> Remove 'session_id' from upstream request headers
|
||
// IDEMPOTENCY_KEY=auto -> Add an Idempotency-Key header per request (uuid)
|
||
// LOG_ERROR_BODY=1 -> Log non-2xx response body (truncated)
|
||
// LOG_ERROR_BODY_BYTES=1024 -> Max bytes to log from error body
|
||
// STRICT_HEADERS=1 -> Rebuild upstream headers from a minimal allowlist
|
||
// RESPONSES_BETA="responses=v1" (override beta header)
|
||
const EXIT_ON_5XX = process.env.EXIT_ON_5XX === "1" || false;
|
||
const READY_FILE = process.env.READY_FILE || "";
|
||
const LOG_DEST = (process.env.LOG_DEST || "stdout").toLowerCase();
|
||
const DISABLE_KEEPALIVE = process.env.DISABLE_KEEPALIVE === "1" || false;
|
||
const FORCE_CLOSE = process.env.FORCE_CLOSE === "1" || false;
|
||
const NO_GZIP = process.env.NO_GZIP === "1" || false;
|
||
const STRIP_SESSION_ID = process.env.STRIP_SESSION_ID === "1" || false;
|
||
const IDEMPOTENCY_KEY = (process.env.IDEMPOTENCY_KEY || "").toLowerCase();
|
||
const LOG_ERROR_BODY = process.env.LOG_ERROR_BODY === "1" || false;
|
||
const LOG_ERROR_BODY_BYTES = Number(process.env.LOG_ERROR_BODY_BYTES || 2048);
|
||
const STRICT_HEADERS = process.env.STRICT_HEADERS === "1" || false;
|
||
const RESPONSES_BETA = process.env.RESPONSES_BETA || "responses=v1";
|
||
|
||
function outWrite(s) {
|
||
if (LOG_DEST === "stderr") process.stderr.write(s + "\n");
|
||
else process.stdout.write(s + "\n");
|
||
}
|
||
|
||
if (!API_KEY) {
|
||
console.error("[fatal] OPENAI_API_KEY missing");
|
||
process.exit(1);
|
||
}
|
||
|
||
function redactHeaders(h) {
|
||
const out = {};
|
||
for (const [k, v] of Object.entries(h || {})) {
|
||
const key = k.toLowerCase();
|
||
if (
|
||
key === "authorization" ||
|
||
key === "proxy-authorization" ||
|
||
key === "cookie" ||
|
||
key === "set-cookie"
|
||
) {
|
||
out[key] = "REDACTED";
|
||
} else {
|
||
out[key] = Array.isArray(v) ? v.map(() => "<omitted>") : String(v);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
function log(ev) {
|
||
try {
|
||
outWrite(JSON.stringify({ ts: new Date().toISOString(), ...ev }));
|
||
} catch {}
|
||
}
|
||
|
||
const server = http.createServer((req, res) => {
|
||
const rid = crypto.randomUUID();
|
||
if (!ALLOWED.some((p) => req.url.startsWith(p))) {
|
||
res.writeHead(403, { "content-type": "application/json" });
|
||
res.end(JSON.stringify({ error: "blocked", path: req.url }));
|
||
return;
|
||
}
|
||
|
||
const chunks = [];
|
||
let size = 0;
|
||
req.on("data", (c) => {
|
||
chunks.push(c);
|
||
size += c.length;
|
||
if (size > 2 * 1024 * 1024) {
|
||
req.destroy(new Error("body too large"));
|
||
}
|
||
});
|
||
req.on("error", (err) => {
|
||
log({ level: "error", rid, msg: "client request error", err: String(err) });
|
||
if (!res.headersSent)
|
||
res.writeHead(400, { "content-type": "application/json" });
|
||
res.end(JSON.stringify({ error: "bad_request" }));
|
||
});
|
||
req.on("end", () => {
|
||
const body = Buffer.concat(chunks);
|
||
const up = new URL(req.url, UPSTREAM);
|
||
let incoming;
|
||
if (STRICT_HEADERS) {
|
||
incoming = {};
|
||
incoming["authorization"] = `Bearer ${API_KEY}`;
|
||
incoming["content-type"] = "application/json";
|
||
incoming["accept"] = "text/event-stream";
|
||
if (NO_GZIP) incoming["accept-encoding"] = "identity";
|
||
if (up.pathname.startsWith("/v1/responses"))
|
||
incoming["openai-beta"] = RESPONSES_BETA;
|
||
incoming["user-agent"] = "code-proxy/1.0";
|
||
incoming["originator"] = "codex_cli_rs";
|
||
if (IDEMPOTENCY_KEY === "auto")
|
||
incoming["idempotency-key"] = crypto.randomUUID();
|
||
} else {
|
||
incoming = { ...req.headers };
|
||
if (!incoming["content-type"])
|
||
incoming["content-type"] = "application/json";
|
||
if (!incoming["accept"]) incoming["accept"] = "text/event-stream";
|
||
if (NO_GZIP) incoming["accept-encoding"] = "identity";
|
||
incoming["authorization"] = `Bearer ${API_KEY}`; // replace with real key
|
||
if (up.pathname.startsWith("/v1/responses")) {
|
||
if (!incoming["openai-beta"]) incoming["openai-beta"] = RESPONSES_BETA;
|
||
}
|
||
if (!incoming["originator"]) incoming["originator"] = "codex_cli_rs";
|
||
if (STRIP_SESSION_ID && "session_id" in incoming)
|
||
delete incoming["session_id"];
|
||
if (IDEMPOTENCY_KEY === "auto")
|
||
incoming["idempotency-key"] = crypto.randomUUID();
|
||
}
|
||
delete incoming["host"];
|
||
incoming["host"] = up.host;
|
||
incoming["connection"] = FORCE_CLOSE ? "close" : "keep-alive";
|
||
if (incoming["content-length"])
|
||
incoming["content-length"] = String(body.length);
|
||
delete incoming["proxy-connection"];
|
||
delete incoming["proxy-authorization"];
|
||
|
||
log({
|
||
level: "info",
|
||
rid,
|
||
phase: "request",
|
||
method: req.method,
|
||
url: up.toString(),
|
||
headers: redactHeaders(incoming),
|
||
body_bytes: body.length,
|
||
});
|
||
|
||
const opts = {
|
||
protocol: up.protocol,
|
||
hostname: up.hostname,
|
||
port: up.port || (up.protocol === "https:" ? 443 : 80),
|
||
path: up.pathname + up.search,
|
||
method: req.method,
|
||
headers: incoming,
|
||
servername: up.hostname,
|
||
agent:
|
||
up.protocol === "https:"
|
||
? new https.Agent({
|
||
keepAlive: !DISABLE_KEEPALIVE,
|
||
maxSockets: 64,
|
||
servername: up.hostname,
|
||
})
|
||
: new http.Agent({ keepAlive: !DISABLE_KEEPALIVE, maxSockets: 64 }),
|
||
};
|
||
|
||
const upstream = (up.protocol === "https:" ? https : http).request(
|
||
opts,
|
||
(upr) => {
|
||
const resHeaders = { ...upr.headers };
|
||
res.writeHead(upr.statusCode || 500, resHeaders);
|
||
log({
|
||
level: "info",
|
||
rid,
|
||
phase: "response_head",
|
||
status: upr.statusCode,
|
||
headers: redactHeaders(resHeaders),
|
||
});
|
||
if (EXIT_ON_5XX && upr.statusCode && upr.statusCode >= 500) {
|
||
// Propagate response then schedule exit(2) once stream completes
|
||
upr.on("end", () => {
|
||
try {
|
||
process.exitCode = 2;
|
||
} catch {}
|
||
});
|
||
}
|
||
let total = 0;
|
||
const chunks = [];
|
||
upr.on("data", (chunk) => {
|
||
total += chunk.length;
|
||
if (LOG_ERROR_BODY && upr.statusCode && upr.statusCode >= 400) {
|
||
if (chunks.length < 32 && total <= LOG_ERROR_BODY_BYTES)
|
||
chunks.push(chunk);
|
||
}
|
||
});
|
||
upr.on("end", () => {
|
||
if (LOG_ERROR_BODY && upr.statusCode && upr.statusCode >= 400) {
|
||
let bodyStr = "";
|
||
try {
|
||
bodyStr = Buffer.concat(chunks).toString("utf8");
|
||
} catch {}
|
||
log({
|
||
level: "error",
|
||
rid,
|
||
phase: "response_error_body",
|
||
status: upr.statusCode,
|
||
body: bodyStr.slice(0, LOG_ERROR_BODY_BYTES),
|
||
});
|
||
}
|
||
log({
|
||
level: "info",
|
||
rid,
|
||
phase: "response_end",
|
||
status: upr.statusCode,
|
||
bytes: total,
|
||
request_id: resHeaders["x-request-id"] || null,
|
||
});
|
||
});
|
||
upr.on("error", (e) => {
|
||
log({ level: "error", rid, phase: "response_error", err: String(e) });
|
||
});
|
||
upr.pipe(res);
|
||
},
|
||
);
|
||
upstream.setTimeout(600000, () => {
|
||
upstream.destroy(new Error("upstream timeout"));
|
||
});
|
||
upstream.on("error", (e) => {
|
||
log({ level: "error", rid, phase: "upstream_error", err: String(e) });
|
||
if (!res.headersSent)
|
||
res.writeHead(502, { "content-type": "application/json" });
|
||
res.end(JSON.stringify({ error: "upstream_error" }));
|
||
});
|
||
upstream.end(body);
|
||
});
|
||
});
|
||
server.headersTimeout = 650000;
|
||
server.keepAliveTimeout = 650000;
|
||
server.listen(PORT, "127.0.0.1", () => {
|
||
log({ level: "info", msg: "proxy listening", addr: "127.0.0.1", port: PORT });
|
||
if (READY_FILE) {
|
||
try {
|
||
require("fs").writeFileSync(READY_FILE, String(Date.now()));
|
||
} catch {}
|
||
}
|
||
});
|