| 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(/<!doctype html>|<html[\s>]/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 { |
| |
| 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}`); |
| }); |
|
|