feat(cli): support Grok Build custom host configuration (#10830)

Adds a Grok Build card to the CLI Code dashboard: configures the main model and per-subagent model overrides, surgically upserts only the [model.omniroute] section of config.toml (preserving unrelated user sections), resolves config.toml via GROK_HOME or the CLI config home, and omits API keys from settings responses. Closes #10829.

Validated in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 14 files):
- 6/48 focused node:test cases in tests/integration/cli-settings-grok-build.test.ts initially failed with 422 instead of 200/409 — traced to this route being the first CLI-tool settings route to actually pass a targetPath into the shared guardCliConfigWrite/ensureCliConfigWriteAllowed container-ephemeral-write guard (every other cli-tools/*-settings route calls it with no path, which always no-ops). This devbox genuinely runs inside Docker with no bind mount for the test's tmpdir fixtures, so the guard correctly refused — a real environment-dependent gap in the PR's own test setup (missing OMNIROUTE_ALLOW_CONTAINER_CONFIG_WRITE=1, the pattern already used by tests/unit/cli-tools-apply-container-422.test.ts for this exact guard). Fixed by setting that flag in the test file's setup, pushed fix-in-place.
- 61/61 tests pass after the fix (grok-build-config, check-tool-config-status, all-statuses-route, cli-settings-grok-build, plus 13/13 UI tests via vitest for GrokBuildToolCard/ToolDetailClient).
- Verified the "omit API keys from settings responses" claim: GET applies omitApiKeys() to both config/settings before responding.
- check-file-size, check-changelog-integrity: OK.
- typecheck:core: clean.
- check-complexity / check-cognitive-complexity: OK, both under baseline.

Co-authored-by: tuandinh0801 <tuandinh0801@users.noreply.github.com>
This commit is contained in:
Tuan Dinh
2026-08-21 10:29:01 +07:00
committed by GitHub
parent 16fc433a4f
commit 6fd4040148
14 changed files with 1857 additions and 261 deletions

View File

@@ -0,0 +1,667 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import CliStatusBadge from "./CliStatusBadge";
import { Button, Card, ManualConfigModal, ModelSelectModal } from "@/shared/components";
import ProviderIcon from "@/shared/components/ProviderIcon";
import type { ToolBatchStatus } from "@/shared/types/cliBatchStatus";
const SETTINGS_ENDPOINT = "/api/cli-tools/grok-build-settings";
const PRESETS_KEY = "omniroute.grokBuildEndpointPresets";
const CUSTOM_ENDPOINT = "__custom__";
const SUBAGENTS = ["general-purpose", "explore", "plan"] as const;
type SubagentType = (typeof SUBAGENTS)[number];
type Message = { type: "success" | "error"; text: string } | null;
type ModelOption = { value: string; label?: string };
type ApiKeyOption = { id: string; name?: string; key?: string };
type EndpointOption = { id: string; label: string; url: string };
type SavedEndpoint = { name: string; baseUrl: string };
type Backup = { id: string; createdAt: string };
type GrokModelStatus = {
model: string | null;
base_url: string | null;
context_window: number | null;
};
type GrokStatus = {
installed?: boolean;
runnable?: boolean;
hasOmniRoute?: boolean;
apiKeyConfigured?: boolean;
configPath?: string;
config?: {
model?: GrokModelStatus | null;
subagentModels?: Partial<Record<SubagentType, GrokModelStatus | null>>;
};
error?: { message?: string } | string;
};
interface GrokBuildToolCardProps {
tool: { name: string; description?: string };
isExpanded?: boolean;
onToggle?: () => void;
apiKeys?: ApiKeyOption[];
activeProviders?: Array<{
provider: string;
id?: string | number;
providerSpecificData?: unknown;
}>;
hasActiveProviders?: boolean;
availableModels?: ModelOption[];
batchStatus?: ToolBatchStatus | null;
lastConfiguredAt?: string | null;
}
const errorText = (body: unknown, fallback: string): string => {
if (!body || typeof body !== "object") return fallback;
const error = (body as { error?: unknown }).error;
if (typeof error === "string") return error;
if (
error &&
typeof error === "object" &&
typeof (error as { message?: unknown }).message === "string"
) {
return (error as { message: string }).message;
}
return fallback;
};
const ensureV1 = (value: string): string => {
const trimmed = value.trim().replace(/\/+$/, "");
if (!trimmed) return "";
return `${trimmed.replace(/(?:\/v1)+$/, "")}/v1`;
};
const readPresets = (): SavedEndpoint[] => {
try {
const value: unknown = JSON.parse(localStorage.getItem(PRESETS_KEY) ?? "[]");
if (!Array.isArray(value)) return [];
return value.filter((item): item is SavedEndpoint =>
Boolean(
item &&
typeof item === "object" &&
typeof item.name === "string" &&
typeof item.baseUrl === "string"
)
);
} catch {
return [];
}
};
const getTunnelUrl = (body: unknown): string => {
if (!body || typeof body !== "object") return "";
const record = body as Record<string, unknown>;
for (const key of ["apiUrl", "publicUrl", "tunnelUrl"]) {
if (typeof record[key] === "string" && record[key]) return ensureV1(record[key]);
}
return "";
};
const modelLabel = (type: SubagentType): string =>
type === "general-purpose" ? "General purpose" : `${type[0].toUpperCase()}${type.slice(1)}`;
/** Configure Grok Build model slots and endpoint access. */
export default function GrokBuildToolCard({
tool,
isExpanded = true,
onToggle = () => undefined,
apiKeys = [],
activeProviders = [],
hasActiveProviders = false,
availableModels = [],
batchStatus = null,
lastConfiguredAt = null,
}: GrokBuildToolCardProps) {
const t = useTranslations("cliTools");
const [status, setStatus] = useState<GrokStatus | null>(null);
const [checking, setChecking] = useState(true);
const [applying, setApplying] = useState(false);
const [resetting, setResetting] = useState(false);
const [message, setMessage] = useState<Message>(null);
const [model, setModel] = useState("");
const [subagentModels, setSubagentModels] = useState<Partial<Record<SubagentType, string>>>({});
const [selectedKeyId, setSelectedKeyId] = useState("");
const [endpoints, setEndpoints] = useState<EndpointOption[]>([]);
const [selectedEndpoint, setSelectedEndpoint] = useState("");
const [customEndpoint, setCustomEndpoint] = useState("");
const [modelTarget, setModelTarget] = useState<"main" | SubagentType | null>(null);
const [showManual, setShowManual] = useState(false);
const [backups, setBackups] = useState<Backup[]>([]);
const [showBackups, setShowBackups] = useState(false);
const [restoringBackup, setRestoringBackup] = useState<string | null>(null);
useEffect(() => {
if (!selectedKeyId && apiKeys[0]?.id) setSelectedKeyId(apiKeys[0].id);
}, [apiKeys, selectedKeyId]);
const hydrateStatus = useCallback((next: GrokStatus) => {
setStatus(next);
setModel(next.config?.model?.model ?? "");
setSubagentModels(
Object.fromEntries(
SUBAGENTS.flatMap((type) => {
const value = next.config?.subagentModels?.[type]?.model;
return value ? [[type, value]] : [];
})
)
);
}, []);
const refreshStatus = useCallback(async () => {
setChecking(true);
try {
const response = await fetch(SETTINGS_ENDPOINT);
const body = (await response.json()) as GrokStatus;
if (!response.ok) throw new Error(errorText(body, "Failed to read Grok Build settings"));
hydrateStatus(body);
} catch (error) {
setMessage({ type: "error", text: error instanceof Error ? error.message : String(error) });
} finally {
setChecking(false);
}
}, [hydrateStatus]);
const refreshEndpoints = useCallback(async () => {
const requests = [
fetch("/api/settings"),
fetch("/api/tunnels/cloudflared"),
fetch("/api/tunnels/tailscale"),
fetch("/api/tunnels/ngrok"),
];
const results = await Promise.allSettled(requests);
const bodies = await Promise.all(
results.map(async (result) =>
result.status === "fulfilled" && result.value.ok ? result.value.json() : null
)
);
const settings = (bodies[0] ?? {}) as Record<string, unknown>;
const next: EndpointOption[] = [];
if (typeof settings.apiPort === "number") {
next.push({
id: "local",
label: "Local",
url: `http://127.0.0.1:${settings.apiPort}/v1`,
});
}
if (typeof settings.cloudUrl === "string" && typeof settings.machineId === "string") {
next.push({
id: "cloud",
label: "Cloud",
url: ensureV1(`${settings.cloudUrl.replace(/\/+$/, "")}/${settings.machineId}`),
});
}
["cloudflared", "tailscale", "ngrok"].forEach((name, index) => {
const url = getTunnelUrl(bodies[index + 1]);
if (url) next.push({ id: name, label: name, url });
});
readPresets().forEach((preset, index) => {
next.push({ id: `saved-${index}`, label: preset.name, url: ensureV1(preset.baseUrl) });
});
next.push({ id: CUSTOM_ENDPOINT, label: "Custom", url: "" });
setEndpoints(next);
setSelectedEndpoint((current) => current || next[0]?.id || CUSTOM_ENDPOINT);
}, []);
const refreshBackups = useCallback(async () => {
try {
const response = await fetch("/api/cli-tools/backups?tool=grok-build");
const body = (await response.json()) as { backups?: Backup[] };
if (response.ok) setBackups(body.backups ?? []);
} catch {
setBackups([]);
}
}, []);
useEffect(() => {
if (!isExpanded) return;
void Promise.all([refreshStatus(), refreshEndpoints(), refreshBackups()]);
}, [isExpanded, refreshBackups, refreshEndpoints, refreshStatus]);
const baseUrl = useMemo(() => {
if (selectedEndpoint === CUSTOM_ENDPOINT) return ensureV1(customEndpoint);
return endpoints.find((endpoint) => endpoint.id === selectedEndpoint)?.url ?? "";
}, [customEndpoint, endpoints, selectedEndpoint]);
const manualToml = useMemo(() => {
const contextWindowFor = (selected: string): number =>
(
availableModels.find((candidate) => candidate.value === selected) as
(ModelOption & { contextWindow?: number; contextLength?: number }) | undefined
)?.contextWindow ??
(
availableModels.find((candidate) => candidate.value === selected) as
(ModelOption & { contextWindow?: number; contextLength?: number }) | undefined
)?.contextLength ??
200000;
const mainModel = model || "provider/model-id";
const blocks = [
`[models]\ndefault = "omniroute"`,
`[model.omniroute]\nmodel = "${mainModel}"\nbase_url = "${baseUrl || "http://127.0.0.1:<API_PORT>/v1"}"\nname = "OmniRoute"\ndescription = "Routed via OmniRoute gateway"\napi_backend = "chat_completions"\napi_key = "<API_KEY_FROM_DASHBOARD>"\ncontext_window = ${contextWindowFor(mainModel)}`,
];
const mappings: string[] = [];
for (const type of SUBAGENTS) {
const selected = subagentModels[type]?.trim();
if (!selected) continue;
const slot = `omniroute-${type}`;
mappings.push(`${type} = "${slot}"`);
blocks.push(
`[model.${slot}]\nmodel = "${selected}"\nbase_url = "${baseUrl || "http://127.0.0.1:<API_PORT>/v1"}"\nname = "OmniRoute ${type}"\ndescription = "Routed via OmniRoute gateway"\napi_backend = "chat_completions"\napi_key = "<API_KEY_FROM_DASHBOARD>"\ncontext_window = ${contextWindowFor(selected)}`
);
}
if (mappings.length) blocks.splice(1, 0, `[subagents.models]\n${mappings.join("\n")}`);
return `${blocks.join("\n\n")}\n`;
}, [availableModels, baseUrl, model, subagentModels]);
const apply = async () => {
setApplying(true);
setMessage(null);
try {
const desiredSubagents = Object.fromEntries(
SUBAGENTS.flatMap((type) => {
const selected = subagentModels[type]?.trim();
return selected ? [[type, { model: selected }]] : [];
})
);
const selectedContext = (selected: string): number | undefined => {
const option = availableModels.find((candidate) => candidate.value === selected) as
(ModelOption & { contextWindow?: number; contextLength?: number }) | undefined;
return option?.contextWindow ?? option?.contextLength;
};
const response = await fetch(SETTINGS_ENDPOINT, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl,
keyId: selectedKeyId || null,
model,
contextWindow: selectedContext(model),
subagentModels: Object.fromEntries(
Object.entries(desiredSubagents).map(([type, entry]) => [
type,
{ ...entry, contextWindow: selectedContext(entry.model) },
])
),
}),
});
const body: unknown = await response.json();
if (!response.ok) throw new Error(errorText(body, "Failed to apply settings"));
setMessage({ type: "success", text: "Grok Build settings applied." });
await Promise.all([refreshStatus(), refreshBackups()]);
} catch (error) {
setMessage({ type: "error", text: error instanceof Error ? error.message : String(error) });
} finally {
setApplying(false);
}
};
const reset = async () => {
setResetting(true);
setMessage(null);
try {
const response = await fetch(SETTINGS_ENDPOINT, { method: "DELETE" });
const body: unknown = await response.json();
if (!response.ok) throw new Error(errorText(body, "Failed to reset settings"));
setMessage({ type: "success", text: "Grok Build settings reset." });
await Promise.all([refreshStatus(), refreshBackups()]);
} catch (error) {
setMessage({ type: "error", text: error instanceof Error ? error.message : String(error) });
} finally {
setResetting(false);
}
};
const selectModel = (selection: unknown) => {
const value = (selection as { value?: unknown })?.value;
if (typeof value !== "string") return;
if (modelTarget === "main") setModel(value);
else if (modelTarget) setSubagentModels((current) => ({ ...current, [modelTarget]: value }));
setModelTarget(null);
};
const restoreBackup = async (backupId: string) => {
setRestoringBackup(backupId);
setMessage(null);
try {
const response = await fetch("/api/cli-tools/backups", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ tool: "grok-build", backupId }),
});
const body: unknown = await response.json();
if (!response.ok) throw new Error(errorText(body, "Failed to restore backup"));
setMessage({ type: "success", text: "Grok Build backup restored." });
await Promise.all([refreshStatus(), refreshBackups()]);
} catch (error) {
setMessage({ type: "error", text: error instanceof Error ? error.message : String(error) });
} finally {
setRestoringBackup(null);
}
};
const configured = Boolean(status?.hasOmniRoute);
const cliReady = Boolean(status?.installed && status?.runnable);
const effectiveConfigStatus = status
? cliReady
? configured
? "configured"
: "not_configured"
: "not_installed"
: (batchStatus?.config.status ?? null);
const rowClass = "flex items-center gap-2";
const labelClass = "w-32 shrink-0 text-right text-sm font-semibold text-text-main";
const inputClass =
"min-w-0 flex-1 rounded border border-border bg-surface px-2 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-primary/50";
return (
<Card padding="sm" className="overflow-hidden">
<div className="flex cursor-pointer items-center justify-between" onClick={onToggle}>
<div className="flex items-center gap-3">
<div className="flex size-8 shrink-0 items-center justify-center">
<ProviderIcon providerId="grok" size={32} type="color" />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<h3 className="text-sm font-medium">{tool.name}</h3>
<CliStatusBadge
effectiveConfigStatus={effectiveConfigStatus}
batchStatus={null}
lastConfiguredAt={lastConfiguredAt ?? batchStatus?.config.lastConfiguredAt ?? null}
/>
</div>
<p className="truncate text-xs text-text-muted">{tool.description}</p>
</div>
</div>
<span
className={`material-symbols-outlined text-[20px] text-text-muted transition-transform ${isExpanded ? "rotate-180" : ""}`}
>
expand_more
</span>
</div>
{isExpanded && (
<div className="mt-4 flex flex-col gap-4 border-t border-border pt-4">
{checking && (
<div className="flex items-center gap-2 text-text-muted">
<span className="material-symbols-outlined animate-spin">progress_activity</span>
<span>{t("checkingCli", { tool: "Grok Build" })}</span>
</div>
)}
{!checking && status && !cliReady && (
<div className="flex items-center gap-3 rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-4">
<span className="material-symbols-outlined text-yellow-500">warning</span>
<div>
<p className="font-medium text-yellow-600 dark:text-yellow-400">
{status.installed
? t("cliNotRunnable", { tool: "Grok Build" })
: t("cliNotInstalled", { tool: "Grok Build" })}
</p>
<p className="text-sm text-text-muted">
Direct Apply needs the Grok Build CLI on this computer. Manual Config stays
available.
</p>
</div>
</div>
)}
{status?.config?.model?.base_url && (
<div className={rowClass}>
<span className={labelClass}>{t("current")}</span>
<span className="material-symbols-outlined text-[14px] text-text-muted">
arrow_forward
</span>
<span className="min-w-0 flex-1 truncate px-2 py-1.5 text-xs text-text-muted">
{status.config.model.base_url}
</span>
</div>
)}
<div className={rowClass}>
<label className={labelClass} htmlFor="grok-build-endpoint">
{t("baseUrl")}
</label>
<span className="material-symbols-outlined text-[14px] text-text-muted">
arrow_forward
</span>
<select
id="grok-build-endpoint"
className={inputClass}
value={selectedEndpoint}
onChange={(event) => setSelectedEndpoint(event.target.value)}
>
{endpoints.map((endpoint) => (
<option key={endpoint.id} value={endpoint.id}>
{endpoint.label}: {endpoint.url || "Enter a URL"}
</option>
))}
</select>
</div>
{selectedEndpoint === CUSTOM_ENDPOINT && (
<div className={rowClass}>
<span className={labelClass}>Custom URL</span>
<span className="material-symbols-outlined text-[14px] text-text-muted">
arrow_forward
</span>
<input
aria-label="Custom endpoint"
className={inputClass}
type="url"
value={customEndpoint}
onChange={(event) => setCustomEndpoint(event.target.value)}
placeholder="https://gateway.example/v1"
/>
</div>
)}
<div className={rowClass}>
<label className={labelClass} htmlFor="grok-build-api-key">
{t("apiKey")}
</label>
<span className="material-symbols-outlined text-[14px] text-text-muted">
arrow_forward
</span>
<select
id="grok-build-api-key"
className={inputClass}
value={selectedKeyId}
onChange={(event) => setSelectedKeyId(event.target.value)}
>
<option value="">Use the OmniRoute default key</option>
{apiKeys.map((key) => (
<option key={key.id} value={key.id}>
{key.key || key.name || key.id}
</option>
))}
</select>
</div>
<div className={rowClass}>
<span className={labelClass}>{t("model")}</span>
<span className="material-symbols-outlined text-[14px] text-text-muted">
arrow_forward
</span>
<Button
variant="outline"
size="sm"
disabled={!hasActiveProviders}
onClick={() => setModelTarget("main")}
>
{t("selectModel")}
</Button>
<input
aria-label="Main model"
className={inputClass}
value={model}
onChange={(event) => setModel(event.target.value)}
placeholder={availableModels[0]?.value || "provider/model-id"}
/>
{model && (
<button
type="button"
className="rounded p-1 text-text-muted transition-colors hover:text-red-500"
title={t("clear")}
onClick={() => setModel("")}
>
<span className="material-symbols-outlined text-[14px]">close</span>
</button>
)}
</div>
<div className="my-2 h-px bg-border/50" />
<div className="mb-2 text-right text-[11px] font-medium uppercase tracking-wider text-text-muted">
Subagent model overrides
</div>
{SUBAGENTS.map((type) => (
<div className={`${rowClass} group`} key={type}>
<span className="w-32 shrink-0 truncate text-right text-[11px] font-mono text-text-main opacity-70 transition-opacity group-hover:opacity-100">
{modelLabel(type)}
</span>
<span className="material-symbols-outlined text-[14px] text-border transition-colors group-hover:text-primary">
arrow_forward
</span>
<Button
variant="outline"
size="sm"
disabled={!hasActiveProviders}
onClick={() => setModelTarget(type)}
>
{t("selectModel")}
</Button>
<input
aria-label={`${modelLabel(type)} model`}
className={inputClass}
value={subagentModels[type] ?? ""}
onChange={(event) =>
setSubagentModels((current) => ({ ...current, [type]: event.target.value }))
}
placeholder={`Use ${model || "the main model"}`}
/>
{subagentModels[type] && (
<button
type="button"
className="rounded p-1 text-text-muted transition-colors hover:text-red-500"
title={t("clear")}
onClick={() =>
setSubagentModels((current) => ({ ...current, [type]: undefined }))
}
>
<span className="material-symbols-outlined text-[14px]">close</span>
</button>
)}
</div>
))}
{message && (
<p
role="status"
className={`rounded px-2 py-1.5 text-xs ${message.type === "success" ? "bg-green-500/10 text-green-600" : "bg-red-500/10 text-red-600"}`}
>
{message.text}
</p>
)}
<div className="flex flex-wrap items-center gap-2">
<Button
variant="primary"
size="sm"
loading={applying}
disabled={!cliReady || !model || !baseUrl}
onClick={apply}
>
<span className="material-symbols-outlined mr-1 text-[14px]">save</span>
{t("apply")}
</Button>
<Button
variant="outline"
size="sm"
loading={resetting}
disabled={!configured}
onClick={reset}
>
<span className="material-symbols-outlined mr-1 text-[14px]">restore</span>
{t("reset")}
</Button>
<Button variant="ghost" size="sm" onClick={() => setShowManual(true)}>
<span className="material-symbols-outlined mr-1 text-[14px]">content_copy</span>
{t("manualConfig")}
</Button>
<div className="flex-1" />
<Button
variant="ghost"
size="sm"
onClick={() => {
setShowBackups((current) => !current);
if (!showBackups) void refreshBackups();
}}
>
<span className="material-symbols-outlined mr-1 text-[14px]">history</span>
{t("backups")}
{backups.length > 0 && ` (${backups.length})`}
</Button>
</div>
{showBackups && (
<div className="mt-2 rounded-lg border border-border bg-surface p-3">
<h4 className="mb-2 flex items-center gap-1 text-xs font-semibold text-text-main">
<span className="material-symbols-outlined text-[14px]">history</span>
{t("configBackups")}
</h4>
{backups.length === 0 ? (
<p className="text-xs text-text-muted">{t("noBackupsYet")}</p>
) : (
<div className="space-y-1.5">
{backups.map((backup) => (
<div
key={backup.id}
className="flex items-center gap-2 rounded bg-black/5 px-2 py-1.5 text-xs dark:bg-white/5"
>
<span className="material-symbols-outlined text-[14px] text-text-muted">
description
</span>
<span className="flex-1 truncate font-mono" title={backup.id}>
{backup.id}
</span>
<span className="whitespace-nowrap text-text-muted">
{new Date(backup.createdAt).toLocaleString()}
</span>
<button
type="button"
className="rounded bg-primary/10 px-2 py-0.5 text-[10px] font-medium text-primary transition-colors hover:bg-primary/20 disabled:opacity-50"
disabled={restoringBackup === backup.id}
onClick={() => void restoreBackup(backup.id)}
>
{restoringBackup === backup.id ? "..." : t("restore")}
</button>
</div>
))}
</div>
)}
</div>
)}
</div>
)}
{modelTarget && (
<ModelSelectModal
isOpen
onClose={() => setModelTarget(null)}
onSelect={selectModel}
selectedModel={modelTarget === "main" ? model : (subagentModels[modelTarget] ?? "")}
activeProviders={activeProviders}
title={`Select ${modelTarget === "main" ? "main" : modelLabel(modelTarget)} model`}
/>
)}
<ManualConfigModal
isOpen={showManual}
onClose={() => setShowManual(false)}
title="Grok Build manual config"
configs={[{ filename: "~/.grok/config.toml", content: manualToml }]}
/>
</Card>
);
}

View File

@@ -15,6 +15,7 @@ import {
DefaultToolCard,
DroidToolCard,
HermesAgentToolCard,
GrokBuildToolCard,
KiloToolCard,
OpenClawToolCard,
} from "./index";
@@ -268,6 +269,8 @@ export default function ToolDetailClient({ toolId, category }: ToolDetailClientP
return <CopilotToolCard {...cardProps} />;
case "hermes-agent":
return <HermesAgentToolCard {...cardProps} />;
case "grok-build":
return <GrokBuildToolCard {...cardProps} />;
case "antigravity":
return <AntigravityToolCard {...cardProps} />;
case "custom":

View File

@@ -9,3 +9,4 @@ export { default as AntigravityToolCard } from "./AntigravityToolCard";
export { default as CopilotToolCard } from "./CopilotToolCard";
export { default as CustomCliCard } from "./CustomCliCard";
export { default as HermesAgentToolCard } from "./HermesAgentToolCard";
export { default as GrokBuildToolCard } from "./GrokBuildToolCard";

View File

@@ -8,10 +8,18 @@ import { buildErrorBody } from "@omniroute/open-sse/utils/error.ts";
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
import { CLI_TOOLS } from "@/shared/constants/cliTools";
import { getCliRuntimeStatus, getCliPrimaryConfigPath } from "@/shared/services/cliRuntime";
import {
getCliConfigHome,
getCliRuntimeStatus,
getCliPrimaryConfigPath,
} from "@/shared/services/cliRuntime";
import { getAllCliToolLastConfigured } from "@/lib/db/cliToolState";
import { checkToolConfigStatus } from "@/lib/cliTools/checkToolConfigStatus";
import { findOmniRouteQwenCodeModel } from "@/shared/services/qwenCodeConfig";
import {
parseGrokBuildConfig,
resolveGrokBuildConfigPath,
} from "@/shared/services/grokBuildConfig";
import { getCached, setCached } from "@/lib/cliTools/batchStatusCache";
import type { ToolBatchStatus, ToolBatchStatusMap } from "@/shared/types/cliBatchStatus";
@@ -19,6 +27,11 @@ const logger = pino({ name: "cli-tools-all-statuses-api" });
const TOOL_CHECK_TIMEOUT_MS = 5000; // 5s per tool max
const getConfigPath = (toolId: string): string | null =>
toolId === "grok-build"
? resolveGrokBuildConfigPath(process.env, getCliConfigHome())
: getCliPrimaryConfigPath(toolId);
/**
* Attempt to extract the endpoint from a config file for a given toolId.
* Returns null if extraction is not possible or the file is not parseable.
@@ -30,6 +43,10 @@ async function extractEndpointFromConfig(
try {
const content = await fs.readFile(configPath, "utf-8");
if (toolId === "grok-build") {
return parseGrokBuildConfig(content).model?.base_url ?? null;
}
// TOML-based tools (codex) — do a best-effort text search
if (toolId === "codex") {
const match = content.match(/base_url\s*=\s*["']([^"'\n]+)["']/i);
@@ -96,7 +113,7 @@ export async function GET(request: Request): Promise<Response> {
const mtimesMap: Record<string, number> = {};
await Promise.allSettled(
toolIds.map(async (toolId) => {
const configPath = getCliPrimaryConfigPath(toolId);
const configPath = getConfigPath(toolId);
if (!configPath) {
mtimesMap[toolId] = 0;
return;
@@ -149,7 +166,7 @@ export async function GET(request: Request): Promise<Response> {
!runtime.installed || !runtime.runnable ? "not_installed" : configStatus;
// Try to extract endpoint from config file
const configPath = getCliPrimaryConfigPath(toolId);
const configPath = getConfigPath(toolId);
const endpoint = configPath ? await extractEndpointFromConfig(toolId, configPath) : null;
const result: ToolBatchStatus = {

View File

@@ -8,7 +8,7 @@ import { cliBackupMutationSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
const VALID_TOOLS = ["claude", "codex", "droid", "openclaw", "cline", "kilo", "qwen"];
const VALID_TOOLS = ["claude", "codex", "droid", "grok-build", "openclaw", "cline", "kilo", "qwen"];
// GET /api/cli-tools/backups?tool=claude — list backups
export async function GET(request) {

View File

@@ -1,300 +1,268 @@
"use server";
import { NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
import {
ensureCliConfigWriteAllowed,
getCliPrimaryConfigPath,
getCliRuntimeStatus,
} from "@/shared/services/cliRuntime";
import { createBackup } from "@/shared/services/backupService";
import { saveCliToolLastConfigured, deleteCliToolLastConfigured } from "@/lib/db/cliToolState";
import { cliModelConfigSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
import { resolveApiKey } from "@/shared/services/apiKeyResolver";
import fs from "node:fs/promises";
import path from "node:path";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts";
import { NextResponse } from "next/server";
import pino from "pino";
import { z } from "zod";
import { requireCliToolsAuth } from "@/lib/api/requireCliToolsAuth";
import { guardCliConfigWrite } from "@/lib/api/cliConfigWriteGuard";
import { deleteCliToolLastConfigured, saveCliToolLastConfigured } from "@/lib/db/cliToolState";
import { getResolvedModelCapabilities } from "@/lib/modelCapabilities";
import { resolveApiKey } from "@/shared/services/apiKeyResolver";
import { createBackup } from "@/shared/services/backupService";
import { getCliConfigHome, getCliRuntimeStatus } from "@/shared/services/cliRuntime";
import {
applyGrokBuildConfig,
GrokBuildConfigConflictError,
GROK_SUBAGENT_TYPES,
parseGrokBuildConfig,
resetGrokBuildConfig,
resolveGrokBuildConfigPath,
type GrokBuildApplyOptions,
type GrokSubagentType,
} from "@/shared/services/grokBuildConfig";
const logger = pino({ name: "grok-build-settings-api" });
const TOOL_ID = "grok-build";
const MODEL_SLOT = "omniroute";
// Grok Build ships with a built-in default model id; restored on Reset when no
// prior custom default was recorded.
const BUILTIN_DEFAULT_MODEL = "grok-build";
const DEFAULT_CONTEXT_WINDOW = 200000;
const getGrokBuildConfigPath = (): string =>
getCliPrimaryConfigPath(TOOL_ID) ?? path.join(process.env.HOME ?? "~", ".grok", "config.toml");
const modelSelectionSchema = z.object({
model: z.string().trim().min(1),
contextWindow: z.number().int().positive().optional(),
});
const getGrokBuildDir = () => path.dirname(getGrokBuildConfigPath());
const grokBuildConfigSchema = z.object({
baseUrl: z
.string()
.trim()
.url()
.refine((value) => ["http:", "https:"].includes(new URL(value).protocol), {
message: "baseUrl must use HTTP or HTTPS",
}),
apiKey: z.string().nullable().optional(),
keyId: z.string().trim().min(1).nullable().optional(),
model: z.string().trim().min(1),
contextWindow: z.number().int().positive().optional(),
subagentModels: z
.object({
"general-purpose": modelSelectionSchema.optional(),
explore: modelSelectionSchema.optional(),
plan: modelSelectionSchema.optional(),
})
.optional(),
});
// [model.omniroute] ... until the next [section] header or EOF
const MODEL_SECTION_RE = new RegExp(
`^\\[model\\.${MODEL_SLOT}\\][ \\t]*\\r?\\n(?:(?!\\[)[^\\r\\n]*\\r?\\n?)*`,
"m"
);
const MODELS_SECTION_RE = /^\[models\][ \t]*\r?\n((?:(?!\[)[^\r\n]*\r?\n?)*)/m;
// Marker written on Apply so Reset can restore the previously configured default.
const PREV_DEFAULT_RE = /^# omniroute-prev-default = "([^"]*)"[ \t]*\r?\n?/m;
/** Resolve Grok Build config.toml from GROK_HOME or the CLI config home. */
function getGrokBuildConfigPath(
env: NodeJS.ProcessEnv = process.env,
configHome = getCliConfigHome()
): string {
return resolveGrokBuildConfigPath(env, configHome);
}
const getTomlField = (body: string, key: string): string | null => {
const m = body.match(new RegExp(`^[ \\t]*${key}[ \\t]*=[ \\t]*"([^"]*)"`, "m"));
return m ? m[1] : null;
};
type GrokModelSection = {
model: string | null;
base_url: string | null;
name: string | null;
api_key: string | null;
api_backend: string | null;
};
/**
* Parse the `~/.grok/config.toml` produced by the Grok Build CLI (a subset of
* TOML — flat `key = "value"` pairs inside `[section]` headers). Grok Build's
* config format is not guaranteed to be quote-escaped or nested, so this reads
* only the flat string fields OmniRoute itself writes.
*/
const parseModelSection = (toml: string): GrokModelSection | null => {
const match = toml.match(MODEL_SECTION_RE);
if (!match) return null;
const body = match[0].replace(/^\[model\.[^\]]+\][ \t]*\r?\n/, "");
return {
model: getTomlField(body, "model"),
base_url: getTomlField(body, "base_url"),
name: getTomlField(body, "name"),
api_key: getTomlField(body, "api_key"),
api_backend: getTomlField(body, "api_backend"),
};
};
const parseModelsDefault = (toml: string): string | null => {
const match = toml.match(MODELS_SECTION_RE);
if (!match) return null;
return getTomlField(match[1] || "", "default");
};
const escapeTomlString = (value: string): string => value.replace(/["\\]/g, "\\$&");
const buildModelSection = (model: string, baseUrl: string, apiKey: string): string => {
const lines = [
`[model.${MODEL_SLOT}]`,
`model = "${escapeTomlString(model)}"`,
`base_url = "${escapeTomlString(baseUrl)}"`,
`name = "OmniRoute"`,
`description = "Routed via OmniRoute gateway"`,
`api_backend = "chat_completions"`,
];
if (apiKey) lines.push(`api_key = "${escapeTomlString(apiKey)}"`);
return `${lines.join("\n")}\n`;
};
/** Insert/replace the `[model.omniroute]` section, preserving the rest of the file. */
const upsertModelSection = (toml: string, section: string): string => {
if (MODEL_SECTION_RE.test(toml)) return toml.replace(MODEL_SECTION_RE, section);
const needsNl = toml.length > 0 && !toml.endsWith("\n");
return `${toml}${needsNl ? "\n" : ""}\n${section}`;
};
const removeModelSection = (toml: string): string =>
toml.replace(MODEL_SECTION_RE, "").replace(/\n{3,}/g, "\n\n");
/** Set or insert `default = "..."` inside an existing `[models]`, or create the section. */
const setModelsDefault = (toml: string, value: string): string => {
const match = toml.match(MODELS_SECTION_RE);
if (match) {
const body = match[1] || "";
const newBody = /^[ \t]*default[ \t]*=/m.test(body)
? body.replace(/^[ \t]*default[ \t]*=[ \t]*"[^"]*"/m, `default = "${value}"`)
: `default = "${value}"\n${body}`;
return toml.replace(match[0], `[models]\n${newBody}`);
}
const block = `[models]\ndefault = "${value}"\n\n`;
return toml.length > 0 ? block + toml : block;
};
/** Remember the previous default once so re-Apply never clobbers it with our own slot. */
const rememberPrevDefault = (toml: string): string => {
if (PREV_DEFAULT_RE.test(toml)) return toml;
const current = parseModelsDefault(toml);
if (!current || current === MODEL_SLOT) return toml;
const marker = `# omniroute-prev-default = "${current}"\n`;
if (MODEL_SECTION_RE.test(toml)) {
return toml.replace(MODEL_SECTION_RE, (section) => marker + section);
}
const needsNl = toml.length > 0 && !toml.endsWith("\n");
return `${toml}${needsNl ? "\n" : ""}${marker}`;
};
/** If `[models].default` still points at our slot, restore the remembered default. */
const clearModelsDefaultIfOurs = (toml: string): string => {
const prevMatch = toml.match(PREV_DEFAULT_RE);
const restoreTo = prevMatch?.[1] || BUILTIN_DEFAULT_MODEL;
let next = toml.replace(PREV_DEFAULT_RE, "");
const current = parseModelsDefault(next);
if (current === MODEL_SLOT) {
next = setModelsDefault(next, restoreTo);
}
return next;
};
const hasOmniRouteConfig = (modelCfg: GrokModelSection | null): boolean =>
Boolean(modelCfg?.base_url);
// Read current config.toml
const readConfigToml = async (): Promise<string> => {
const readConfigToml = async (configPath: string): Promise<string> => {
try {
return await fs.readFile(getGrokBuildConfigPath(), "utf-8");
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") return "";
throw err;
return await fs.readFile(configPath, "utf8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "";
throw error;
}
};
// GET — check Grok Build CLI and return current [model.omniroute] config
export async function GET(request: Request) {
const writeAtomic = async (filePath: string, content: string): Promise<void> => {
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
const mode = process.platform === "win32" ? undefined : 0o600;
try {
await fs.writeFile(tempPath, content, { encoding: "utf8", mode });
if (mode !== undefined) await fs.chmod(tempPath, mode);
await fs.rename(tempPath, filePath);
} catch (error) {
await fs.unlink(tempPath).catch(() => undefined);
throw error;
}
};
const normalizeBaseUrl = (baseUrl: string): string => {
const url = new URL(baseUrl);
url.pathname = `${url.pathname.replace(/(?:\/v1)*\/?$/, "")}/v1`.replace(/\/+/g, "/");
url.search = "";
url.hash = "";
return url.toString().replace(/\/$/, "");
};
const resolveContextWindow = (value: number | undefined, model: string): number => {
if (value !== undefined) return value;
return getResolvedModelCapabilities(model).contextWindow ?? DEFAULT_CONTEXT_WINDOW;
};
const normalizeApplyOptions = (
data: z.infer<typeof grokBuildConfigSchema>,
apiKey: string
): GrokBuildApplyOptions => {
const options: GrokBuildApplyOptions = {
baseUrl: normalizeBaseUrl(data.baseUrl),
apiKey,
model: data.model,
contextWindow: resolveContextWindow(data.contextWindow, data.model),
};
if (data.subagentModels !== undefined) {
options.subagentModels = {};
for (const type of GROK_SUBAGENT_TYPES) {
const selected = data.subagentModels[type];
if (!selected) continue;
options.subagentModels[type] = {
model: selected.model,
contextWindow: resolveContextWindow(selected.contextWindow, selected.model),
};
}
}
return options;
};
const omitApiKey = <T extends { api_key: string | null }>(model: T): Omit<T, "api_key"> => {
const { api_key: _apiKey, ...publicModel } = model;
return publicModel;
};
const omitApiKeys = (settings: ReturnType<typeof parseGrokBuildConfig>) => ({
...settings,
model: settings.model ? omitApiKey(settings.model) : null,
subagentModels: Object.fromEntries(
GROK_SUBAGENT_TYPES.map((type) => [
type,
settings.subagentModels[type] ? omitApiKey(settings.subagentModels[type]) : null,
])
) as Record<
GrokSubagentType,
Omit<NonNullable<(typeof settings.subagentModels)[GrokSubagentType]>, "api_key"> | null
>,
});
const hasOmniRouteConfig = (settings: GrokBuildSettings): boolean =>
settings.default === "omniroute" &&
settings.model?.base_url !== null &&
settings.model?.api_backend === "chat_completions";
/** Return Grok Build runtime and OmniRoute config status. */
export async function GET(request: Request): Promise<Response> {
const authError = await requireCliToolsAuth(request);
if (authError) return authError;
try {
const runtime = await getCliRuntimeStatus(TOOL_ID);
if (!runtime.installed || !runtime.runnable) {
return NextResponse.json({
installed: runtime.installed,
runnable: runtime.runnable,
command: runtime.command,
commandPath: runtime.commandPath,
runtimeMode: runtime.runtimeMode,
reason: runtime.reason,
config: null,
message:
runtime.installed && !runtime.runnable
? "Grok Build is installed but not runnable"
: "Grok Build is not installed",
});
}
const toml = await readConfigToml();
const model = parseModelSection(toml);
const defaultModel = parseModelsDefault(toml);
const configPath = getGrokBuildConfigPath();
const [runtime, toml] = await Promise.all([
getCliRuntimeStatus(TOOL_ID),
readConfigToml(configPath),
]);
const settings = parseGrokBuildConfig(toml);
const publicSettings = omitApiKeys(settings);
const apiKeyConfigured = Boolean(
settings.model?.api_key ||
GROK_SUBAGENT_TYPES.some((type) => settings.subagentModels[type]?.api_key)
);
return NextResponse.json({
installed: runtime.installed,
runnable: runtime.runnable,
command: runtime.command,
commandPath: runtime.commandPath,
runtimeMode: runtime.runtimeMode,
reason: runtime.reason,
config: { model, default: defaultModel },
hasOmniRoute: hasOmniRouteConfig(model),
configPath: getGrokBuildConfigPath(),
...runtime,
config: publicSettings,
settings: publicSettings,
hasOmniRoute: hasOmniRouteConfig(settings),
apiKeyConfigured,
configPath,
});
} catch (err) {
return NextResponse.json({ error: { message: sanitizeErrorMessage(err) } }, { status: 500 });
} catch (error) {
logger.error({ err: error }, "Failed to read Grok Build settings");
return NextResponse.json({ error: { message: sanitizeErrorMessage(error) } }, { status: 500 });
}
}
// POST — write the [model.omniroute] section into ~/.grok/config.toml and set it default
export async function POST(request: Request) {
/** Apply OmniRoute model slots to Grok Build. */
export async function POST(request: Request): Promise<Response> {
const authError = await requireCliToolsAuth(request);
if (authError) return authError;
let rawBody;
let rawBody: unknown;
try {
rawBody = await request.json();
} catch {
return NextResponse.json({ error: { message: "Invalid JSON body" } }, { status: 400 });
}
const validation = grokBuildConfigSchema.safeParse(rawBody);
if (!validation.success) {
return NextResponse.json(
{ error: { message: "Invalid request", details: validation.error.issues } },
{ status: 400 }
);
}
try {
const writeGuard = ensureCliConfigWriteAllowed();
if (writeGuard) {
return NextResponse.json({ error: writeGuard }, { status: 403 });
}
// Extract keyId BEFORE Zod validation — Zod strips unknown fields
const keyId = typeof rawBody?.keyId === "string" ? rawBody.keyId.trim() : null;
const validation = validateBody(cliModelConfigSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { baseUrl, model } = validation.data;
const apiKey = await resolveApiKey(keyId, validation.data.apiKey);
const configPath = getGrokBuildConfigPath();
const grokDir = getGrokBuildDir();
const writeError = guardCliConfigWrite(configPath, { toolLabel: "Grok Build" });
if (writeError) return writeError;
await fs.mkdir(grokDir, { recursive: true });
const apiKey = await resolveApiKey(validation.data.keyId, validation.data.apiKey);
const toml = applyGrokBuildConfig(
await readConfigToml(configPath),
normalizeApplyOptions(validation.data, apiKey)
);
await fs.mkdir(path.dirname(configPath), { recursive: true });
await createBackup(TOOL_ID, configPath);
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
let toml = await readConfigToml();
toml = rememberPrevDefault(toml);
toml = upsertModelSection(toml, buildModelSection(model, normalizedBaseUrl, apiKey || ""));
toml = setModelsDefault(toml, MODEL_SLOT);
await fs.writeFile(configPath, toml, "utf-8");
await writeAtomic(configPath, toml);
try {
saveCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
logger.warn("Failed to record Grok Build config time");
}
return NextResponse.json({
success: true,
message: "Grok Build settings applied successfully!",
configPath,
modelSlot: MODEL_SLOT,
modelSlot: "omniroute",
});
} catch (err) {
return NextResponse.json({ error: { message: sanitizeErrorMessage(err) } }, { status: 500 });
} catch (error) {
if (error instanceof GrokBuildConfigConflictError) {
return NextResponse.json({ error: { message: error.message } }, { status: 409 });
}
logger.error({ err: error }, "Failed to apply Grok Build settings");
return NextResponse.json({ error: { message: sanitizeErrorMessage(error) } }, { status: 500 });
}
}
// DELETE — remove the [model.omniroute] section and restore the previous default
export async function DELETE(request: Request) {
/** Remove OmniRoute model slots from Grok Build. */
export async function DELETE(request: Request): Promise<Response> {
const authError = await requireCliToolsAuth(request);
if (authError) return authError;
try {
const writeGuard = ensureCliConfigWriteAllowed();
if (writeGuard) {
return NextResponse.json({ error: writeGuard }, { status: 403 });
}
const configPath = getGrokBuildConfigPath();
const writeError = guardCliConfigWrite(configPath, { toolLabel: "Grok Build" });
if (writeError) return writeError;
let toml: string;
try {
toml = await fs.readFile(configPath, "utf-8");
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
return NextResponse.json({ success: true, message: "No config file to reset" });
}
throw err;
const current = await readConfigToml(configPath);
if (!current) {
return NextResponse.json({ success: true, message: "No config file to reset" });
}
await createBackup(TOOL_ID, configPath);
toml = removeModelSection(toml);
toml = clearModelsDefaultIfOurs(toml);
await fs.writeFile(configPath, toml, "utf-8");
await writeAtomic(configPath, resetGrokBuildConfig(current));
try {
deleteCliToolLastConfigured(TOOL_ID);
} catch {
/* non-critical */
logger.warn("Failed to clear Grok Build config time");
}
return NextResponse.json({
success: true,
message: "OmniRoute model slot removed from Grok Build",
message: "OmniRoute model slots removed from Grok Build",
});
} catch (err) {
return NextResponse.json({ error: { message: sanitizeErrorMessage(err) } }, { status: 500 });
} catch (error) {
logger.error({ err: error }, "Failed to reset Grok Build settings");
return NextResponse.json({ error: { message: sanitizeErrorMessage(error) } }, { status: 500 });
}
}

View File

@@ -1,8 +1,12 @@
// DRY: shared between /api/cli-tools/status and /api/cli-tools/all-statuses (plan 14 F2)
import fs from "fs/promises";
import { getCliPrimaryConfigPath } from "@/shared/services/cliRuntime";
import { getCliConfigHome, getCliPrimaryConfigPath } from "@/shared/services/cliRuntime";
import { hasOmniRouteQwenCodeConfig } from "@/shared/services/qwenCodeConfig";
import {
parseGrokBuildConfig,
resolveGrokBuildConfigPath,
} from "@/shared/services/grokBuildConfig";
import { getRuntimePorts } from "@/lib/runtime/ports";
const { apiPort } = getRuntimePorts();
@@ -21,11 +25,24 @@ export async function checkToolConfigStatus(
_configPathOverride?: string
): Promise<"configured" | "not_configured" | "not_installed" | "unknown" | "other"> {
try {
const configPath = _configPathOverride ?? getCliPrimaryConfigPath(toolId);
const configPath =
_configPathOverride ??
(toolId === "grok-build"
? resolveGrokBuildConfigPath(process.env, getCliConfigHome())
: getCliPrimaryConfigPath(toolId));
if (!configPath) return "unknown";
const content = await fs.readFile(configPath, "utf-8");
if (toolId === "grok-build") {
const settings = parseGrokBuildConfig(content);
return settings.default === "omniroute" &&
settings.model?.base_url &&
settings.model.api_backend === "chat_completions"
? "configured"
: "not_configured";
}
// Codex uses TOML config — parse as raw text, not JSON
if (toolId === "codex") {
const lower = content.toLowerCase();
@@ -88,8 +105,8 @@ export async function checkToolConfigStatus(
// (user may configure an external domain instead of localhost)
if (
toolId === "cline" &&
((config.actModeApiProvider === "openai" || config.planModeApiProvider === "openai") &&
((config.openAiBaseUrl as string) || "").trim().length > 0)
(config.actModeApiProvider === "openai" || config.planModeApiProvider === "openai") &&
((config.openAiBaseUrl as string) || "").trim().length > 0
) {
return "configured";
}

View File

@@ -0,0 +1,314 @@
import path from "node:path";
import { parse as parseToml } from "smol-toml";
export const GROK_MAIN_MODEL_SLOT = "omniroute";
export const GROK_SUBAGENT_TYPES = ["general-purpose", "explore", "plan"] as const;
export type GrokSubagentType = (typeof GROK_SUBAGENT_TYPES)[number];
export interface GrokModelConfig {
model: string | null;
base_url: string | null;
name: string | null;
api_key: string | null;
api_backend: string | null;
context_window: number | null;
}
export interface GrokBuildSettings {
model: GrokModelConfig | null;
default: string | null;
subagentModels: Record<GrokSubagentType, GrokModelConfig | null>;
subagentMappings: Record<GrokSubagentType, string | null>;
}
export interface GrokBuildApplyOptions {
baseUrl: string;
apiKey?: string | null;
model: string;
contextWindow?: number;
subagentModels?: Partial<Record<GrokSubagentType, { model: string; contextWindow?: number }>>;
}
/** Resolve config.toml from GROK_HOME or the CLI config home. */
export function resolveGrokBuildConfigPath(env: NodeJS.ProcessEnv, configHome: string): string {
const grokHome = env.GROK_HOME?.trim();
if (!grokHome) return path.join(configHome, ".grok", "config.toml");
const hasTraversal = grokHome.split(/[\\/]+/).includes("..");
if (!path.isAbsolute(grokHome) || hasTraversal) {
throw new Error("GROK_HOME must be an absolute path without traversal");
}
if (/[$`;&|<>\r\n\0]/.test(grokHome)) {
throw new Error("GROK_HOME contains invalid characters");
}
return path.join(path.normalize(grokHome), "config.toml");
}
const UNSET_SENTINEL = "__omniroute_unset__";
const MANAGED_MARKER = '# omniroute-managed = "true"';
const LEGACY_DESCRIPTION = "Routed via OmniRoute gateway";
const MODELS_SECTION = "models";
const SUBAGENT_MODELS_SECTION = "subagents.models";
const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const tomlString = (value: string): string => JSON.stringify(String(value));
const modelSlot = (type: GrokSubagentType): string => `${GROK_MAIN_MODEL_SLOT}-${type}`;
const sectionRegExp = (section: string): RegExp =>
new RegExp(`^\\[${escapeRegExp(section)}\\][ \\t]*\\r?\\n((?:(?!\\[)[^\\r\\n]*\\r?\\n?)*)`, "m");
const previousDefaultRegExp = /^# omniroute-prev-default = "([^"]*)"[ \t]*\r?\n?/m;
const previousSubagentRegExp = (type: GrokSubagentType): RegExp =>
new RegExp(`^# omniroute-prev-subagent-${escapeRegExp(type)} = "([^"]*)"[ \\t]*\\r?\\n?`, "m");
const getSectionBody = (toml: string, section: string): string | null =>
toml.match(sectionRegExp(section))?.[1] ?? null;
const getSectionString = (toml: string, section: string, key: string): string | null => {
const body = getSectionBody(toml, section);
if (body === null) return null;
const field = body.match(new RegExp(`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[ \\t]*"([^"]*)"`, "m"));
return field?.[1] ?? null;
};
const getSectionNumber = (toml: string, section: string, key: string): number | null => {
const body = getSectionBody(toml, section);
if (body === null) return null;
const field = body.match(new RegExp(`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[ \\t]*([0-9]+)`, "m"));
if (!field) return null;
const value = Number(field[1]);
return Number.isSafeInteger(value) && value > 0 ? value : null;
};
const setSectionString = (toml: string, section: string, key: string, value: string): string => {
const match = toml.match(sectionRegExp(section));
const line = `${key} = ${tomlString(value)}`;
if (!match) {
const prefix = toml.length > 0 && !toml.endsWith("\n") ? `${toml}\n` : toml;
return `${prefix}${prefix ? "\n" : ""}[${section}]\n${line}\n`;
}
const body = match[1] ?? "";
const fieldRegExp = new RegExp(
`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[^\\r\\n]*(?:\\r?\\n|$)`,
"m"
);
const nextBody = fieldRegExp.test(body)
? body.replace(fieldRegExp, `${line}\n`)
: `${line}\n${body}`;
return toml.replace(match[0], `[${section}]\n${nextBody}`);
};
const deleteSectionField = (toml: string, section: string, key: string): string => {
const match = toml.match(sectionRegExp(section));
if (!match) return toml;
const fieldRegExp = new RegExp(
`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[^\\r\\n]*(?:\\r?\\n|$)`,
"m"
);
const nextBody = (match[1] ?? "").replace(fieldRegExp, "");
if (!nextBody.trim()) return toml.replace(match[0], "").replace(/\n{3,}/g, "\n\n");
return toml.replace(match[0], `[${section}]\n${nextBody}`);
};
const parseModelSection = (toml: string, slot: string): GrokModelConfig | null => {
if (getSectionBody(toml, `model.${slot}`) === null) return null;
return {
model: getSectionString(toml, `model.${slot}`, "model"),
base_url: getSectionString(toml, `model.${slot}`, "base_url"),
name: getSectionString(toml, `model.${slot}`, "name"),
api_key: getSectionString(toml, `model.${slot}`, "api_key"),
api_backend: getSectionString(toml, `model.${slot}`, "api_backend"),
context_window: getSectionNumber(toml, `model.${slot}`, "context_window"),
};
};
const buildModelSection = (options: {
slot: string;
model: string;
baseUrl: string;
apiKey?: string | null;
contextWindow?: number;
name: string;
}): string => {
const lines = [
`[model.${options.slot}]`,
MANAGED_MARKER,
`model = ${tomlString(options.model)}`,
`base_url = ${tomlString(options.baseUrl)}`,
`name = ${tomlString(options.name)}`,
`description = ${tomlString(LEGACY_DESCRIPTION)}`,
'api_backend = "chat_completions"',
];
if (options.apiKey) lines.push(`api_key = ${tomlString(options.apiKey)}`);
if (Number.isSafeInteger(options.contextWindow) && Number(options.contextWindow) > 0) {
lines.push(`context_window = ${Math.floor(Number(options.contextWindow))}`);
}
return `${lines.join("\n")}\n`;
};
const upsertModelSection = (
toml: string,
options: Parameters<typeof buildModelSection>[0]
): string => {
const regexp = sectionRegExp(`model.${options.slot}`);
const section = buildModelSection(options);
if (regexp.test(toml)) return toml.replace(regexp, section);
const prefix = toml.length > 0 && !toml.endsWith("\n") ? `${toml}\n` : toml;
return `${prefix}${prefix ? "\n" : ""}${section}`;
};
const removeModelSection = (toml: string, slot: string): string =>
toml.replace(sectionRegExp(`model.${slot}`), "").replace(/\n{3,}/g, "\n\n");
const insertMarker = (toml: string, marker: string): string => {
const mainSection = sectionRegExp(`model.${GROK_MAIN_MODEL_SLOT}`);
if (mainSection.test(toml)) {
return toml.replace(mainSection, (section) => `${marker}${section}`);
}
const prefix = toml.length > 0 && !toml.endsWith("\n") ? `${toml}\n` : toml;
return `${prefix}${marker}`;
};
const rememberPreviousDefault = (toml: string): string => {
if (previousDefaultRegExp.test(toml)) return toml;
const current = getSectionString(toml, MODELS_SECTION, "default");
const previous = !current || current === "grok-build" ? UNSET_SENTINEL : current;
if (current === GROK_MAIN_MODEL_SLOT) return toml;
return insertMarker(toml, `# omniroute-prev-default = ${tomlString(previous)}\n`);
};
const restorePreviousDefault = (toml: string): string => {
const previous = toml.match(previousDefaultRegExp)?.[1] ?? UNSET_SENTINEL;
let next = toml.replace(previousDefaultRegExp, "");
if (getSectionString(next, MODELS_SECTION, "default") !== GROK_MAIN_MODEL_SLOT) return next;
if (previous === UNSET_SENTINEL || previous === "grok-build") {
return deleteSectionField(next, MODELS_SECTION, "default");
}
return setSectionString(next, MODELS_SECTION, "default", previous);
};
const rememberPreviousSubagent = (toml: string, type: GrokSubagentType): string => {
if (previousSubagentRegExp(type).test(toml)) return toml;
const current = getSectionString(toml, SUBAGENT_MODELS_SECTION, type);
const previous = current ?? UNSET_SENTINEL;
return insertMarker(toml, `# omniroute-prev-subagent-${type} = ${tomlString(previous)}\n`);
};
const restorePreviousSubagent = (toml: string, type: GrokSubagentType): string => {
const regexp = previousSubagentRegExp(type);
const previous = toml.match(regexp)?.[1] ?? UNSET_SENTINEL;
let next = toml.replace(regexp, "");
if (getSectionString(next, SUBAGENT_MODELS_SECTION, type) !== modelSlot(type)) return next;
if (previous === UNSET_SENTINEL) {
return deleteSectionField(next, SUBAGENT_MODELS_SECTION, type);
}
return setSectionString(next, SUBAGENT_MODELS_SECTION, type, previous);
};
const isLegacyOwnedMainSection = (toml: string): boolean => {
const section = parseModelSection(toml, GROK_MAIN_MODEL_SLOT);
if (!section) return false;
const body = getSectionBody(toml, `model.${GROK_MAIN_MODEL_SLOT}`) ?? "";
const keys = [...body.matchAll(/^\s*([A-Za-z0-9_-]+)\s*=/gm)].map((match) => match[1]);
const allowed = new Set(["model", "base_url", "name", "description", "api_backend", "api_key"]);
return (
keys.every((key) => allowed.has(key)) &&
section.model !== null &&
section.base_url !== null &&
section.name === "OmniRoute" &&
section.api_backend === "chat_completions" &&
getSectionString(toml, `model.${GROK_MAIN_MODEL_SLOT}`, "description") === LEGACY_DESCRIPTION
);
};
const assertMainSlotOwnership = (toml: string): void => {
const body = getSectionBody(toml, `model.${GROK_MAIN_MODEL_SLOT}`);
if (body === null || body.includes(MANAGED_MARKER) || isLegacyOwnedMainSection(toml)) return;
throw new GrokBuildConfigConflictError();
};
export class GrokBuildConfigConflictError extends Error {
constructor() {
super("The [model.omniroute] table exists and OmniRoute does not own it");
this.name = "GrokBuildConfigConflictError";
}
}
/** Parse the Grok Build fields that OmniRoute manages. */
export function parseGrokBuildConfig(toml: string): GrokBuildSettings {
if (toml.trim()) parseToml(toml);
const subagentModels = {} as Record<GrokSubagentType, GrokModelConfig | null>;
const subagentMappings = {} as Record<GrokSubagentType, string | null>;
for (const type of GROK_SUBAGENT_TYPES) {
const mapping = getSectionString(toml, SUBAGENT_MODELS_SECTION, type);
subagentMappings[type] = mapping;
subagentModels[type] = mapping === modelSlot(type) ? parseModelSection(toml, mapping) : null;
}
return {
model: parseModelSection(toml, GROK_MAIN_MODEL_SLOT),
default: getSectionString(toml, MODELS_SECTION, "default"),
subagentModels,
subagentMappings,
};
}
/** Apply the OmniRoute model slots and preserve unrelated TOML text. */
export function applyGrokBuildConfig(toml: string, options: GrokBuildApplyOptions): string {
if (toml.trim()) parseToml(toml);
assertMainSlotOwnership(toml);
let next = rememberPreviousDefault(toml);
next = upsertModelSection(next, {
slot: GROK_MAIN_MODEL_SLOT,
model: options.model,
baseUrl: options.baseUrl,
apiKey: options.apiKey,
contextWindow: options.contextWindow,
name: "OmniRoute",
});
next = setSectionString(next, MODELS_SECTION, "default", GROK_MAIN_MODEL_SLOT);
if (options.subagentModels !== undefined) {
for (const type of GROK_SUBAGENT_TYPES) {
const selected = options.subagentModels[type];
const slot = modelSlot(type);
if (selected?.model) {
next = rememberPreviousSubagent(next, type);
next = upsertModelSection(next, {
slot,
model: selected.model,
baseUrl: options.baseUrl,
apiKey: options.apiKey,
contextWindow: selected.contextWindow,
name: `OmniRoute ${type}`,
});
next = setSectionString(next, SUBAGENT_MODELS_SECTION, type, slot);
} else {
next = restorePreviousSubagent(next, type);
next = removeModelSection(next, slot);
}
}
}
return next;
}
/** Remove the OmniRoute model slots and restore values that users did not change. */
export function resetGrokBuildConfig(toml: string): string {
if (toml.trim()) parseToml(toml);
let next = toml;
for (const type of GROK_SUBAGENT_TYPES) {
next = restorePreviousSubagent(next, type);
next = removeModelSection(next, modelSlot(type));
}
next = removeModelSection(next, GROK_MAIN_MODEL_SLOT);
next = restorePreviousDefault(next);
return next.replace(/\n{3,}/g, "\n\n");
}
/** Return the managed slot for a Grok Build subagent type. */
export function getGrokSubagentSlot(type: string): string | null {
return GROK_SUBAGENT_TYPES.includes(type as GrokSubagentType)
? modelSlot(type as GrokSubagentType)
: null;
}

View File

@@ -254,3 +254,40 @@ test("refresh=true bypasses a matching cached CLI result", async () => {
const body = (await response.json()) as Record<string, { detection?: { version?: string } }>;
assert.notEqual(body[toolId]?.detection?.version, cachedVersion);
});
test("grok-build status uses GROK_HOME and returns its managed endpoint", async () => {
const grokHome = fs.mkdtempSync(path.join(os.tmpdir(), "all-statuses-grok-home-"));
const original = process.env.GROK_HOME;
process.env.GROK_HOME = grokHome;
try {
fs.writeFileSync(
path.join(grokHome, "config.toml"),
[
"[models]",
'default = "omniroute"',
"",
"[model.omniroute]",
'model = "openai/gpt-5.5"',
'base_url = "https://gateway.example/v1"',
'api_backend = "chat_completions"',
"",
].join("\n")
);
const response = await allStatusesRoute.GET(
new Request("http://localhost/api/cli-tools/all-statuses?refresh=true")
);
assert.equal(response.status, 200);
const body = (await response.json()) as Record<
string,
{ config?: { status?: string; endpoint?: string | null } }
>;
if (body["grok-build"]?.config?.status !== "not_installed") {
assert.equal(body["grok-build"]?.config?.status, "configured");
}
assert.equal(body["grok-build"]?.config?.endpoint, "https://gateway.example/v1");
} finally {
if (original === undefined) delete process.env.GROK_HOME;
else process.env.GROK_HOME = original;
fs.rmSync(grokHome, { recursive: true, force: true });
}
});

View File

@@ -20,15 +20,21 @@ const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-grok-buil
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.API_KEY_SECRET = "test-api-key-secret-grok-build";
process.env.JWT_SECRET = "test-jwt-secret-grok-build";
// guardCliConfigWrite refuses a write when the target isn't bind-mounted from
// the host inside a real container (see cliConfigWriteGuard.ts). This test
// suite runs inside CI/devbox containers with no such mount for its tmpdir
// fixtures, so allow the write here — the refusal path itself is covered by
// tests/unit/cli-tools-apply-container-422.test.ts.
process.env.OMNIROUTE_ALLOW_CONTAINER_CONFIG_WRITE = "1";
// Import DB reset helpers (must be before route import)
const core = await import("../../src/lib/db/core.ts");
const localDb = await import("../../src/lib/localDb.ts");
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
// Import route handlers
const { GET, POST, DELETE } = await import(
"../../src/app/api/cli-tools/grok-build-settings/route.ts"
);
const { GET, POST, DELETE } =
await import("../../src/app/api/cli-tools/grok-build-settings/route.ts");
async function resetStorage() {
delete process.env.INITIAL_PASSWORD;
@@ -149,10 +155,10 @@ test("grok-build-settings POST: writes [model.omniroute] section and preserves e
preservedLines.includes('base_url = "https://example.test/v1"'),
"Pre-existing unrelated [model.*] section must be preserved"
);
// The previous default must be remembered for Reset to restore.
// The obsolete built-in default must become an absent-value sentinel.
assert.ok(
content.includes('omniroute-prev-default = "grok-build"'),
"Previous default should be remembered as a marker comment"
content.includes('omniroute-prev-default = "__omniroute_unset__"'),
"An obsolete default must use the absent-value sentinel"
);
}
} finally {
@@ -175,7 +181,7 @@ test("grok-build-settings DELETE: removes our section, preserves the rest, resto
"[models]",
'default = "omniroute"',
"",
"# omniroute-prev-default = \"grok-build\"",
'# omniroute-prev-default = "grok-build"',
"[model.omniroute]",
'model = "grok-4.5"',
'base_url = "http://localhost:20128/v1"',
@@ -209,7 +215,7 @@ test("grok-build-settings DELETE: removes our section, preserves the rest, resto
survivingLines.includes('base_url = "https://example.test/v1"'),
"Unrelated section must survive"
);
assert.ok(content.includes('default = "grok-build"'), "Previous default should be restored");
assert.ok(!/^default\s*=/m.test(content), "The obsolete default must not be restored");
}
} finally {
process.env.HOME = origHome;
@@ -235,6 +241,111 @@ test("grok-build-settings DELETE: no-op success when no config file exists", asy
}
});
test("grok-build-settings: honors GROK_HOME and rejects a relative value", async () => {
const grokHome = fs.mkdtempSync(path.join(os.tmpdir(), "grok-build-env-home-"));
const original = process.env.GROK_HOME;
try {
process.env.GROK_HOME = grokHome;
const res = await POST(
new Request("http://localhost/api/cli-tools/grok-build-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl: "http://localhost:20128/v1/v1/",
apiKey: "sk-private",
model: "openai/gpt-5.5",
contextWindow: 321000,
subagentModels: {
explore: { model: "xai/grok-4", contextWindow: 256000 },
},
}),
})
);
assert.equal(res.status, 200);
const configPath = path.join(grokHome, "config.toml");
const content = fs.readFileSync(configPath, "utf8");
assert.match(content, /base_url = "http:\/\/localhost:20128\/v1"/);
assert.match(content, /context_window = 321000/);
assert.match(content, /\[model\.omniroute-explore\]/);
assert.match(content, /explore = "omniroute-explore"/);
const getRes = await GET(new Request("http://localhost/api/cli-tools/grok-build-settings"));
assert.equal(getRes.status, 200);
const body = await getRes.json();
assert.equal(body.apiKeyConfigured, true);
assert.equal("api_key" in body.config.model, false);
assert.equal("api_key" in body.config.subagentModels.explore, false);
assert.doesNotMatch(JSON.stringify(body), /sk-private/);
process.env.GROK_HOME = "relative/path";
const invalid = await POST(
new Request("http://localhost/api/cli-tools/grok-build-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ baseUrl: "http://localhost:20128", model: "xai/grok-4" }),
})
);
assert.equal(invalid.status, 500);
} finally {
if (original === undefined) delete process.env.GROK_HOME;
else process.env.GROK_HOME = original;
fs.rmSync(grokHome, { recursive: true, force: true });
}
});
test("grok-build-settings POST: returns 409 for an unowned omniroute slot", async () => {
const grokHome = fs.mkdtempSync(path.join(os.tmpdir(), "grok-build-conflict-"));
const original = process.env.GROK_HOME;
process.env.GROK_HOME = grokHome;
try {
fs.writeFileSync(
path.join(grokHome, "config.toml"),
'[model.omniroute]\nmodel = "private"\nbase_url = "https://example.test/v1"\n'
);
const res = await POST(
new Request("http://localhost/api/cli-tools/grok-build-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ baseUrl: "http://localhost:20128", model: "xai/grok-4" }),
})
);
assert.equal(res.status, 409);
} finally {
if (original === undefined) delete process.env.GROK_HOME;
else process.env.GROK_HOME = original;
fs.rmSync(grokHome, { recursive: true, force: true });
}
});
test("grok-build-settings POST: resolves keyId to an unmasked key", async () => {
const grokHome = fs.mkdtempSync(path.join(os.tmpdir(), "grok-build-key-id-"));
const original = process.env.GROK_HOME;
process.env.GROK_HOME = grokHome;
try {
const key = await apiKeysDb.createApiKey("Grok Build key", "grok-build-test-machine");
const res = await POST(
new Request("http://localhost/api/cli-tools/grok-build-settings", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
baseUrl: "http://localhost:20128",
keyId: key.id,
apiKey: "sk_****",
model: "openai/gpt-5.5",
}),
})
);
assert.equal(res.status, 200);
const content = fs.readFileSync(path.join(grokHome, "config.toml"), "utf8");
assert.match(content, new RegExp(`api_key = ${JSON.stringify(key.key)}`));
assert.doesNotMatch(content, /sk_\*\*\*\*/);
} finally {
if (original === undefined) delete process.env.GROK_HOME;
else process.env.GROK_HOME = original;
fs.rmSync(grokHome, { recursive: true, force: true });
}
});
// ── Test 6: Error sanitization (Hard Rule #12) ───────────────────────────────
test("grok-build-settings: error responses do not leak stack traces", async () => {

View File

@@ -40,11 +40,7 @@ async function writeCodexConfig(opts: {
if (opts.authApiKey !== undefined) {
const authPath = path.join(tmpDir, "auth.json");
await fs.writeFile(
authPath,
JSON.stringify({ OPENAI_API_KEY: opts.authApiKey }),
"utf-8"
);
await fs.writeFile(authPath, JSON.stringify({ OPENAI_API_KEY: opts.authApiKey }), "utf-8");
}
return configPath;
@@ -62,10 +58,7 @@ test("claude: returns 'configured' when ANTHROPIC_BASE_URL is set", async () =>
});
test("claude: returns 'not_configured' when ANTHROPIC_BASE_URL is absent", async () => {
const configPath = await writeTempFile(
"settings.json",
JSON.stringify({ env: {} })
);
const configPath = await writeTempFile("settings.json", JSON.stringify({ env: {} }));
const result = await checkToolConfigStatus("claude", configPath);
assert.equal(result, "not_configured");
});
@@ -116,6 +109,29 @@ test("hermes: returns 'not_configured' when config points elsewhere", async () =
assert.equal(result, "not_configured");
});
test("grok-build: requires the managed default and chat completions backend", async () => {
const configured = await writeTempFile(
"config.toml",
[
"[models]",
'default = "omniroute"',
"",
"[model.omniroute]",
'model = "openai/gpt-5.5"',
'base_url = "https://gateway.example/v1"',
'api_backend = "chat_completions"',
"",
].join("\n")
);
assert.equal(await checkToolConfigStatus("grok-build", configured), "configured");
const inactive = await writeTempFile(
"config.toml",
'[models]\ndefault = "custom"\n\n[model.omniroute]\nbase_url = "https://gateway.example/v1"\n'
);
assert.equal(await checkToolConfigStatus("grok-build", inactive), "not_configured");
});
// ── Droid / Openclaw / Kilo ───────────────────────────────────────────────────
test("droid: returns 'configured' when JSON config contains sk_omniroute marker", async () => {
@@ -205,10 +221,7 @@ test("error path: non-existent file returns 'not_configured' (no throw)", async
test("unknown toolId: returns 'unknown' (no configPath for unknown tool)", async () => {
// unknown tool has no config path via getCliPrimaryConfigPath — configPathOverride not needed
// but we can also test via override with a valid JSON file to hit the default branch
const configPath = await writeTempFile(
"unknown.json",
JSON.stringify({ foo: "bar" })
);
const configPath = await writeTempFile("unknown.json", JSON.stringify({ foo: "bar" }));
const result = await checkToolConfigStatus("totally-unknown-tool-id", configPath);
assert.equal(result, "unknown");
});

View File

@@ -0,0 +1,165 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
applyGrokBuildConfig,
GrokBuildConfigConflictError,
parseGrokBuildConfig,
resetGrokBuildConfig,
} from "../../src/shared/services/grokBuildConfig.ts";
const main = {
baseUrl: "http://localhost:20128/v1",
apiKey: "sk-test",
model: "openai/gpt-5.5",
contextWindow: 400000,
};
test("apply adds the main slot and preserves unrelated TOML", () => {
const source = [
"# user comment",
"[models]",
'default = "custom"',
'theme = "dark"',
"",
"[model.custom]",
'model = "custom-model"',
'base_url = "https://example.test/v1"',
"",
].join("\n");
const result = applyGrokBuildConfig(source, main);
const parsed = parseGrokBuildConfig(result);
assert.equal(parsed.default, "omniroute");
assert.equal(parsed.model?.model, main.model);
assert.equal(parsed.model?.context_window, main.contextWindow);
assert.match(result, /# omniroute-prev-default = "custom"/);
assert.match(result, /# user comment/);
assert.match(result, /theme = "dark"/);
assert.match(result, /\[model\.custom\]/);
});
test("apply records an absent default once and reset removes it", () => {
const appliedTwice = applyGrokBuildConfig(applyGrokBuildConfig("[ui]\ncompact = true\n", main), {
...main,
model: "anthropic/claude-opus-4-1",
});
assert.equal(appliedTwice.match(/omniroute-prev-default/g)?.length, 1);
const reset = resetGrokBuildConfig(appliedTwice);
assert.doesNotMatch(reset, /^default\s*=/m);
assert.doesNotMatch(reset, /\[model\.omniroute\]/);
assert.match(reset, /\[ui\]\ncompact = true/);
});
test("reset never restores the obsolete grok-build default", () => {
const source = [
"[models]",
'default = "omniroute"',
"",
'# omniroute-prev-default = "grok-build"',
"[model.omniroute]",
'# omniroute-managed = "true"',
'model = "openai/gpt-5.5"',
'base_url = "http://localhost:20128/v1"',
'name = "OmniRoute"',
'description = "Routed via OmniRoute gateway"',
'api_backend = "chat_completions"',
"",
].join("\n");
const result = resetGrokBuildConfig(source);
assert.doesNotMatch(result, /^default\s*=/m);
assert.doesNotMatch(result, /grok-build/);
});
test("apply manages all subagent slots and keeps context windows", () => {
const result = applyGrokBuildConfig(
["[subagents.models]", 'general-purpose = "old-general"', 'explore = "old-explore"', ""].join(
"\n"
),
{
...main,
subagentModels: {
"general-purpose": { model: "google/gemini-2.5-pro", contextWindow: 1048576 },
plan: { model: "anthropic/claude-sonnet-4", contextWindow: 200000 },
},
}
);
const parsed = parseGrokBuildConfig(result);
assert.equal(parsed.subagentMappings["general-purpose"], "omniroute-general-purpose");
assert.equal(parsed.subagentMappings.explore, "old-explore");
assert.equal(parsed.subagentMappings.plan, "omniroute-plan");
assert.equal(parsed.subagentModels["general-purpose"]?.context_window, 1048576);
assert.equal(parsed.subagentModels.plan?.context_window, 200000);
assert.match(result, /omniroute-prev-subagent-general-purpose = "old-general"/);
assert.match(result, /omniroute-prev-subagent-plan = "__omniroute_unset__"/);
});
test("an absent subagentModels property preserves current subagent values", () => {
const source = applyGrokBuildConfig("", {
...main,
subagentModels: { explore: { model: "xai/grok-4", contextWindow: 256000 } },
});
const result = applyGrokBuildConfig(source, { ...main, model: "openai/gpt-5.5-codex" });
assert.equal(parseGrokBuildConfig(result).subagentModels.explore?.model, "xai/grok-4");
});
test("an empty subagentModels object removes all managed overrides", () => {
const source = applyGrokBuildConfig('[subagents.models]\nexplore = "user-explore"\n', {
...main,
subagentModels: {
explore: { model: "xai/grok-4", contextWindow: 256000 },
plan: { model: "openai/gpt-5.5", contextWindow: 400000 },
},
});
const result = applyGrokBuildConfig(source, { ...main, subagentModels: {} });
const parsed = parseGrokBuildConfig(result);
assert.equal(parsed.subagentMappings.explore, "user-explore");
assert.equal(parsed.subagentMappings.plan, null);
assert.doesNotMatch(result, /\[model\.omniroute-(?:explore|plan)\]/);
});
test("reset restores only mappings that still reference managed slots", () => {
let source = applyGrokBuildConfig('[subagents.models]\nexplore = "old-explore"\n', {
...main,
subagentModels: { explore: { model: "xai/grok-4", contextWindow: 256000 } },
});
source = source.replace('explore = "omniroute-explore"', 'explore = "user-changed-explore"');
const result = resetGrokBuildConfig(source);
assert.match(result, /explore = "user-changed-explore"/);
assert.doesNotMatch(result, /omniroute-prev-subagent/);
});
test("apply accepts the exact legacy OmniRoute table", () => {
const legacy = [
"[model.omniroute]",
'model = "grok-4.5"',
'base_url = "http://localhost:20128/v1"',
'name = "OmniRoute"',
'description = "Routed via OmniRoute gateway"',
'api_backend = "chat_completions"',
'api_key = "sk-old"',
"",
].join("\n");
assert.doesNotThrow(() => applyGrokBuildConfig(legacy, main));
});
test("apply rejects an unowned model.omniroute table", () => {
const source = [
"[model.omniroute]",
'model = "private-model"',
'base_url = "https://example.test/v1"',
'name = "User model"',
'api_backend = "chat_completions"',
"",
].join("\n");
assert.throws(() => applyGrokBuildConfig(source, main), GrokBuildConfigConflictError);
});

View File

@@ -0,0 +1,264 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
let manualConfig = "";
vi.mock("@/shared/components", () => ({
Card: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Button: ({ children, onClick, disabled }: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button type="button" onClick={onClick} disabled={disabled}>
{children}
</button>
),
ModelSelectModal: () => null,
ManualConfigModal: ({
isOpen,
configs,
}: {
isOpen: boolean;
configs: Array<{ content: string }>;
}) => {
manualConfig = configs[0]?.content ?? "";
return isOpen ? <div data-testid="manual-toml">{manualConfig}</div> : null;
},
}));
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const { default: GrokBuildToolCard } =
await import("@/app/(dashboard)/dashboard/cli-code/components/GrokBuildToolCard");
let container: HTMLElement;
let root: Root;
const response = (body: unknown, ok = true) => ({ ok, json: async () => body });
const renderCard = async () => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
await act(async () => {
root.render(
<GrokBuildToolCard
tool={{ name: "Grok Build", description: "Custom model host" }}
isExpanded
apiKeys={[{ id: "key-1", name: "Dashboard key", key: "sk_****" }]}
activeProviders={[{ provider: "openai" }]}
hasActiveProviders
availableModels={[{ value: "openai/gpt-5.5" }]}
/>
);
await Promise.resolve();
await Promise.resolve();
});
};
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
manualConfig = "";
localStorage.clear();
localStorage.setItem(
"omniroute.grokBuildEndpointPresets",
JSON.stringify([{ name: "Saved office", baseUrl: "https://office.example" }])
);
fetchMock.mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url === "/api/cli-tools/grok-build-settings" && init?.method === "POST") {
return Promise.resolve(response({ success: true }));
}
if (url === "/api/cli-tools/grok-build-settings" && init?.method === "DELETE") {
return Promise.resolve(response({ success: true }));
}
if (url === "/api/cli-tools/grok-build-settings") {
return Promise.resolve(
response({
installed: true,
runnable: true,
hasOmniRoute: true,
config: {
model: {
model: "openai/gpt-5.5",
base_url: "http://127.0.0.1:30200/v1",
context_window: 400000,
},
subagentModels: {},
},
})
);
}
if (url === "/api/settings") {
return Promise.resolve(
response({
apiPort: 30200,
cloudUrl: "https://cloud.example",
machineId: "machine-1",
})
);
}
if (url === "/api/tunnels/cloudflared") {
return Promise.resolve(response({ apiUrl: "https://cf.example/v1" }));
}
if (url === "/api/tunnels/tailscale") {
return Promise.resolve(response({ tunnelUrl: "https://tail.example" }));
}
if (url === "/api/tunnels/ngrok") {
return Promise.resolve(response({ publicUrl: "https://ngrok.example" }));
}
if (url === "/api/cli-tools/backups?tool=grok-build") {
return Promise.resolve(response({ backups: [] }));
}
return Promise.resolve(response({}));
});
});
afterEach(() => {
act(() => root?.unmount());
container?.remove();
vi.clearAllMocks();
});
describe("GrokBuildToolCard", () => {
it("loads local, cloud, tunnel, saved, and custom endpoint choices", async () => {
await renderCard();
const text = container.textContent ?? "";
expect(text).toContain("http://127.0.0.1:30200/v1");
expect(text).toContain("https://cloud.example/machine-1/v1");
expect(text).toContain("https://cf.example/v1");
expect(text).toContain("https://tail.example/v1");
expect(text).toContain("https://ngrok.example/v1");
expect(text).toContain("Saved office");
expect(text).toContain("Custom");
});
it("sends keyId and the complete subagent set on Apply", async () => {
await renderCard();
const inputs = Array.from(container.querySelectorAll("input"));
const explore = inputs.find((input) => input.getAttribute("aria-label") === "Explore model");
await act(async () => {
if (!explore) throw new Error("Explore model input is missing");
const trackedValue = (
explore as HTMLInputElement & { _valueTracker?: { setValue(value: string): void } }
)._valueTracker;
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
setter?.call(explore, "xai/grok-4");
trackedValue?.setValue("");
explore.dispatchEvent(new Event("input", { bubbles: true }));
await Promise.resolve();
});
const apply = Array.from(container.querySelectorAll("button")).find((button) =>
button.textContent?.includes("Apply")
);
await act(async () => apply?.click());
const call = fetchMock.mock.calls.find(
([url, init]) => url === "/api/cli-tools/grok-build-settings" && init?.method === "POST"
);
const body = JSON.parse(String(call?.[1]?.body));
expect(body.keyId).toBe("key-1");
expect(body.apiKey).toBeUndefined();
expect(body.model).toBe("openai/gpt-5.5");
expect(body.subagentModels).toEqual({ explore: { model: "xai/grok-4" } });
});
it("resets settings and shows manual TOML without a real key", async () => {
await renderCard();
const buttons = Array.from(container.querySelectorAll("button"));
await act(async () => buttons.find((button) => button.textContent?.includes("Reset"))?.click());
expect(fetchMock).toHaveBeenCalledWith(
"/api/cli-tools/grok-build-settings",
expect.objectContaining({ method: "DELETE" })
);
await act(async () =>
buttons.find((button) => button.textContent?.includes("Manual Config"))?.click()
);
expect(container.querySelector("[data-testid='manual-toml']")).not.toBeNull();
expect(manualConfig).toContain("<API_KEY_FROM_DASHBOARD>");
expect(manualConfig).toContain('api_backend = "chat_completions"');
expect(manualConfig).not.toContain("sk_****");
});
it("shows API errors", async () => {
fetchMock.mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
if (String(input) === "/api/cli-tools/grok-build-settings" && init?.method === "POST") {
return Promise.resolve(response({ error: { message: "Conflict" } }, false));
}
if (String(input) === "/api/settings") {
return Promise.resolve(response({ apiPort: 30200 }));
}
if (String(input) === "/api/cli-tools/grok-build-settings") {
return Promise.resolve(
response({
installed: true,
runnable: true,
hasOmniRoute: false,
config: { model: { model: "openai/gpt-5.5" } },
})
);
}
return Promise.resolve(response({}));
});
await renderCard();
const apply = Array.from(container.querySelectorAll("button")).find((button) =>
button.textContent?.includes("Apply")
);
await act(async () => apply?.click());
expect(container.querySelector("[role='status']")?.textContent).toContain("Conflict");
});
it("uses the standard CLI card rows and restores backups", async () => {
fetchMock.mockImplementation((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url === "/api/cli-tools/backups" && init?.method === "POST") {
return Promise.resolve(response({ success: true }));
}
if (url === "/api/cli-tools/backups?tool=grok-build") {
return Promise.resolve(
response({ backups: [{ id: "config_2026.toml", createdAt: "2026-08-20T00:00:00Z" }] })
);
}
if (url === "/api/settings") return Promise.resolve(response({ apiPort: 30200 }));
if (url === "/api/cli-tools/grok-build-settings") {
return Promise.resolve(
response({
installed: true,
runnable: true,
hasOmniRoute: true,
config: {
model: {
model: "openai/gpt-5.5",
base_url: "http://127.0.0.1:30200/v1",
},
subagentModels: {},
},
})
);
}
return Promise.resolve(response({}));
});
await renderCard();
expect(container.textContent).toContain("Base URL");
expect(container.textContent).toContain("Subagent model overrides");
const backups = Array.from(container.querySelectorAll("button")).find((button) =>
button.textContent?.includes("Backups")
);
await act(async () => backups?.click());
const restore = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "Restore"
);
await act(async () => restore?.click());
const call = fetchMock.mock.calls.find(
([url, init]) => url === "/api/cli-tools/backups" && init?.method === "POST"
);
expect(JSON.parse(String(call?.[1]?.body))).toEqual({
tool: "grok-build",
backupId: "config_2026.toml",
});
});
});

View File

@@ -73,6 +73,17 @@ vi.mock("@/shared/constants/cliTools", () => ({
baseUrlSupport: "full",
defaultModels: [],
},
"grok-build": {
id: "grok-build",
name: "Grok Build",
icon: "terminal",
color: "#1DA1F2",
category: "code",
configType: "custom",
vendor: "xAI",
baseUrlSupport: "full",
defaultModels: [],
},
"hermes-agent": {
id: "hermes-agent",
name: "Hermes Agent",
@@ -125,6 +136,7 @@ vi.mock("../../../src/app/(dashboard)/dashboard/cli-code/components/index", () =
CopilotToolCard: () => <div data-testid="CopilotToolCard" />,
CustomCliCard: () => <div data-testid="CustomCliCard" />,
HermesAgentToolCard: () => <div data-testid="HermesAgentToolCard" />,
GrokBuildToolCard: () => <div data-testid="GrokBuildToolCard" />,
}));
vi.mock("../../../src/app/(dashboard)/dashboard/cli-code/components/CliproxyapiToolCard", () => ({
@@ -288,6 +300,13 @@ describe("ToolDetailClient", () => {
expect(card!.getAttribute("data-toolid")).toBe("forge");
});
it("renders GrokBuildToolCard for grok-build", async () => {
const container = renderDetail("grok-build", "code");
await act(async () => {});
expect(container.querySelector("[data-testid='GrokBuildToolCard']")).not.toBeNull();
expect(container.querySelector("[data-testid='DefaultToolCard']")).toBeNull();
});
it("renders nothing (null) for completely unknown toolId", async () => {
const container = renderDetail("totally-unknown-xyz", "code");
await act(async () => {});