2api
feat: configurable stream stall timeout + per-provider UI
88c4c60
Raw
History Blame Contribute Delete
5.76 kB
"use server";
import { NextResponse } from "next/server";
import { exec } from "child_process";
import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
import os from "os";
const execAsync = promisify(exec);
const PROVIDER_NAME = "9router";
const API_KEY_ENV = "OPENAI_API_KEY";
const getHermesDir = () => path.join(os.homedir(), ".hermes");
const getHermesConfigPath = () => path.join(getHermesDir(), "config.yaml");
const getHermesEnvPath = () => path.join(getHermesDir(), ".env");
// Match top-level "model:" block (until next non-indented, non-empty line)
const MODEL_BLOCK_RE = /^model:[ \t]*\r?\n((?:[ \t]+.*\r?\n?|[ \t]*\r?\n)*)/m;
const buildModelBlock = (model, baseUrl) =>
`model:\n default: "${model}"\n provider: "custom"\n base_url: "${baseUrl}"\n`;
// Parse current model block back to fields (best-effort, simple key:value)
const parseModelBlock = (yaml) => {
const match = yaml.match(MODEL_BLOCK_RE);
if (!match) return null;
const body = match[1] || "";
const get = (key) => {
const m = body.match(new RegExp(`^[ \\t]+${key}:[ \\t]*["']?([^"'\\r\\n]+)["']?`, "m"));
return m ? m[1].trim() : null;
};
return {
default: get("default"),
provider: get("provider"),
base_url: get("base_url"),
};
};
const upsertModelBlock = (yaml, newBlock) => {
if (MODEL_BLOCK_RE.test(yaml)) return yaml.replace(MODEL_BLOCK_RE, newBlock);
return yaml.length > 0 ? `${newBlock}\n${yaml}` : newBlock;
};
const removeModelBlock = (yaml) => yaml.replace(MODEL_BLOCK_RE, "").replace(/^\n+/, "");
// .env helpers — upsert/remove single KEY=VALUE line
const upsertEnvVar = (envText, key, value) => {
const re = new RegExp(`^${key}=.*$`, "m");
const line = `${key}=${value}`;
if (re.test(envText)) return envText.replace(re, line);
return envText.length > 0 && !envText.endsWith("\n") ? `${envText}\n${line}\n` : `${envText}${line}\n`;
};
const removeEnvVar = (envText, key) => {
const re = new RegExp(`^${key}=.*\\r?\\n?`, "m");
return envText.replace(re, "");
};
const checkHermesInstalled = async () => {
try {
const isWindows = os.platform() === "win32";
const command = isWindows ? "where hermes" : "which hermes";
await execAsync(command, { windowsHide: true });
return true;
} catch {
try {
await fs.access(getHermesConfigPath());
return true;
} catch {
return false;
}
}
};
const readConfigYaml = async () => {
try {
return await fs.readFile(getHermesConfigPath(), "utf-8");
} catch (error) {
if (error.code === "ENOENT") return "";
throw error;
}
};
const readEnvFile = async () => {
try {
return await fs.readFile(getHermesEnvPath(), "utf-8");
} catch (error) {
if (error.code === "ENOENT") return "";
throw error;
}
};
// Detect 9router by base_url containing localhost/127.0.0.1 or matching tunnel URL
const has9RouterConfig = (modelCfg) => {
if (!modelCfg?.base_url) return false;
return modelCfg.provider === "custom" && /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(modelCfg.base_url);
};
export async function GET() {
try {
const installed = await checkHermesInstalled();
if (!installed) {
return NextResponse.json({ installed: false, settings: null, message: "Hermes Agent is not installed" });
}
const yaml = await readConfigYaml();
const model = parseModelBlock(yaml);
return NextResponse.json({
installed: true,
settings: { model },
has9Router: has9RouterConfig(model),
configPath: getHermesConfigPath(),
});
} catch (error) {
console.log("Error checking hermes settings:", error);
return NextResponse.json({ error: "Failed to check hermes settings" }, { status: 500 });
}
}
export async function POST(request) {
try {
const { baseUrl, apiKey, model } = await request.json();
if (!baseUrl || !model) {
return NextResponse.json({ error: "baseUrl and model are required" }, { status: 400 });
}
const dir = getHermesDir();
await fs.mkdir(dir, { recursive: true });
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
// Update config.yaml — replace/insert model: block, keep everything else
const existingYaml = await readConfigYaml();
const newYaml = upsertModelBlock(existingYaml, buildModelBlock(model, normalizedBaseUrl));
await fs.writeFile(getHermesConfigPath(), newYaml);
// Update .env — upsert OPENAI_API_KEY only when caller provides one
if (apiKey) {
const existingEnv = await readEnvFile();
const newEnv = upsertEnvVar(existingEnv, API_KEY_ENV, apiKey);
await fs.writeFile(getHermesEnvPath(), newEnv);
}
return NextResponse.json({
success: true,
message: "Hermes settings applied successfully!",
configPath: getHermesConfigPath(),
});
} catch (error) {
console.log("Error updating hermes settings:", error);
return NextResponse.json({ error: "Failed to update hermes settings" }, { status: 500 });
}
}
export async function DELETE() {
try {
const configPath = getHermesConfigPath();
let yaml = "";
try {
yaml = await fs.readFile(configPath, "utf-8");
} catch (error) {
if (error.code === "ENOENT") {
return NextResponse.json({ success: true, message: "No config file to reset" });
}
throw error;
}
const newYaml = removeModelBlock(yaml);
await fs.writeFile(configPath, newYaml);
return NextResponse.json({ success: true, message: `${PROVIDER_NAME} model block removed` });
} catch (error) {
console.log("Error resetting hermes settings:", error);
return NextResponse.json({ error: "Failed to reset hermes settings" }, { status: 500 });
}
}