feat(hermes): multi-role model config (delegation + auxiliary slots)
POST /api/cli-tools/hermes-settings now accepts selections:
[{role, model}] and writes model/delegation/auxiliary.<role> blocks
into ~/.hermes/config.yaml non-destructively (comments and other keys
preserved). GET returns the current delegation/auxiliary config,
DELETE removes all 9router-managed blocks. Dashboard card gains a
collapsible Model Roles section aligned with the existing rows; the
bare `model` payload from the CLI quick setup still works.
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,10 @@ import BaseUrlSelect from "./BaseUrlSelect";
|
|||||||
import { rememberEndpoint } from "./cliEndpointPresets";
|
import { rememberEndpoint } from "./cliEndpointPresets";
|
||||||
import ApiKeySelect from "./ApiKeySelect";
|
import ApiKeySelect from "./ApiKeySelect";
|
||||||
import { matchKnownEndpoint } from "./cliEndpointMatch";
|
import { matchKnownEndpoint } from "./cliEndpointMatch";
|
||||||
|
import { CLI_TOOLS } from "@/shared/constants/cliTools";
|
||||||
|
|
||||||
const ENDPOINT = "/api/cli-tools/hermes-settings";
|
const ENDPOINT = "/api/cli-tools/hermes-settings";
|
||||||
|
const HERMES_ROLES = CLI_TOOLS.hermes?.roles || [];
|
||||||
|
|
||||||
export default function HermesToolCard({
|
export default function HermesToolCard({
|
||||||
tool,
|
tool,
|
||||||
@@ -32,6 +34,8 @@ export default function HermesToolCard({
|
|||||||
const [message, setMessage] = useState(null);
|
const [message, setMessage] = useState(null);
|
||||||
const [selectedApiKey, setSelectedApiKey] = useState("");
|
const [selectedApiKey, setSelectedApiKey] = useState("");
|
||||||
const [selectedModel, setSelectedModel] = useState("");
|
const [selectedModel, setSelectedModel] = useState("");
|
||||||
|
const [roleModels, setRoleModels] = useState({});
|
||||||
|
const [modalTarget, setModalTarget] = useState("default");
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [modelAliases, setModelAliases] = useState({});
|
const [modelAliases, setModelAliases] = useState({});
|
||||||
const [showManualConfigModal, setShowManualConfigModal] = useState(false);
|
const [showManualConfigModal, setShowManualConfigModal] = useState(false);
|
||||||
@@ -82,6 +86,12 @@ export default function HermesToolCard({
|
|||||||
hasInitializedModel.current = true;
|
hasInitializedModel.current = true;
|
||||||
const cfg = hermesStatus.settings?.model;
|
const cfg = hermesStatus.settings?.model;
|
||||||
if (cfg?.default) setSelectedModel(cfg.default);
|
if (cfg?.default) setSelectedModel(cfg.default);
|
||||||
|
const initial = {};
|
||||||
|
if (hermesStatus.settings?.delegation?.model) initial.delegation = hermesStatus.settings.delegation.model;
|
||||||
|
for (const [role, rcfg] of Object.entries(hermesStatus.settings?.auxiliary || {})) {
|
||||||
|
if (rcfg?.model) initial[role] = rcfg.model;
|
||||||
|
}
|
||||||
|
setRoleModels(initial);
|
||||||
}
|
}
|
||||||
}, [hermesStatus]);
|
}, [hermesStatus]);
|
||||||
|
|
||||||
@@ -126,7 +136,12 @@ export default function HermesToolCard({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
baseUrl: getEffectiveBaseUrl(),
|
baseUrl: getEffectiveBaseUrl(),
|
||||||
apiKey: keyToUse,
|
apiKey: keyToUse,
|
||||||
model: selectedModel,
|
selections: [
|
||||||
|
{ role: "default", model: selectedModel },
|
||||||
|
...Object.entries(roleModels)
|
||||||
|
.filter(([, model]) => model?.trim())
|
||||||
|
.map(([role, model]) => ({ role, model: model.trim() })),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -154,6 +169,7 @@ export default function HermesToolCard({
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setMessage({ type: "success", text: "Settings reset successfully!" });
|
setMessage({ type: "success", text: "Settings reset successfully!" });
|
||||||
setSelectedModel("");
|
setSelectedModel("");
|
||||||
|
setRoleModels({});
|
||||||
checkStatus();
|
checkStatus();
|
||||||
} else {
|
} else {
|
||||||
setMessage({ type: "error", text: data.error || "Failed to reset settings" });
|
setMessage({ type: "error", text: data.error || "Failed to reset settings" });
|
||||||
@@ -166,16 +182,35 @@ export default function HermesToolCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleModelSelect = (model) => {
|
const handleModelSelect = (model) => {
|
||||||
setSelectedModel(model.value);
|
if (modalTarget === "default") {
|
||||||
|
setSelectedModel(model.value);
|
||||||
|
} else {
|
||||||
|
setRoleModels((prev) => ({ ...prev, [modalTarget]: model.value }));
|
||||||
|
}
|
||||||
setModalOpen(false);
|
setModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openModelModal = (target) => {
|
||||||
|
setModalTarget(target);
|
||||||
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const getManualConfigs = () => {
|
const getManualConfigs = () => {
|
||||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||||
? selectedApiKey
|
? selectedApiKey
|
||||||
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
|
||||||
|
|
||||||
const yamlContent = `model:\n default: "${selectedModel || "provider/model-id"}"\n provider: "custom"\n base_url: "${getEffectiveBaseUrl()}"\n api_key: \${OPENAI_API_KEY}\n`;
|
const base = getEffectiveBaseUrl();
|
||||||
|
let yamlContent = `model:\n default: "${selectedModel || "provider/model-id"}"\n provider: "custom"\n base_url: "${base}"\n api_key: \${OPENAI_API_KEY}\n`;
|
||||||
|
if (roleModels.delegation?.trim()) {
|
||||||
|
yamlContent += `delegation:\n model: "${roleModels.delegation.trim()}"\n provider: "custom"\n base_url: "${base}"\n api_key: \${OPENAI_API_KEY}\n`;
|
||||||
|
}
|
||||||
|
const auxRoles = Object.entries(roleModels).filter(([role, model]) => role !== "delegation" && model?.trim());
|
||||||
|
if (auxRoles.length > 0) {
|
||||||
|
yamlContent += `auxiliary:\n${auxRoles.map(([role, model]) =>
|
||||||
|
` ${role}:\n provider: "custom"\n model: "${model.trim()}"\n base_url: "${base}"\n api_key: \${OPENAI_API_KEY}\n`
|
||||||
|
).join("")}`;
|
||||||
|
}
|
||||||
const envContent = `OPENAI_API_KEY=${keyToUse}\n`;
|
const envContent = `OPENAI_API_KEY=${keyToUse}\n`;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -274,8 +309,49 @@ export default function HermesToolCard({
|
|||||||
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="w-full min-w-0 pl-2 pr-7 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
|
<input type="text" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} placeholder="provider/model-id" className="w-full min-w-0 pl-2 pr-7 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5" />
|
||||||
{selectedModel && <button onClick={() => setSelectedModel("")} className="absolute right-1 top-1/2 -translate-y-1/2 p-0.5 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
|
{selectedModel && <button onClick={() => setSelectedModel("")} className="absolute right-1 top-1/2 -translate-y-1/2 p-0.5 text-text-muted hover:text-red-500 rounded transition-colors" title="Clear"><span className="material-symbols-outlined text-[14px]">close</span></button>}
|
||||||
</div>
|
</div>
|
||||||
<button onClick={() => setModalOpen(true)} disabled={!hasActiveProviders} className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
|
<button onClick={() => openModelModal("default")} disabled={!hasActiveProviders} className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}>Select</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details className="group">
|
||||||
|
<summary className="cursor-pointer select-none text-xs font-semibold text-text-main hover:text-primary transition-colors">
|
||||||
|
<span className="material-symbols-outlined align-middle text-[16px] text-text-muted group-open:rotate-90 transition-transform">chevron_right</span>
|
||||||
|
Model Roles (optional)
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 flex flex-col gap-1.5">
|
||||||
|
{HERMES_ROLES.map((role) => (
|
||||||
|
<div key={role.id} className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
|
||||||
|
<span className="truncate text-xs font-semibold text-text-main sm:text-right sm:text-sm" title={role.label}>{role.label}</span>
|
||||||
|
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
|
||||||
|
<div className="relative w-full min-w-0">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={roleModels[role.id] || ""}
|
||||||
|
onChange={(e) => setRoleModels((prev) => ({ ...prev, [role.id]: e.target.value }))}
|
||||||
|
placeholder="inherit default"
|
||||||
|
className="w-full min-w-0 pl-2 pr-7 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
|
||||||
|
/>
|
||||||
|
{roleModels[role.id] && (
|
||||||
|
<button
|
||||||
|
onClick={() => setRoleModels((prev) => ({ ...prev, [role.id]: "" }))}
|
||||||
|
className="absolute right-1 top-1/2 -translate-y-1/2 p-0.5 text-text-muted hover:text-red-500 rounded transition-colors"
|
||||||
|
title="Clear"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined text-[14px]">close</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => openModelModal(role.id)}
|
||||||
|
disabled={!hasActiveProviders}
|
||||||
|
className={`w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<p className="text-xs text-text-muted">Empty roles inherit the default model.</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{message && (
|
{message && (
|
||||||
@@ -306,10 +382,10 @@ export default function HermesToolCard({
|
|||||||
isOpen={modalOpen}
|
isOpen={modalOpen}
|
||||||
onClose={() => setModalOpen(false)}
|
onClose={() => setModalOpen(false)}
|
||||||
onSelect={handleModelSelect}
|
onSelect={handleModelSelect}
|
||||||
selectedModel={selectedModel}
|
selectedModel={modalTarget === "default" ? selectedModel : roleModels[modalTarget] || ""}
|
||||||
activeProviders={activeProviders}
|
activeProviders={activeProviders}
|
||||||
modelAliases={modelAliases}
|
modelAliases={modelAliases}
|
||||||
title="Select Model for Hermes Agent"
|
title={`Select Model for Hermes Agent${modalTarget !== "default" ? ` — ${HERMES_ROLES.find((r) => r.id === modalTarget)?.label || modalTarget}` : ""}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,20 @@ const getHermesEnvPath = () => path.join(getHermesDir(), ".env");
|
|||||||
|
|
||||||
// Match top-level "model:" block (until next non-indented, non-empty line)
|
// 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 MODEL_BLOCK_RE = /^model:[ \t]*\r?\n((?:[ \t]+.*\r?\n?|[ \t]*\r?\n)*)/m;
|
||||||
|
const DELEGATION_BLOCK_RE = /^delegation:[ \t]*\r?\n((?:[ \t]+.*\r?\n?|[ \t]*\r?\n)*)/m;
|
||||||
|
// "auxiliary:" block; children are 2-space-indented role keys with 4+-space fields
|
||||||
|
const AUX_BLOCK_RE = /^auxiliary:[ \t]*\r?\n((?:(?:[ \t]+.*\r?\n?)|(?:[ \t]*\r?\n))*)/m;
|
||||||
|
const auxRoleRe = (role) => new RegExp(`^ ${role}:[ \\t]*\\r?\\n(?:(?:[ \\t]{4,}.*\\r?\\n?)|(?:[ \\t]*\\r?\\n))*`, "m");
|
||||||
|
|
||||||
const buildModelBlock = (model, baseUrl) =>
|
const buildModelBlock = (model, baseUrl) =>
|
||||||
`model:\n default: "${model}"\n provider: "custom"\n base_url: "${baseUrl}"\n api_key: \${OPENAI_API_KEY}\n`;
|
`model:\n default: "${model}"\n provider: "custom"\n base_url: "${baseUrl}"\n api_key: \${OPENAI_API_KEY}\n`;
|
||||||
|
|
||||||
|
const buildDelegationBlock = (model, baseUrl) =>
|
||||||
|
`delegation:\n model: "${model}"\n provider: "custom"\n base_url: "${baseUrl}"\n api_key: \${OPENAI_API_KEY}\n`;
|
||||||
|
|
||||||
|
const buildAuxRoleBlock = (role, model, baseUrl) =>
|
||||||
|
` ${role}:\n provider: "custom"\n model: "${model}"\n base_url: "${baseUrl}"\n api_key: \${OPENAI_API_KEY}\n`;
|
||||||
|
|
||||||
// Parse current model block back to fields (best-effort, simple key:value)
|
// Parse current model block back to fields (best-effort, simple key:value)
|
||||||
const parseModelBlock = (yaml) => {
|
const parseModelBlock = (yaml) => {
|
||||||
const match = yaml.match(MODEL_BLOCK_RE);
|
const match = yaml.match(MODEL_BLOCK_RE);
|
||||||
@@ -44,6 +54,60 @@ const upsertModelBlock = (yaml, newBlock) => {
|
|||||||
return yaml.length > 0 ? `${newBlock}\n${yaml}` : newBlock;
|
return yaml.length > 0 ? `${newBlock}\n${yaml}` : newBlock;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const upsertDelegationBlock = (yaml, newBlock) => {
|
||||||
|
if (DELEGATION_BLOCK_RE.test(yaml)) return yaml.replace(DELEGATION_BLOCK_RE, newBlock);
|
||||||
|
return yaml.endsWith("\n") || yaml.length === 0 ? `${yaml}${newBlock}` : `${yaml}\n${newBlock}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDelegationBlock = (yaml) => yaml.replace(DELEGATION_BLOCK_RE, "");
|
||||||
|
|
||||||
|
const upsertAuxRole = (yaml, role, roleBlock) => {
|
||||||
|
const re = auxRoleRe(role);
|
||||||
|
const m = yaml.match(AUX_BLOCK_RE);
|
||||||
|
if (!m) {
|
||||||
|
const block = `auxiliary:\n${roleBlock}`;
|
||||||
|
return yaml.endsWith("\n") || yaml.length === 0 ? `${yaml}${block}` : `${yaml}\n${block}`;
|
||||||
|
}
|
||||||
|
const body = re.test(m[1]) ? m[1].replace(re, roleBlock) : `${m[1]}${roleBlock}`;
|
||||||
|
return yaml.replace(AUX_BLOCK_RE, `auxiliary:\n${body}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAuxRole = (yaml, role) => {
|
||||||
|
const m = yaml.match(AUX_BLOCK_RE);
|
||||||
|
if (!m) return yaml;
|
||||||
|
const body = m[1].replace(auxRoleRe(role), "");
|
||||||
|
if (body.trim() === "") return yaml.replace(AUX_BLOCK_RE, "");
|
||||||
|
return yaml.replace(AUX_BLOCK_RE, `auxiliary:\n${body}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// role -> { model, provider, base_url } for every entry under "auxiliary:"
|
||||||
|
const parseAuxRoles = (yaml) => {
|
||||||
|
const m = yaml.match(AUX_BLOCK_RE);
|
||||||
|
if (!m) return {};
|
||||||
|
const roles = {};
|
||||||
|
const subRe = /^ ([A-Za-z0-9_]+):[ \t]*\r?\n((?:(?:[ \t]{4,}.*\r?\n?)|(?:[ \t]*\r?\n))*)/gm;
|
||||||
|
let sm;
|
||||||
|
while ((sm = subRe.exec(m[1]))) {
|
||||||
|
const get = (key) => {
|
||||||
|
const km = sm[2].match(new RegExp(`^[ \\t]+${key}:[ \\t]*["']?([^"'\\r\\n]+)["']?`, "m"));
|
||||||
|
return km ? km[1].trim() : null;
|
||||||
|
};
|
||||||
|
roles[sm[1]] = { model: get("model"), provider: get("provider"), base_url: get("base_url") };
|
||||||
|
}
|
||||||
|
return roles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseDelegationBlock = (yaml) => {
|
||||||
|
const match = yaml.match(DELEGATION_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 { model: get("model"), provider: get("provider"), base_url: get("base_url") };
|
||||||
|
};
|
||||||
|
|
||||||
const removeModelBlock = (yaml) => yaml.replace(MODEL_BLOCK_RE, "").replace(/^\n+/, "");
|
const removeModelBlock = (yaml) => yaml.replace(MODEL_BLOCK_RE, "").replace(/^\n+/, "");
|
||||||
|
|
||||||
// .env helpers — upsert/remove single KEY=VALUE line
|
// .env helpers — upsert/remove single KEY=VALUE line
|
||||||
@@ -107,10 +171,12 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
const yaml = await readConfigYaml();
|
const yaml = await readConfigYaml();
|
||||||
const model = parseModelBlock(yaml);
|
const model = parseModelBlock(yaml);
|
||||||
|
const delegation = parseDelegationBlock(yaml);
|
||||||
|
const auxiliary = parseAuxRoles(yaml);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
installed: true,
|
installed: true,
|
||||||
settings: { model },
|
settings: { model, delegation, auxiliary },
|
||||||
has9Router: has9RouterConfig(model),
|
has9Router: has9RouterConfig(model) || has9RouterConfig(delegation) || Object.values(auxiliary).some(has9RouterConfig),
|
||||||
configPath: getHermesConfigPath(),
|
configPath: getHermesConfigPath(),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -121,8 +187,14 @@ export async function GET() {
|
|||||||
|
|
||||||
export async function POST(request) {
|
export async function POST(request) {
|
||||||
try {
|
try {
|
||||||
const { baseUrl, apiKey, model } = await request.json();
|
const { baseUrl, apiKey, model, selections } = await request.json();
|
||||||
if (!baseUrl || !model) {
|
// selections: [{role, model}] — "default" plus any auxiliary/delegation slots.
|
||||||
|
// Legacy callers (CLI quick setup) send a bare `model` → treat as default role.
|
||||||
|
const sel = Array.isArray(selections) && selections.some((s) => s?.role && s?.model)
|
||||||
|
? selections.filter((s) => s?.role && s?.model)
|
||||||
|
: model ? [{ role: "default", model }] : [];
|
||||||
|
const defaultSel = sel.find((s) => s.role === "default");
|
||||||
|
if (!baseUrl || !defaultSel) {
|
||||||
return NextResponse.json({ error: "baseUrl and model are required" }, { status: 400 });
|
return NextResponse.json({ error: "baseUrl and model are required" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +203,17 @@ export async function POST(request) {
|
|||||||
|
|
||||||
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
||||||
|
|
||||||
// Update config.yaml — replace/insert model: block, keep everything else
|
// Update config.yaml — upsert each role block, keep everything else
|
||||||
const existingYaml = await readConfigYaml();
|
let newYaml = await readConfigYaml();
|
||||||
const newYaml = upsertModelBlock(existingYaml, buildModelBlock(model, normalizedBaseUrl));
|
for (const { role, model: roleModel } of sel) {
|
||||||
|
if (role === "default") {
|
||||||
|
newYaml = upsertModelBlock(newYaml, buildModelBlock(roleModel, normalizedBaseUrl));
|
||||||
|
} else if (role === "delegation") {
|
||||||
|
newYaml = upsertDelegationBlock(newYaml, buildDelegationBlock(roleModel, normalizedBaseUrl));
|
||||||
|
} else {
|
||||||
|
newYaml = upsertAuxRole(newYaml, role, buildAuxRoleBlock(role, roleModel, normalizedBaseUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
await fs.writeFile(getHermesConfigPath(), newYaml);
|
await fs.writeFile(getHermesConfigPath(), newYaml);
|
||||||
|
|
||||||
// Update .env — upsert OPENAI_API_KEY only when caller provides one
|
// Update .env — upsert OPENAI_API_KEY only when caller provides one
|
||||||
@@ -166,9 +246,15 @@ export async function DELETE() {
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const newYaml = removeModelBlock(yaml);
|
let newYaml = removeModelBlock(yaml);
|
||||||
|
newYaml = removeDelegationBlock(newYaml);
|
||||||
|
// Only drop auxiliary entries we manage (custom provider, any base URL — covers tunnels)
|
||||||
|
for (const [role, cfg] of Object.entries(parseAuxRoles(yaml))) {
|
||||||
|
if (cfg?.provider === "custom") newYaml = removeAuxRole(newYaml, role);
|
||||||
|
}
|
||||||
|
newYaml = newYaml.replace(/^\n+/, "");
|
||||||
await fs.writeFile(configPath, newYaml);
|
await fs.writeFile(configPath, newYaml);
|
||||||
return NextResponse.json({ success: true, message: `${PROVIDER_NAME} model block removed` });
|
return NextResponse.json({ success: true, message: `${PROVIDER_NAME} model blocks removed` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error resetting hermes settings:", error);
|
console.log("Error resetting hermes settings:", error);
|
||||||
return NextResponse.json({ error: "Failed to reset hermes settings" }, { status: 500 });
|
return NextResponse.json({ error: "Failed to reset hermes settings" }, { status: 500 });
|
||||||
|
|||||||
@@ -165,6 +165,22 @@ export const CLI_TOOLS = {
|
|||||||
color: "#8B5CF6",
|
color: "#8B5CF6",
|
||||||
description: "Nous Research self-improving AI agent",
|
description: "Nous Research self-improving AI agent",
|
||||||
configType: "custom",
|
configType: "custom",
|
||||||
|
// Model slots Hermes supports besides the default ("model:" block).
|
||||||
|
// "default" is not listed — the card renders it as the main model picker.
|
||||||
|
roles: [
|
||||||
|
{ id: "delegation", label: "Delegation (subagents)" },
|
||||||
|
{ id: "vision", label: "Vision" },
|
||||||
|
{ id: "web_extract", label: "Web Extract" },
|
||||||
|
{ id: "compression", label: "Compression" },
|
||||||
|
{ id: "title_generation", label: "Title Generation" },
|
||||||
|
{ id: "approval", label: "Approval" },
|
||||||
|
{ id: "skills_hub", label: "Skills Hub" },
|
||||||
|
{ id: "mcp", label: "MCP" },
|
||||||
|
{ id: "memory_query_rewrite", label: "Memory Query Rewrite" },
|
||||||
|
{ id: "background_review", label: "Background Review" },
|
||||||
|
{ id: "curator", label: "Curator" },
|
||||||
|
{ id: "monitor", label: "Monitor" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
droid: {
|
droid: {
|
||||||
id: "droid",
|
id: "droid",
|
||||||
|
|||||||
Reference in New Issue
Block a user