Files
dev/scripts/openai-proxy.js
T
zeekayandHanzo Dev 6cbbea2d03 merge: just-every/code v0.6.169 (e4fe007150) — keep Hanzo identity
Our history is just-every's with author emails rewritten, so the graphs are
disjoint and git finds no merge base. Supplied one by hand: d1f1f51a41, the
just-every commit matching our Oct-2025 fork point (22 files apart). 9618
upstream commits, 7004 files, +1.81M/-159k, 37 conflicts.

Resolution:
- Upstream code where our side was the Oct-2025 prototype of a feature they
  have since reimplemented 3-5x larger (agent_runs, browser_sessions,
  history_cell/{agent,browser}, the vt100 harness and its snapshots).
- Upstream's config.rs module split (builder/defaults/sources/validation)
  kept; zen5-coder and the provider-follows-model rule re-applied on top.
- Ours where the file IS the identity: README, package.json, postinstall,
  release notes pipeline, integration-zed.
- Hybrid where upstream improved a file we had branded: updates.rs (their
  release-repo migration, our hanzoai/dev), release.yml (their fail-fast npm
  auth check, our package name), chatwidget.rs (their turn handling, our
  HANZO_USER_KEY hint), exec human output (their 237 lines, our banner).

Superseded and dropped deliberately: our resume_command_name() coder-alias
hack — upstream's derive_resume_command_name() reads the invocation basename
generically, which yields "dev" for us.

LLM.md now records the real topology and the sync point; AGENTS.md and
CLAUDE.md are symlinks to it again. Single parent on purpose: adopting
jecode/main as a second parent would re-import 13,742 commits carrying the
upstream author emails a prior rewrite deliberately removed.

Version 0.2.188 -> 0.2.189.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 23:09:12 -07:00

199 lines
9.1 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 {}
}
});