#!/usr/bin/env node import { spawn } from "node:child_process"; import fs from "node:fs"; import { copyFile, readFile, rm } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { DEFAULT_CODEX_CONFIG_PATH, DEFAULT_HEALTH_PATH, DEFAULT_LISTEN_HOST, DEFAULT_LISTEN_PORT, DEFAULT_REASONING_EQUALS, DEFAULT_REASONING_MATCH_MODE, DEFAULT_STATE_ROOT, ensureDirectory, expandEscapedLineBreaks, getCodexProviderContext, getGatewayBaseUrl, getGatewayStatePaths, normalizeIntArray, normalizePhraseArray, normalizeReasoningMatchMode, normalizeStringArray, parseOptions, readJsonFile, setCodexProviderBaseUrl, waitGatewayHealth, writeJsonFile, writeUtf8File, } from "./admin-lib.mjs"; const DEFAULT_PROFILES_DIR = path.join(os.homedir(), ".config", "codex-retry-gateway", "profiles"); const DEFAULT_REQUEST_BODY_LIMIT_BYTES = 1024 * 1024 * 1024; const LEGACY_DEFAULT_REQUEST_BODY_LIMIT_BYTES = 10 * 1024 * 1024; function parseEnvFile(content) { const parsed = {}; for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith("#")) { continue; } const separatorIndex = line.indexOf("="); if (separatorIndex <= 0) { continue; } const key = line.slice(0, separatorIndex).trim(); let value = line.slice(separatorIndex + 1).trim(); if (value.startsWith('"') && value.endsWith('"')) { try { value = JSON.parse(value); } catch { value = value.slice(1, -1); } } else if (value.startsWith("'") && value.endsWith("'")) { value = value.slice(1, -1); } parsed[key] = typeof value === "string" ? expandEscapedLineBreaks(value) : value; } return parsed; } function getProfileName(options) { const positional = options._?.[0]; return `${options.profile || positional || process.env.CODEX_RETRY_GATEWAY_PROFILE || ""}`.trim(); } function resolveDefaultRequestedProfileName(existingState) { const stateProfileName = `${existingState?.profile_name || ""}`.trim(); if (stateProfileName) { return stateProfileName; } return process.env.CODEX_RETRY_GATEWAY_DEFAULT_PROFILE || "default"; } function resolvePreferredProfileName({ requestedProfileName, existingState, preferStateProfile, profilesDir }) { if (!preferStateProfile) { return requestedProfileName; } const stateProfileName = `${existingState?.profile_name || ""}`.trim(); if (!stateProfileName) { return requestedProfileName; } const stateProfilePath = path.join(profilesDir, `${stateProfileName}.env`); if (!fs.existsSync(stateProfilePath)) { return requestedProfileName; } return stateProfileName; } function boolFromEnv(value, fallback = false) { if (value === undefined || value === null || value === "") { return fallback; } return ["1", "true", "yes", "on"].includes(`${value}`.trim().toLowerCase()); } function normalizeRequestBodyLimitBytes(value) { const parsed = Number.parseInt(`${value ?? ""}`, 10); if (!Number.isFinite(parsed) || parsed <= 0) { return DEFAULT_REQUEST_BODY_LIMIT_BYTES; } if (parsed === LEGACY_DEFAULT_REQUEST_BODY_LIMIT_BYTES) { return DEFAULT_REQUEST_BODY_LIMIT_BYTES; } return parsed; } function normalizeAuthMode(value) { const mode = `${value || "passthrough"}`.trim().toLowerCase(); if (["passthrough", "fixed_bearer", "manual_bearer", "auth_json"].includes(mode)) { return mode; } return "passthrough"; } function inferProfileAuthMode(profileEnv) { if (profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_MODE) { return normalizeAuthMode(profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_MODE); } if ( profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_JSON_PATH || profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_JSON_KEY ) { return "auth_json"; } if ( profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_FILE || profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_ENV ) { return "fixed_bearer"; } return "passthrough"; } function buildProfileAuthConfig(profileEnv) { const authMode = inferProfileAuthMode(profileEnv); const authConfig = { upstream_auth_mode: authMode, upstream_auth_env: "", upstream_auth_file: "", upstream_auth_json_path: "", upstream_auth_json_key: "", }; if (authMode === "fixed_bearer") { authConfig.upstream_auth_env = profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_ENV || "CODEX_RETRY_GATEWAY_UPSTREAM_API_KEY"; authConfig.upstream_auth_file = profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_FILE || ""; } else if (authMode === "manual_bearer") { authConfig.upstream_auth_file = profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_FILE || ""; } else if (authMode === "auth_json") { authConfig.upstream_auth_json_path = profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_JSON_PATH || ""; authConfig.upstream_auth_json_key = profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_AUTH_JSON_KEY || "OPENAI_API_KEY"; } return authConfig; } async function loadProfileEnv(profileName, profilesDir) { const profilePath = path.join(profilesDir, `${profileName}.env`); if (!fs.existsSync(profilePath)) { throw new Error(`Profile env file was not found: ${profilePath}`); } const content = await readFile(profilePath, "utf8"); return { profilePath, env: parseEnvFile(content), }; } function buildProfileConfig({ profileName, profileEnv, existingGatewayConfig, providerContext, localGatewayBaseUrl }) { const upstreamBaseUrl = profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_BASE_URL || existingGatewayConfig?.upstream_base_url || (providerContext.currentBaseUrl === localGatewayBaseUrl ? null : providerContext.currentBaseUrl); if (!upstreamBaseUrl) { throw new Error("A real upstream base_url could not be determined for this profile."); } const profileAuthConfig = buildProfileAuthConfig(profileEnv); const reasoningMatchMode = normalizeReasoningMatchMode( profileEnv.CODEX_RETRY_GATEWAY_REASONING_MATCH_MODE || existingGatewayConfig?.reasoning_match_mode || DEFAULT_REASONING_MATCH_MODE, ); return { profile_name: profileName, listen_host: profileEnv.CODEX_RETRY_GATEWAY_LISTEN_HOST || DEFAULT_LISTEN_HOST, listen_port: profileEnv.CODEX_RETRY_GATEWAY_LISTEN_PORT ? Number.parseInt(`${profileEnv.CODEX_RETRY_GATEWAY_LISTEN_PORT}`, 10) : DEFAULT_LISTEN_PORT, upstream_base_url: upstreamBaseUrl, ...profileAuthConfig, request_body_limit_bytes: profileEnv.CODEX_RETRY_GATEWAY_REQUEST_BODY_LIMIT_BYTES ? normalizeRequestBodyLimitBytes(profileEnv.CODEX_RETRY_GATEWAY_REQUEST_BODY_LIMIT_BYTES) : normalizeRequestBodyLimitBytes(existingGatewayConfig?.request_body_limit_bytes), request_history_limit: profileEnv.CODEX_RETRY_GATEWAY_REQUEST_HISTORY_LIMIT ? Number.parseInt(`${profileEnv.CODEX_RETRY_GATEWAY_REQUEST_HISTORY_LIMIT}`, 10) : Number.parseInt(`${existingGatewayConfig?.request_history_limit || 200}`, 10), model_remap: profileEnv.CODEX_RETRY_GATEWAY_MODEL_REMAP || existingGatewayConfig?.model_remap || "", endpoints: normalizeStringArray( profileEnv.CODEX_RETRY_GATEWAY_ENDPOINTS || existingGatewayConfig?.endpoints, ["/responses", "/chat/completions", "/v1/responses", "/v1/chat/completions"], ), reasoning_match_mode: reasoningMatchMode, reasoning_equals: normalizeIntArray( profileEnv.CODEX_RETRY_GATEWAY_REASONING_EQUALS || existingGatewayConfig?.reasoning_equals, DEFAULT_REASONING_EQUALS, ), retryable_status_codes: normalizeIntArray( profileEnv.CODEX_RETRY_GATEWAY_RETRYABLE_STATUS_CODES || existingGatewayConfig?.retryable_status_codes, [429, 503], ), retryable_error_messages: normalizePhraseArray( profileEnv.CODEX_RETRY_GATEWAY_RETRYABLE_ERROR_MESSAGES || existingGatewayConfig?.retryable_error_messages, [ "Selected model is at capacity. Please try a different model.", "stream disconnected before completion: Concurrency limit exceeded for account, please retry later", ], ), management_access_key: profileEnv.CODEX_RETRY_GATEWAY_MANAGEMENT_ACCESS_KEY || existingGatewayConfig?.management_access_key || "", upstream_fetch_retry_attempts: profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_FETCH_RETRY_ATTEMPTS ? Number.parseInt(`${profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_FETCH_RETRY_ATTEMPTS}`, 10) : Number.parseInt(`${existingGatewayConfig?.upstream_fetch_retry_attempts || 5}`, 10), upstream_fetch_retry_backoff_ms: profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_FETCH_RETRY_BACKOFF_MS ? Number.parseInt(`${profileEnv.CODEX_RETRY_GATEWAY_UPSTREAM_FETCH_RETRY_BACKOFF_MS}`, 10) : Number.parseInt(`${existingGatewayConfig?.upstream_fetch_retry_backoff_ms || 350}`, 10), non_stream_status_code: profileEnv.CODEX_RETRY_GATEWAY_NON_STREAM_STATUS_CODE ? Number.parseInt(`${profileEnv.CODEX_RETRY_GATEWAY_NON_STREAM_STATUS_CODE}`, 10) : Number.parseInt(`${existingGatewayConfig?.non_stream_status_code || 502}`, 10), stream_action: profileEnv.CODEX_RETRY_GATEWAY_STREAM_ACTION || existingGatewayConfig?.stream_action || "strict_502", log_match: profileEnv.CODEX_RETRY_GATEWAY_LOG_MATCH === undefined ? existingGatewayConfig?.log_match !== false : boolFromEnv(profileEnv.CODEX_RETRY_GATEWAY_LOG_MATCH, true), health_path: profileEnv.CODEX_RETRY_GATEWAY_HEALTH_PATH || existingGatewayConfig?.health_path || DEFAULT_HEALTH_PATH, }; } async function ensureCodexPointsToGateway({ paths, codexConfigPath, providerContext, localGatewayBaseUrl }) { await ensureDirectory(paths.backupDir); const existingState = await readJsonFile(paths.statePath); let originalBaseUrl = providerContext.currentBaseUrl; if (providerContext.currentBaseUrl === localGatewayBaseUrl) { originalBaseUrl = existingState?.original_base_url || null; } if (!originalBaseUrl || originalBaseUrl === localGatewayBaseUrl) { throw new Error("A restorable original Codex base_url could not be determined."); } const backupPath = existingState?.latest_backup_path || path.join( paths.backupDir, `config-${new Date().toISOString().replace(/[:.]/g, "").replace("T", "-").slice(0, 15)}.toml`, ); if (!existingState?.latest_backup_path) { await copyFile(codexConfigPath, backupPath); } if (providerContext.currentBaseUrl !== localGatewayBaseUrl) { await setCodexProviderBaseUrl({ codexConfigPath, providerName: providerContext.providerName, newBaseUrl: localGatewayBaseUrl, }); } return { existingState, originalBaseUrl, backupPath }; } async function main() { const options = parseOptions(process.argv, { booleanFlags: ["no-codex-config-update", "prefer-state-profile"] }); const profilesDir = options.profilesDir || DEFAULT_PROFILES_DIR; const stateRoot = options.stateRoot || process.env.CODEX_RETRY_GATEWAY_STATE_ROOT || DEFAULT_STATE_ROOT; const codexConfigPath = options.codexConfigPath || process.env.CODEX_RETRY_GATEWAY_CODEX_CONFIG_PATH || DEFAULT_CODEX_CONFIG_PATH; const paths = getGatewayStatePaths(stateRoot); await ensureDirectory(paths.stateRoot); await ensureDirectory(paths.configDir); await ensureDirectory(paths.logDir); await ensureDirectory(paths.backupDir); const existingState = await readJsonFile(paths.statePath); const requestedProfileName = getProfileName(options) || resolveDefaultRequestedProfileName(existingState); const profileName = resolvePreferredProfileName({ requestedProfileName, existingState, preferStateProfile: Boolean(options.preferStateProfile), profilesDir, }); if (profileName !== requestedProfileName) { process.stdout.write( `[run-profile] prefer-state-profile requested=${requestedProfileName} effective=${profileName}\n`, ); } const { profilePath, env: profileEnv } = await loadProfileEnv(profileName, profilesDir); for (const [key, value] of Object.entries(profileEnv)) { process.env[key] = value; } const providerContext = await getCodexProviderContext(codexConfigPath); const listenHost = profileEnv.CODEX_RETRY_GATEWAY_LISTEN_HOST || DEFAULT_LISTEN_HOST; const listenPort = profileEnv.CODEX_RETRY_GATEWAY_LISTEN_PORT ? Number.parseInt(`${profileEnv.CODEX_RETRY_GATEWAY_LISTEN_PORT}`, 10) : DEFAULT_LISTEN_PORT; const localGatewayBaseUrl = getGatewayBaseUrl(listenHost, listenPort); const existingGatewayConfig = await readJsonFile(paths.configPath); const gatewayConfig = buildProfileConfig({ profileName, profileEnv, existingGatewayConfig, providerContext, localGatewayBaseUrl, }); const installState = options.noCodexConfigUpdate ? { existingState, originalBaseUrl: providerContext.currentBaseUrl, backupPath: null, } : await ensureCodexPointsToGateway({ paths, codexConfigPath, providerContext, localGatewayBaseUrl, }); await writeJsonFile(paths.configPath, gatewayConfig); await writeJsonFile(paths.statePath, { ...(installState.existingState || {}), installed_at: installState.existingState?.installed_at || new Date().toISOString(), last_started_at: new Date().toISOString(), profile_name: profileName, profile_env_path: profilePath, codex_config_path: codexConfigPath, provider_name: providerContext.providerName, original_base_url: installState.originalBaseUrl, gateway_base_url: localGatewayBaseUrl, gateway_config_path: paths.configPath, gateway_log_path: paths.logPath, gateway_pid_path: paths.pidPath, latest_backup_path: installState.backupPath || installState.existingState?.latest_backup_path || null, state_root: paths.stateRoot, }); await writeUtf8File(paths.pidPath, `${process.pid}\n`); const gatewayEntry = path.resolve(import.meta.dirname, "..", "gateway.mjs"); const child = spawn(process.execPath, [gatewayEntry, "--config", paths.configPath, "--log", paths.logPath], { cwd: path.resolve(import.meta.dirname, ".."), stdio: "inherit", windowsHide: true, }); let stoppingBySignal = false; const forwardSignal = (signal) => { stoppingBySignal = true; if (!child.killed) { child.kill(signal); } }; process.on("SIGTERM", () => forwardSignal("SIGTERM")); process.on("SIGINT", () => forwardSignal("SIGINT")); child.on("exit", async (code, signal) => { await rm(paths.pidPath, { force: true }).catch(() => {}); if (stoppingBySignal) { process.exit(0); } process.exit(code ?? (signal ? 0 : 1)); }); try { await waitGatewayHealth({ listenHost: gatewayConfig.listen_host, listenPort: gatewayConfig.listen_port, healthPath: gatewayConfig.health_path, }); } catch (error) { if (!child.killed) { child.kill("SIGTERM"); } throw error; } } main().catch((error) => { process.stderr.write(`${error?.stack || error}\n`); process.exit(1); });