import express from "express"; import { spawn } from "node:child_process"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { tmpdir } from "node:os"; const app = express(); const port = Number(process.env.PORT || 7860); const codexCliCommand = process.env.CODEX_CLI_COMMAND || process.env.FIG2CODE_CLI_COMMAND || "codex"; const defaultCodexModel = process.env.CODEX_MODEL || process.env.FIG2CODE_AI_MODEL || "gpt-5.1-codex-max"; const codexRuntimeHome = process.env.CODEX_RUNTIME_HOME || path.join(process.cwd(), ".codex-home"); app.use(express.json({ limit: "2mb" })); function createHttpError(status, message, details = undefined) { const error = new Error(message); error.status = status; if (details !== undefined) { error.details = details; } return error; } function normalizeMessages(messages) { if (!Array.isArray(messages) || messages.length === 0) { throw createHttpError(400, "Request body must include a non-empty messages array."); } return messages.map((message, index) => { if (!message || typeof message !== "object") { throw createHttpError(400, `messages[${index}] must be an object.`); } const role = typeof message.role === "string" ? message.role.trim() : ""; const content = message.content; if (!role) { throw createHttpError(400, `messages[${index}].role is required.`); } if (typeof content !== "string" && !Array.isArray(content) && content !== null) { throw createHttpError(400, `messages[${index}].content must be a string or array.`); } return { ...message, role }; }); } function normalizeModelName(value) { const normalized = String(value || "").trim(); return normalized || defaultCodexModel; } function buildCodexPrompt(messages) { const lines = ["You are a helpful assistant. Return only the final answer."]; for (const message of messages) { const role = typeof message?.role === "string" ? message.role.trim() : "user"; const content = typeof message?.content === "string" ? message.content.trim() : Array.isArray(message?.content) ? message.content.map((part) => (typeof part === "string" ? part : String(part?.text || ""))).join("\n").trim() : ""; if (content) { lines.push(`${role.toUpperCase()}: ${content}`); } } lines.push("ASSISTANT:"); return lines.join("\n"); } function extractMessageText(text) { const normalized = String(text || "").trim(); if (!normalized) { return ""; } const fencedCodeMatch = normalized.match(/```(?:[a-zA-Z0-9_+-]+)?\r?\n([\s\S]*?)```/); if (fencedCodeMatch?.[1]) { return fencedCodeMatch[1].trim(); } const htmlStartIndex = normalized.toLowerCase().search(/|]/i); if (htmlStartIndex >= 0) { return normalized.slice(htmlStartIndex).trim(); } return normalized; } function extractAuthJsonFromBody(body) { const explicitJsonText = typeof body?.codexAuthJson === "string" ? body.codexAuthJson.trim() : ""; if (explicitJsonText) { try { return JSON.parse(explicitJsonText); } catch { // If it's not JSON text, treat it as already serialized auth JSON content. return explicitJsonText; } } const explicitObject = body?.codexAuth || body?.authJson || body?.auth; if (explicitObject && typeof explicitObject === "object") { return explicitObject; } return null; } function resolveBodyApiKey(bodyAuth) { if (!bodyAuth) { return ""; } if (typeof bodyAuth === "string") { try { const parsed = JSON.parse(bodyAuth); return typeof parsed?.OPENAI_API_KEY === "string" ? parsed.OPENAI_API_KEY.trim() : ""; } catch { return ""; } } if (typeof bodyAuth !== "object") { return ""; } return typeof bodyAuth.OPENAI_API_KEY === "string" ? bodyAuth.OPENAI_API_KEY.trim() : ""; } async function resolveAuthFileContents(bodyAuth) { if (!bodyAuth) { return ""; } if (typeof bodyAuth === "string") { const trimmed = bodyAuth.trim(); if (!trimmed) { return ""; } try { JSON.parse(trimmed); return trimmed; } catch { return trimmed; } } return JSON.stringify(bodyAuth, null, 2); } async function writeCodexAuthToTempHome(authJsonText) { await mkdir(codexRuntimeHome, { recursive: true }); const tempHome = await mkdtemp(path.join(codexRuntimeHome, "session-")); const codexDir = path.join(tempHome, ".codex"); const authPath = path.join(codexDir, "auth.json"); await mkdir(codexDir, { recursive: true }); await writeFile(authPath, `${String(authJsonText || "").trimEnd()}\n`, "utf8"); return { tempHome, authPath }; } async function readLastMessage(lastMessagePath) { try { return await readFile(lastMessagePath, "utf8"); } catch { return ""; } } async function requestCodexCompletion(messages, { model = defaultCodexModel, codexBinary = codexCliCommand, env = process.env, cwd = process.cwd(), spawnImpl = spawn, authJson = null, bodyApiKey = "", } = {}) { const tempHome = authJson ? await writeCodexAuthToTempHome(await resolveAuthFileContents(authJson)) : null; const tempDir = await mkdtemp(path.join(tmpdir(), "hf-codex-")); const lastMessagePath = path.join(tempDir, "last-message.txt"); const prompt = buildCodexPrompt(messages); try { const childEnv = { ...env, ...(bodyApiKey ? { OPENAI_API_KEY: bodyApiKey } : {}), ...(tempHome ? { HOME: tempHome.tempHome } : {}), }; const args = [ "exec", "--output-last-message", lastMessagePath, "--cd", cwd, "--skip-git-repo-check", "--model", normalizeModelName(model), "-", ]; const child = spawnImpl(codexBinary, args, { cwd, env: childEnv, stdio: ["pipe", "pipe", "pipe"], shell: process.platform === "win32", }); let stdout = ""; let stderr = ""; if (child.stdout) { child.stdout.on("data", (chunk) => { stdout += String(chunk); }); } if (child.stderr) { child.stderr.on("data", (chunk) => { stderr += String(chunk); }); } const exitCode = await new Promise((resolve, reject) => { child.on("error", reject); child.on("close", (code, signal) => { if (signal) { reject(new Error(`Codex was terminated by signal ${signal}.`)); return; } resolve(Number.isFinite(Number(code)) ? Number(code) : 1); }); if (child.stdin) { child.stdin.end(prompt); } }); const lastMessage = await readLastMessage(lastMessagePath); const messageText = extractMessageText(lastMessage || stdout || stderr); if (exitCode !== 0) { const suffix = stderr.trim() ? ` Stderr: ${stderr.trim().slice(0, 600)}` : ""; throw new Error(`Codex request failed with exit code ${exitCode}.${suffix}`); } return { data: { exitCode, stdout, stderr, lastMessage, model: normalizeModelName(model), }, messageText, }; } finally { await rm(tempDir, { recursive: true, force: true }).catch(() => {}); if (tempHome?.tempHome) { await rm(tempHome.tempHome, { recursive: true, force: true }).catch(() => {}); } } } app.get("/", (_req, res) => { res.json({ name: "hf-codex-exec-api", status: "ok", routes: ["/healthz", "/v1/chat/completions"], }); }); app.get("/healthz", (_req, res) => { res.json({ ok: true, backend: "codex", model: defaultCodexModel, authMode: "request-body", }); }); app.post("/v1/chat/completions", async (req, res, next) => { try { const messages = normalizeMessages(req.body?.messages); const model = normalizeModelName(req.body?.model); const bodyAuth = extractAuthJsonFromBody(req.body); const bodyApiKey = typeof req.body?.codexApiKey === "string" ? req.body.codexApiKey.trim() : resolveBodyApiKey(bodyAuth); if (!bodyAuth && !bodyApiKey) { throw createHttpError( 400, "Request body must include codexAuthJson/codexAuth or codexApiKey for Codex authentication." ); } const completion = await requestCodexCompletion(messages, { model, authJson: bodyAuth, bodyApiKey, cwd: process.cwd(), env: process.env, spawnImpl: spawn, }); res.json({ id: `codex-${Date.now()}`, object: "chat.completion", created: Math.floor(Date.now() / 1000), model, choices: [ { index: 0, message: { role: "assistant", content: completion.messageText, }, finish_reason: "stop", }, ], usage: null, codex: { stdout: completion.data.stdout, stderr: completion.data.stderr, }, }); } catch (error) { next(error); } }); app.use((error, _req, res, _next) => { const status = Number.isFinite(Number(error?.status)) ? Number(error.status) : 500; res.status(status).json({ error: { message: error?.message || "Unexpected server error.", details: error?.details || null, }, }); }); app.listen(port, "0.0.0.0", () => { console.log(`hf-codex-exec-api listening on port ${port}`); });