AI_PROJECT / src /scripts /codex-local-proxy.mjs
chenchenaoyang's picture
Deploy from Codex on 2026-04-08
4b8c220 verified
#!/usr/bin/env node
import { spawn } from "node:child_process";
import http from "node:http";
import { Readable } from "node:stream";
import path from "node:path";
import { CodexAccountPool, classifyFailure } from "../proxy/codex-account-pool.mjs";
import { sanitizeForLogs } from "../shared/secret-sanitizer.mjs";
const DEFAULT_HOST = "127.0.0.1";
const DEFAULT_PORT = 8787;
const DEFAULT_TOKENS_DIR = path.resolve(process.cwd(), "acc_pool");
const DEFAULT_UPSTREAM_BASE = "https://chatgpt.com/backend-api/codex";
const DEFAULT_REFRESH_ENDPOINT = "https://auth.openai.com/oauth/token";
const DEFAULT_CLIENT_VERSION = "0.117.0";
const DEFAULT_PROBE_URL = `${DEFAULT_UPSTREAM_BASE}/models?client_version=${DEFAULT_CLIENT_VERSION}`;
const DEFAULT_LOCAL_API_KEY = "local-acc-pool-proxy-key";
const DEFAULT_MAX_SWITCH_ATTEMPTS = 3;
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
const SUPPORTED_PATHS = new Set([
"/models",
"/responses",
"/v1/models",
"/v1/responses",
"/v1/chat/completions",
]);
function parseArgs(argv) {
const args = {};
for (const part of argv) {
if (!part.startsWith("--")) continue;
const raw = part.slice(2);
const idx = raw.indexOf("=");
if (idx < 0) {
args[raw] = "true";
continue;
}
args[raw.slice(0, idx)] = raw.slice(idx + 1);
}
return args;
}
function printUsage() {
console.log(`Usage:
node src/scripts/codex-local-proxy.mjs
Options:
--host=127.0.0.1
--port=8787
--tokens-dir=acc_pool
--upstream-base=https://chatgpt.com/backend-api/codex
--refresh-endpoint=https://auth.openai.com/oauth/token
--probe-url=https://chatgpt.com/backend-api/codex/models?client_version=0.117.0
--local-api-key=local-acc-pool-proxy-key
--max-switch-attempts=3
--request-timeout-ms=60000
--proxy-url=http://127.0.0.1:8118
--help
`);
}
async function maybeRespawnWithProxy(argv) {
const args = parseArgs(argv);
const proxyUrl = args["proxy-url"] || "";
if (!proxyUrl || process.env.CODEX_PROXY_BOOTSTRAPPED === "1") {
return false;
}
const childEnv = {
...process.env,
CODEX_PROXY_BOOTSTRAPPED: "1",
NODE_USE_ENV_PROXY: "1",
HTTPS_PROXY: proxyUrl,
HTTP_PROXY: proxyUrl,
ALL_PROXY: proxyUrl,
};
const child = spawn(process.execPath, [process.argv[1], ...argv], {
stdio: "inherit",
env: childEnv,
});
await new Promise((resolve, reject) => {
child.on("exit", (code) => {
process.exitCode = code ?? 1;
resolve();
});
child.on("error", reject);
});
return true;
}
function safeJson(value) {
return JSON.stringify(value, null, 2);
}
function getRequestPath(url = "/") {
try {
const parsed = new URL(url, "http://localhost");
return parsed.pathname;
} catch {
return "/";
}
}
function resolveUpstreamUrl(reqUrl, requestPath, options) {
const upstreamBase = options.upstreamBase.replace(/\/+$/, "");
const parsed = new URL(reqUrl || "/", "http://localhost");
if (upstreamBase.includes("chatgpt.com/backend-api/codex")) {
if (requestPath === "/models" || requestPath === "/v1/models") {
return `${upstreamBase}/models?client_version=${encodeURIComponent(
options.clientVersion,
)}`;
}
if (requestPath === "/responses" || requestPath === "/v1/responses") {
return `${upstreamBase}/responses`;
}
}
return `${upstreamBase}${parsed.pathname}${parsed.search}`;
}
function copyHeadersForUpstream(reqHeaders, token) {
const headers = {};
for (const [key, value] of Object.entries(reqHeaders || {})) {
if (value == null) continue;
const lower = key.toLowerCase();
if (["host", "authorization", "content-length", "connection"].includes(lower)) {
continue;
}
headers[key] = value;
}
headers.authorization = `Bearer ${token}`;
return headers;
}
function copyHeadersToClient(res, upstreamHeaders) {
for (const [key, value] of upstreamHeaders.entries()) {
if (key.toLowerCase() === "transfer-encoding") continue;
res.setHeader(key, value);
}
}
function readRequestBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("error", reject);
req.on("end", () => resolve(Buffer.concat(chunks)));
});
}
function requireLocalAuth(req, expectedKey) {
const auth = String(req.headers.authorization || "");
const expected = `Bearer ${expectedKey}`;
return auth === expected;
}
function classifyRetryableFailure(status, detail) {
const result = classifyFailure({ status, detail });
const retryable = ["auth", "rate_limit", "quota", "server", "network"].includes(
result.category,
);
return { ...result, retryable };
}
export function accountSummary(account) {
if (!account) return null;
return {
id: account.id,
email: account.email,
accountId: account.accountId,
healthy: account.healthy,
cooldownUntil: account.cooldownUntilMs ? new Date(account.cooldownUntilMs).toISOString() : null,
lastFailureReason: account.lastFailureReason,
lastValidation: account.lastValidation,
};
}
function createConsoleStartupLogger() {
return (event, payload = {}) => {
const time = new Date().toISOString();
const sanitized = sanitizeForLogs(payload);
const details = Object.entries(sanitized)
.filter(([, value]) => value !== undefined && value !== null && value !== "")
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
.join(" ");
console.log(`[startup] ${time} ${event}${details ? ` ${details}` : ""}`);
};
}
function unauthorized(res) {
res.statusCode = 401;
res.setHeader("content-type", "application/json");
res.end(safeJson({ error: { message: "Unauthorized local proxy key." } }));
}
export async function createProxyService(options) {
const startupLogger = typeof options.logger === "function" ? options.logger : createConsoleStartupLogger();
const fetchFn = options.fetchFn || (await createFetchWithProxy(options.proxyUrl));
const pool = new CodexAccountPool({
tokensDir: options.tokensDir,
refreshEndpoint: options.refreshEndpoint,
probeUrl: options.probeUrl,
fetchFn,
logger: startupLogger,
loadSnapshot: options.loadSnapshot,
saveSnapshot: options.saveSnapshot,
sourcePath: options.sourcePath,
});
startupLogger("pool:load:start", { tokensDir: options.tokensDir });
await pool.load();
startupLogger("pool:load:done", { count: pool.listAccounts().length });
startupLogger("pool:initial-account:start");
const active = await pool.getInitialAccount();
if (!active) {
const snapshot = pool.listAccounts().map((account) => ({
id: account.id,
email: account.email,
accountId: account.accountId,
cooldownUntil: account.cooldownUntilMs
? new Date(account.cooldownUntilMs).toISOString()
: null,
lastFailureReason: account.lastFailureReason || "(none)",
hasRefreshToken: Boolean(account.refreshToken),
}));
throw new Error(
`No usable account after initial probe. Accounts: ${JSON.stringify(snapshot)}`,
);
}
startupLogger("pool:initial-account:done", {
id: active.id,
accountId: active.accountId,
});
async function reload() {
await pool.load();
const nextActive = await pool.getInitialAccount();
if (!nextActive) {
throw new Error("No usable account after reload.");
}
return nextActive;
}
function getAdminStatus() {
return {
active: accountSummary(pool.getActiveAccount()),
accounts: pool.listAccounts().map(accountSummary),
};
}
async function handleRequest(
req,
res,
{ requestUrl = req.url, exposeHealthDetails = true, exposeStatus = true } = {},
) {
const requestPath = getRequestPath(requestUrl);
if (requestPath === "/healthz") {
res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.end(
safeJson(
exposeHealthDetails
? { ok: true, active: accountSummary(pool.getActiveAccount()) }
: { ok: true },
),
);
return;
}
if (requestPath === "/proxy/status" && exposeStatus) {
res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.end(safeJson(getAdminStatus()));
return;
}
if (!requireLocalAuth(req, options.localApiKey)) {
unauthorized(res);
return;
}
if (!SUPPORTED_PATHS.has(requestPath)) {
res.statusCode = 404;
res.setHeader("content-type", "application/json");
res.end(
safeJson({
error: {
message: `Unsupported path: ${requestPath}. Supported: ${[
...SUPPORTED_PATHS,
].join(", ")}`,
},
}),
);
return;
}
const requestBody =
req.method === "GET" || req.method === "HEAD" ? null : await readRequestBody(req);
const excluded = new Set();
const maxAttempts = Math.max(1, Number(options.maxSwitchAttempts) + 1);
let lastFailure = null;
let succeeded = false;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const current =
attempt === 0 ? pool.getActiveAccount() || (await pool.getInitialAccount()) : pool.pickNextHealthyAccount(excluded);
if (!current) {
break;
}
excluded.add(current.id);
if (pool.isCoolingDown(current)) {
continue;
}
try {
if (pool.needsRefresh(current)) {
await pool.refreshAccount(current);
}
if (
options.upstreamBase.includes("chatgpt.com/backend-api/codex") &&
requestPath === "/v1/chat/completions"
) {
res.statusCode = 501;
res.setHeader("content-type", "application/json");
res.end(
safeJson({
error: {
message:
"ChatGPT/Codex upstream mode currently supports /v1/models and /v1/responses only.",
},
}),
);
return;
}
const upstreamUrl = resolveUpstreamUrl(requestUrl, requestPath, options);
const upstreamHeaders = copyHeadersForUpstream(req.headers, current.accessToken);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), options.requestTimeoutMs);
let upstream;
try {
upstream = await fetchFn(upstreamUrl, {
method: req.method,
headers: upstreamHeaders,
body: requestBody,
signal: controller.signal,
});
} finally {
clearTimeout(timeout);
}
if (!upstream.ok) {
const detail = await upstream.text();
const classified = classifyRetryableFailure(upstream.status, detail);
pool.markFailure(current, classified.category, detail || classified.reason);
lastFailure = {
status: upstream.status,
category: classified.category,
reason: detail || classified.reason,
};
if (classified.retryable) {
continue;
}
res.statusCode = upstream.status;
res.setHeader("content-type", "application/json");
res.end(
safeJson({
error: {
message: detail || "Upstream request failed.",
category: classified.category,
},
}),
);
return;
}
pool.markSuccess(current);
succeeded = true;
res.statusCode = upstream.status;
copyHeadersToClient(res, upstream.headers);
if (!upstream.body) {
res.end();
return;
}
Readable.fromWeb(upstream.body).pipe(res);
return;
} catch (error) {
const detail = error?.message || String(error);
const classified = classifyRetryableFailure(0, detail);
pool.markFailure(current, classified.category, detail);
lastFailure = {
status: 0,
category: classified.category,
reason: detail,
};
continue;
}
}
if (!succeeded) {
res.statusCode = 503;
res.setHeader("content-type", "application/json");
res.end(
safeJson({
error: {
message: "No healthy account available.",
lastFailure,
},
}),
);
}
}
return {
pool,
handleRequest,
reload,
getAdminStatus,
};
}
export async function createProxyServer(options) {
const service = await createProxyService(options);
const server = http.createServer((req, res) =>
service.handleRequest(req, res, {
exposeHealthDetails: true,
exposeStatus: true,
}),
);
return { server, pool: service.pool, service };
}
async function createFetchWithProxy(proxyUrl) {
if (!proxyUrl) {
return fetch;
}
process.env.NODE_USE_ENV_PROXY = "1";
process.env.HTTPS_PROXY = proxyUrl;
process.env.HTTP_PROXY = proxyUrl;
process.env.ALL_PROXY = proxyUrl;
return fetch;
}
async function verifyStartupAccounts(pool) {
const reports = [];
for (const account of pool.listAccounts()) {
if (pool.isCoolingDown(account)) {
reports.push({
id: account.id,
ok: false,
stage: "cooldown",
reason: account.lastFailureReason || "cooldown",
});
continue;
}
try {
const result = await pool.ensureAccountHealthy(account);
reports.push({
id: account.id,
ok: Boolean(result?.ok),
stage: "probe",
reason: result?.ok
? "ok"
: `${result?.category || "unknown"}:${result?.detail || result?.reason || "failed"}`,
});
} catch (error) {
reports.push({
id: account.id,
ok: false,
stage: "exception",
reason: error?.message || String(error),
});
}
}
return reports;
}
async function main() {
if (await maybeRespawnWithProxy(process.argv.slice(2))) {
return;
}
const args = parseArgs(process.argv.slice(2));
if (args.help === "true") {
printUsage();
return;
}
const envProxy =
process.env.CODEX_PROXY_UPSTREAM_PROXY ||
process.env.HTTPS_PROXY ||
process.env.HTTP_PROXY ||
"";
const options = {
host: args.host || process.env.CODEX_PROXY_HOST || DEFAULT_HOST,
port: Number(args.port || process.env.CODEX_PROXY_PORT || DEFAULT_PORT),
tokensDir: path.resolve(args["tokens-dir"] || process.env.CODEX_TOKENS_DIR || DEFAULT_TOKENS_DIR),
upstreamBase: args["upstream-base"] || process.env.CODEX_PROXY_UPSTREAM_BASE || DEFAULT_UPSTREAM_BASE,
refreshEndpoint:
args["refresh-endpoint"] || process.env.CODEX_PROXY_REFRESH_ENDPOINT || DEFAULT_REFRESH_ENDPOINT,
probeUrl: args["probe-url"] || process.env.CODEX_PROXY_PROBE_URL || DEFAULT_PROBE_URL,
localApiKey: args["local-api-key"] || process.env.CODEX_PROXY_API_KEY || DEFAULT_LOCAL_API_KEY,
maxSwitchAttempts: Number(
args["max-switch-attempts"] || process.env.CODEX_PROXY_MAX_SWITCH_ATTEMPTS || DEFAULT_MAX_SWITCH_ATTEMPTS,
),
requestTimeoutMs: Number(
args["request-timeout-ms"] || process.env.CODEX_PROXY_REQUEST_TIMEOUT_MS || DEFAULT_REQUEST_TIMEOUT_MS,
),
proxyUrl: args["proxy-url"] || envProxy || "",
clientVersion:
args["client-version"] || process.env.CODEX_PROXY_CLIENT_VERSION || DEFAULT_CLIENT_VERSION,
};
console.log("[startup] preparing local proxy");
console.log(`[startup] host=${options.host} port=${options.port}`);
console.log(`[startup] tokensDir=${options.tokensDir}`);
console.log(`[startup] upstreamBase=${options.upstreamBase}`);
console.log(`[startup] upstreamProxy=${options.proxyUrl || "(none)"}`);
let server;
let pool;
try {
({ server, pool } = await createProxyServer(options));
} catch (error) {
const fetchFn = await createFetchWithProxy(options.proxyUrl);
const bootstrapPool = new CodexAccountPool({
tokensDir: options.tokensDir,
refreshEndpoint: options.refreshEndpoint,
probeUrl: options.probeUrl,
fetchFn,
});
await bootstrapPool.load();
const reports = await verifyStartupAccounts(bootstrapPool);
console.error(error?.message || error);
console.error("Startup diagnostics:");
for (const report of reports) {
console.error(`- ${report.id} [${report.stage}] ${report.reason}`);
}
process.exitCode = 1;
return;
}
server.listen(options.port, options.host, () => {
const active = pool.getActiveAccount();
console.log(`Codex 本地代理已启动:http://${options.host}:${options.port}`);
console.log(`初始活跃账号:${active?.email || active?.id || "(none)"}`);
console.log(`上游代理:${options.proxyUrl || "(none)"}`);
});
}
const directRun = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
if (directRun) {
main().catch((error) => {
console.error(error?.message || error);
process.exitCode = 1;
});
}