Spaces:
Runtime error
Runtime error
File size: 6,534 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | import { spawn } from "node:child_process";
import { t } from "../i18n.mjs";
import { resolveActiveContext } from "../contexts.mjs";
/** OpenAI/Codex env keys stripped from the child so a stale OpenAI key/base-url
* in the shell can't shadow the omniroute provider (defense-in-depth). Mirrors
* free-claude-code's codex adapter. NOTE: this does NOT silence codex's
* `refresh_token` log noise — that comes from a stored OpenAI session in
* ~/.codex/auth.json, not the env; it is cosmetic and does not block requests. */
const STRIPPED_CODEX_ENV_KEYS = [
"OPENAI_API_KEY",
"OPENAI_BASE_URL",
"OPENAI_API_BASE",
"OPENAI_ORG_ID",
"OPENAI_ORGANIZATION",
"CODEX_API_KEY",
];
/** Placeholder so codex's `env_key` is always satisfied when the backend is open. */
const NO_AUTH_SENTINEL = "omniroute-no-auth";
function stripTrailingSlash(value) {
let s = String(value);
let end = s.length;
while (end > 0 && s.charCodeAt(end - 1) === 47) end--;
return end === s.length ? s : s.slice(0, end);
}
/** TOML assignment for a `-c key=value` codex flag (strings get quoted). */
function tomlAssign(key, value) {
if (typeof value === "boolean" || typeof value === "number") return `${key}=${value}`;
return `${key}=${JSON.stringify(String(value))}`;
}
/**
* Resolve the OmniRoute root base URL + auth for codex, honouring (in order):
* explicit flags → active context (remote mode) → localhost:<port>.
* @returns {{ baseUrl:string, authToken:string|undefined }}
*/
export function resolveCodexTarget(opts = {}) {
const explicit = opts.remote ?? opts.baseUrl;
let baseUrl;
if (explicit) {
baseUrl = stripTrailingSlash(explicit).replace(/\/v1$/, "");
} else {
let fromCtx;
try {
fromCtx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl;
} catch {
/* no context */
}
baseUrl = fromCtx
? stripTrailingSlash(fromCtx).replace(/\/v1$/, "")
: `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
let authToken = opts.apiKey ?? opts["api-key"];
if (!authToken) {
try {
const ctx = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
authToken = ctx?.accessToken || ctx?.apiKey || undefined;
} catch {
/* no context auth */
}
}
if (!authToken) authToken = process.env.OMNIROUTE_API_KEY;
return { baseUrl, authToken };
}
/** Health-check an OmniRoute root URL before launching Codex. */
async function healthCheck(baseUrl, timeoutMs = 3000) {
try {
const res = await fetch(`${baseUrl}/api/monitoring/health`, {
signal: AbortSignal.timeout(timeoutMs),
});
return res.ok;
} catch {
return false;
}
}
/**
* Build the env for the Codex child: strip stale OpenAI/Codex creds, then set
* OMNIROUTE_API_KEY (the provider env_key) to the resolved token or a sentinel.
* @param {Record<string,string>} baseEnv
* @param {string|undefined} authToken
* @returns {Record<string,string>}
*/
export function buildCodexEnv(baseEnv, authToken) {
const env = { ...baseEnv };
for (const key of STRIPPED_CODEX_ENV_KEYS) delete env[key];
env.OMNIROUTE_API_KEY = (authToken && String(authToken).trim()) || NO_AUTH_SENTINEL;
return env;
}
/**
* Codex `-c` flags that define the `omniroute` provider inline, so launch works
* WITHOUT a pre-existing ~/.codex/config.toml. Mirrors free-claude-code.
* @param {string} baseUrl OmniRoute root URL (no /v1)
* @returns {string[]}
*/
export function buildCodexProviderArgs(baseUrl) {
return [
"-c",
tomlAssign("model_provider", "omniroute"),
"-c",
tomlAssign("model_providers.omniroute.name", "OmniRoute"),
"-c",
tomlAssign("model_providers.omniroute.base_url", `${baseUrl}/v1`),
"-c",
tomlAssign("model_providers.omniroute.env_key", "OMNIROUTE_API_KEY"),
"-c",
tomlAssign("model_providers.omniroute.wire_api", "responses"),
"-c",
tomlAssign("model_providers.omniroute.requires_openai_auth", false),
];
}
/**
* @param {{port?:string, remote?:string, profile?:string, apiKey?:string}} opts
* @param {string[]} codexArgs pass-through args for the codex binary
* @returns {Promise<number>} exit code
*/
export async function runLaunchCodexCommand(opts = {}, codexArgs = []) {
const { baseUrl, authToken } = resolveCodexTarget(opts);
if (!(await healthCheck(baseUrl))) {
console.error(
(t("launch.notRunning") || "OmniRoute is not reachable at {port}. Start it with 'omniroute serve'.").replace(
"{port}",
baseUrl
)
);
return 1;
}
// Provider injected via -c (works without config.toml); then the profile (model),
// then the user's pass-through args.
const providerArgs = buildCodexProviderArgs(baseUrl);
const profileArgs = opts.profile ? ["--profile", opts.profile] : [];
const extraArgs = [...providerArgs, ...profileArgs, ...codexArgs];
const env = buildCodexEnv(process.env, authToken);
return await new Promise((resolve) => {
const child = spawn("codex", extraArgs, { env, stdio: "inherit" });
child.on("error", (err) => {
if (err?.code === "ENOENT") {
console.error(
"The 'codex' CLI was not found in PATH. Install with:\n npm install -g @openai/codex"
);
resolve(127);
} else {
console.error(String(err?.message || err));
resolve(1);
}
});
child.on("exit", (code) => resolve(code ?? 0));
});
}
export function registerLaunchCodex(program) {
program
.command("launch-codex")
.description(
t("launchCodex.description") || "Launch Codex CLI pointed at OmniRoute (local or remote VPS)"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option("--remote <url>", "Remote OmniRoute base URL, e.g. http://192.168.0.15:20128 (overrides --port + context)")
.option("--profile <name>", "Codex profile to activate (passed as --profile <name>)")
.option("-p, --p <name>", "Alias for --profile")
.option("--api-key <key>", "OmniRoute API key (overrides OMNIROUTE_API_KEY env var for this invocation)")
.allowUnknownOption(true)
.allowExcessArguments(true)
.argument("[codexArgs...]", "arguments passed through to the codex binary")
.action(async (codexArgs, opts) => {
const merged = { ...opts, profile: opts.profile ?? opts.p };
const exitCode = await runLaunchCodexCommand(merged, codexArgs ?? []);
if (exitCode !== 0) process.exit(exitCode);
});
}
|