ai-v1 / server.js
AB498's picture
.
70a644a
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 {
// 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}`);
});