feat(cli-tools): configure Grok Build subagent models

Add separate model selectors for Grok Build main, general-purpose,
explore, and plan agents. Each override gets an independent 9Router
custom-model slot and context_window derived from 9Router model
capabilities. Preserve and restore pre-existing config on reset.
This commit is contained in:
rixzkiye
2026-07-19 10:44:17 +07:00
parent 0513bf393f
commit e0ba667450
7 changed files with 679 additions and 337 deletions

View File

@@ -2,7 +2,7 @@
## Features
- **xAI**: Grok Imagine video generation (`/v1/videos`) + CLI
- **CLI tools**: Grok Build setup — writes `[model.9router]` to `~/.grok/config.toml`
- **CLI tools**: Grok Build setup — choose separate main/general-purpose/explore/plan models and preserve each model's context window
- **GitHub Copilot**: route Claude models through Copilot's native `/v1/messages`
- **Kiro**: add GPT-5.6 model family (#2596)
- **RTK**: `X-9Router-Token-Saver` header to bypass token savers per request

View File

@@ -1,7 +1,8 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef, useCallback } from "react";
import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components";
import { useModelCaps } from "@/shared/hooks/useModelCaps";
import Image from "next/image";
import BaseUrlSelect from "./BaseUrlSelect";
import ApiKeySelect from "./ApiKeySelect";
@@ -9,12 +10,59 @@ import { matchKnownEndpoint } from "./cliEndpointMatch";
const ENDPOINT = "/api/cli-tools/grok-build-settings";
const MODEL_SLOT = "9router";
const SUBAGENT_TYPES = [
{ id: "general-purpose", label: "General-purpose", help: "Implementation, testing, and full-capability delegated tasks" },
{ id: "explore", label: "Explore", help: "Read-only codebase research and investigation" },
{ id: "plan", label: "Plan", help: "Architecture and implementation planning" },
];
function ModelField({ label, value, placeholder, onChange, onSelect, disabled, help }) {
return (
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="sm:text-right">
<span className="text-xs font-semibold text-text-main sm:text-sm">{label}</span>
{help && <p className="mt-0.5 text-[10px] leading-tight text-text-muted">{help}</p>}
</div>
<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={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
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"
/>
{value && (
<button
type="button"
onClick={() => onChange("")}
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 (inherit main model for subagents)"
>
<span className="material-symbols-outlined text-[14px]">close</span>
</button>
)}
</div>
<button
type="button"
onClick={onSelect}
disabled={disabled}
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 ${
!disabled
? "bg-surface border-border text-text-main hover:border-primary cursor-pointer"
: "opacity-50 cursor-not-allowed border-border"
}`}
>
Select
</button>
</div>
);
}
export default function GrokBuildToolCard({
tool,
isExpanded,
onToggle,
baseUrl,
hasActiveProviders,
apiKeys,
activeProviders,
@@ -25,47 +73,49 @@ export default function GrokBuildToolCard({
tailscaleEnabled,
tailscaleUrl,
}) {
const { getCaps } = useModelCaps();
const getContextWindow = (model) => getCaps(model)?.contextWindow || null;
const initialModel = initialStatus?.settings?.model?.model || "";
const initialSubagents = Object.fromEntries(
SUBAGENT_TYPES
.map((type) => [type.id, initialStatus?.settings?.subagentModels?.[type.id]?.model])
.filter(([, model]) => Boolean(model)),
);
const [grokStatus, setGrokStatus] = useState(initialStatus || null);
const [checking, setChecking] = useState(false);
const [applying, setApplying] = useState(false);
const [restoring, setRestoring] = useState(false);
const [message, setMessage] = useState(null);
const [selectedApiKey, setSelectedApiKey] = useState("");
const [selectedModel, setSelectedModel] = useState("");
const [modalOpen, setModalOpen] = useState(false);
const [selectedApiKey, setSelectedApiKey] = useState(apiKeys?.[0]?.key || "");
const [selectedModel, setSelectedModel] = useState(initialModel);
const [subagentModels, setSubagentModels] = useState(initialSubagents);
const [modelTarget, setModelTarget] = useState(null); // "main" or subagent type
const [modelAliases, setModelAliases] = useState({});
const [showManualConfigModal, setShowManualConfigModal] = useState(false);
const [customBaseUrl, setCustomBaseUrl] = useState("");
const hasInitializedModel = useRef(false);
const hasFetchedStatus = useRef(Boolean(initialStatus));
const getConfigStatus = () => {
if (!grokStatus?.installed) return null;
const cfg = grokStatus.settings?.model;
if (!cfg?.base_url) return "not_configured";
if (matchKnownEndpoint(cfg.base_url, { tunnelPublicUrl, tailscaleUrl })) return "configured";
return "other";
};
const configuredModel = grokStatus?.settings?.model;
const configStatus = !grokStatus?.installed
? null
: !configuredModel?.base_url
? "not_configured"
: matchKnownEndpoint(configuredModel.base_url, { tunnelPublicUrl, tailscaleUrl })
? "configured"
: "other";
const configStatus = getConfigStatus();
const hydrateForm = useCallback((status) => {
const mainModel = status?.settings?.model?.model || "";
const configuredSubagents = Object.fromEntries(
SUBAGENT_TYPES
.map((type) => [type.id, status?.settings?.subagentModels?.[type.id]?.model])
.filter(([, model]) => Boolean(model)),
);
setSelectedModel(mainModel);
setSubagentModels(configuredSubagents);
}, []);
useEffect(() => {
if (apiKeys?.length > 0 && !selectedApiKey) {
setSelectedApiKey(apiKeys[0].key);
}
}, [apiKeys, selectedApiKey]);
useEffect(() => {
if (initialStatus) setGrokStatus(initialStatus);
}, [initialStatus]);
useEffect(() => {
if (isExpanded) {
if (!grokStatus) checkStatus();
fetchModelAliases();
}
}, [isExpanded]);
const fetchModelAliases = async () => {
const fetchModelAliases = useCallback(async () => {
try {
const res = await fetch("/api/models/alias");
const data = await res.json();
@@ -73,40 +123,38 @@ export default function GrokBuildToolCard({
} catch (error) {
console.log("Error fetching model aliases:", error);
}
};
}, []);
useEffect(() => {
if (grokStatus?.installed && !hasInitializedModel.current) {
hasInitializedModel.current = true;
const cfg = grokStatus.settings?.model;
if (cfg?.model) setSelectedModel(cfg.model);
}
}, [grokStatus]);
const checkStatus = async () => {
const checkStatus = useCallback(async ({ hydrate = false } = {}) => {
setChecking(true);
try {
const res = await fetch(ENDPOINT);
const data = await res.json();
setGrokStatus(data);
const status = await res.json();
setGrokStatus(status);
hasFetchedStatus.current = true;
if (hydrate) hydrateForm(status);
} catch (error) {
setGrokStatus({ installed: false, error: error.message });
} finally {
setChecking(false);
}
};
}, [hydrateForm]);
const normalizeLocalhost = (url) => url.replace("://localhost", "://127.0.0.1");
const getLocalBaseUrl = () => {
if (typeof window !== "undefined") {
return normalizeLocalhost(window.location.origin);
}
return "http://127.0.0.1:20128";
};
useEffect(() => {
if (!isExpanded) return;
let cancelled = false;
const synchronize = async () => {
if (!hasFetchedStatus.current) await checkStatus({ hydrate: true });
if (!cancelled) await fetchModelAliases();
};
synchronize();
return () => { cancelled = true; };
}, [isExpanded, checkStatus, fetchModelAliases]);
const getEffectiveBaseUrl = () => {
const url = customBaseUrl || getLocalBaseUrl();
const url = customBaseUrl || (typeof window !== "undefined"
? window.location.origin.replace("://localhost", "://127.0.0.1")
: "http://127.0.0.1:20128");
return url.endsWith("/v1") ? url : `${url}/v1`;
};
@@ -117,6 +165,11 @@ export default function GrokBuildToolCard({
const keyToUse = selectedApiKey?.trim()
|| (apiKeys?.length > 0 ? apiKeys[0].key : null)
|| (!cloudEnabled ? "sk_9router" : null);
const mappedSubagents = {};
for (const type of SUBAGENT_TYPES) {
const model = subagentModels[type.id]?.trim();
if (model) mappedSubagents[type.id] = { model, contextWindow: getContextWindow(model) };
}
const res = await fetch(ENDPOINT, {
method: "POST",
@@ -125,11 +178,13 @@ export default function GrokBuildToolCard({
baseUrl: getEffectiveBaseUrl(),
apiKey: keyToUse,
model: selectedModel,
contextWindow: getContextWindow(selectedModel),
subagentModels: mappedSubagents,
}),
});
const data = await res.json();
if (res.ok) {
setMessage({ type: "success", text: "Settings applied successfully!" });
setMessage({ type: "success", text: "Main and subagent models applied successfully!" });
checkStatus();
} else {
setMessage({ type: "error", text: data.error || "Failed to apply settings" });
@@ -150,6 +205,7 @@ export default function GrokBuildToolCard({
if (res.ok) {
setMessage({ type: "success", text: "Settings reset successfully!" });
setSelectedModel("");
setSubagentModels({});
checkStatus();
} else {
setMessage({ type: "error", text: data.error || "Failed to reset settings" });
@@ -162,31 +218,33 @@ export default function GrokBuildToolCard({
};
const handleModelSelect = (model) => {
setSelectedModel(model.value);
setModalOpen(false);
if (modelTarget === "main") {
setSelectedModel(model.value);
} else if (modelTarget) {
setSubagentModels((current) => ({ ...current, [modelTarget]: model.value }));
}
setModelTarget(null);
};
const getManualConfigs = () => {
const keyToUse = (selectedApiKey && selectedApiKey.trim())
? selectedApiKey
: (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
const modelId = selectedModel || "provider/model-id";
const tomlContent = `[models]
default = "${MODEL_SLOT}"
[model.${MODEL_SLOT}]
model = "${modelId}"
base_url = "${getEffectiveBaseUrl()}"
name = "9Router"
description = "Routed via 9Router gateway"
api_backend = "chat_completions"
api_key = "${keyToUse}"
`;
return [
{ filename: "~/.grok/config.toml", content: tomlContent },
const keyToUse = selectedApiKey?.trim()
|| (!cloudEnabled ? "sk_9router" : "<API_KEY_FROM_DASHBOARD>");
const baseUrl = getEffectiveBaseUrl();
const mainModel = selectedModel || "provider/model-id";
const blocks = [
`[models]\ndefault = "${MODEL_SLOT}"`,
`[model.${MODEL_SLOT}]\nmodel = "${mainModel}"\nbase_url = "${baseUrl}"\nname = "9Router"\ndescription = "Routed via 9Router gateway"\napi_backend = "chat_completions"\napi_key = "${keyToUse}"\ncontext_window = ${getContextWindow(mainModel) || 200000}`,
];
const mappings = [];
for (const type of SUBAGENT_TYPES) {
const model = subagentModels[type.id]?.trim();
if (!model) continue;
const slot = `${MODEL_SLOT}-${type.id}`;
mappings.push(`${type.id} = "${slot}"`);
blocks.push(`[model.${slot}]\nmodel = "${model}"\nbase_url = "${baseUrl}"\nname = "9Router ${type.id}"\ndescription = "Routed via 9Router gateway"\napi_backend = "chat_completions"\napi_key = "${keyToUse}"\ncontext_window = ${getContextWindow(model) || 200000}`);
}
if (mappings.length) blocks.splice(1, 0, `[subagents.models]\n${mappings.join("\n")}`);
return [{ filename: "~/.grok/config.toml", content: `${blocks.join("\n\n")}\n` }];
};
return (
@@ -202,8 +260,8 @@ api_key = "${keyToUse}"
className="size-8 object-contain rounded-lg"
sizes="32px"
onError={(e) => { e.target.style.display = "none"; }}
loading="lazy"
decoding="async"
loading="lazy"
decoding="async"
/>
</div>
<div className="min-w-0">
@@ -221,170 +279,105 @@ api_key = "${keyToUse}"
{isExpanded && (
<div className="mt-4 pt-4 border-t border-border flex flex-col gap-4">
{checking && (
<div className="flex items-center gap-2 text-text-muted">
<span className="material-symbols-outlined animate-spin">progress_activity</span>
<span>Checking Grok Build...</span>
</div>
)}
{checking && <div className="flex items-center gap-2 text-text-muted"><span className="material-symbols-outlined animate-spin">progress_activity</span><span>Checking Grok Build...</span></div>}
{!checking && grokStatus && !grokStatus.installed && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-3 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<div className="flex items-start gap-3">
<span className="material-symbols-outlined text-yellow-500">warning</span>
<div className="flex-1">
<p className="font-medium text-yellow-600 dark:text-yellow-400">Grok Build not detected locally</p>
<p className="text-sm text-text-muted mt-1">Install:</p>
<code className="block mt-2 p-2 bg-black/20 rounded text-xs font-mono">curl -fsSL https://x.ai/cli/install.sh | bash</code>
<p className="text-sm text-text-muted mt-2">Manual configuration is still available if 9router is deployed on a remote server.</p>
</div>
</div>
<div className="flex flex-col sm:flex-row sm:items-center gap-2 pl-0 sm:pl-9">
<Button
variant="secondary"
size="sm"
onClick={() => setShowManualConfigModal(true)}
className="w-full sm:w-auto !bg-yellow-500/20 !border-yellow-500/40 !text-yellow-700 dark:!text-yellow-300 hover:!bg-yellow-500/30"
>
<span className="material-symbols-outlined text-[18px] mr-1">content_copy</span>
Manual Config
</Button>
<div className="flex flex-col gap-3 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<div className="flex items-start gap-3">
<span className="material-symbols-outlined text-yellow-500">warning</span>
<div className="flex-1">
<p className="font-medium text-yellow-600 dark:text-yellow-400">Grok Build not detected locally</p>
<code className="block mt-2 p-2 bg-black/20 rounded text-xs font-mono">curl -fsSL https://x.ai/cli/install.sh | bash</code>
</div>
</div>
<Button variant="secondary" size="sm" onClick={() => setShowManualConfigModal(true)} className="w-full sm:w-auto"><span className="material-symbols-outlined text-[18px] mr-1">content_copy</span>Manual Config</Button>
</div>
)}
{!checking && grokStatus?.installed && (
<>
<div className="flex flex-col gap-2">
{tool.notes && tool.notes.length > 0 && (
<div className="flex flex-col gap-2 mb-2">
{tool.notes.map((note, idx) => (
<div
key={idx}
className={`flex items-start gap-2 p-2 rounded text-xs ${
note.type === "warning" ? "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400" :
note.type === "error" ? "bg-red-500/10 text-red-600 dark:text-red-400" :
"bg-blue-500/10 text-blue-600 dark:text-blue-400"
}`}
>
<span className="material-symbols-outlined text-[14px] mt-0.5">
{note.type === "warning" ? "warning" : note.type === "error" ? "error" : "info"}
</span>
{tool.notes?.length > 0 && (
<div className="mb-2 flex flex-col gap-2">
{tool.notes.map((note, index) => (
<div key={index} className={`flex items-start gap-2 rounded p-2 text-xs ${note.type === "warning" ? "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400" : "bg-blue-500/10 text-blue-600 dark:text-blue-400"}`}>
<span className="material-symbols-outlined mt-0.5 text-[14px]">{note.type === "warning" ? "warning" : "info"}</span>
<span>{note.text}</span>
</div>
))}
</div>
)}
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Select Endpoint</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<BaseUrlSelect
value={customBaseUrl || getEffectiveBaseUrl()}
onChange={setCustomBaseUrl}
requiresExternalUrl={tool.requiresExternalUrl}
tunnelEnabled={tunnelEnabled}
tunnelPublicUrl={tunnelPublicUrl}
tailscaleEnabled={tailscaleEnabled}
tailscaleUrl={tailscaleUrl}
/>
<BaseUrlSelect value={customBaseUrl || getEffectiveBaseUrl()} onChange={setCustomBaseUrl} requiresExternalUrl={tool.requiresExternalUrl} tunnelEnabled={tunnelEnabled} tunnelPublicUrl={tunnelPublicUrl} tailscaleEnabled={tailscaleEnabled} tailscaleUrl={tailscaleUrl} />
</div>
{grokStatus?.settings?.model?.base_url && (
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
{configuredModel?.base_url && (
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Current</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">
{grokStatus.settings.model.base_url}
{grokStatus.settings.model.model ? ` · ${grokStatus.settings.model.model}` : ""}
</span>
<span className="min-w-0 truncate rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5">{configuredModel.base_url} · {configuredModel.model}{configuredModel.context_window ? ` · ${(configuredModel.context_window / 1000).toLocaleString()}K ctx` : ""}</span>
</div>
)}
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">API Key</span>
<span className="material-symbols-outlined hidden text-text-muted text-[14px] sm:inline">arrow_forward</span>
<ApiKeySelect value={selectedApiKey} onChange={setSelectedApiKey} apiKeys={apiKeys} cloudEnabled={cloudEnabled} />
</div>
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2">
<span className="text-xs font-semibold text-text-main sm:text-right sm:text-sm">Default Model</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={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>
)}
<ModelField label="Main Model" value={selectedModel} onChange={setSelectedModel} placeholder="provider/model-id" onSelect={() => setModelTarget("main")} disabled={!hasActiveProviders} />
<div className="my-1 border-t border-border pt-3">
<div className="mb-2 flex items-start gap-2">
<span className="material-symbols-outlined text-primary text-[16px]">account_tree</span>
<div>
<p className="text-xs font-semibold text-text-main">Subagent model overrides</p>
<p className="text-[10px] text-text-muted">Leave blank to inherit Main Model. Each override keeps its own context window.</p>
</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>
</div>
{SUBAGENT_TYPES.map((type) => (
<ModelField
key={type.id}
label={type.label}
help={type.help}
value={subagentModels[type.id] || ""}
onChange={(value) => setSubagentModels((current) => ({ ...current, [type.id]: value }))}
placeholder={`${selectedModel || "Main Model"} (inherit)`}
onSelect={() => setModelTarget(type.id)}
disabled={!hasActiveProviders}
/>
))}
</div>
{message && (
<div className={`flex items-center gap-2 px-2 py-1.5 rounded text-xs ${message.type === "success" ? "bg-green-500/10 text-green-600" : "bg-red-500/10 text-red-600"}`}>
<span className="material-symbols-outlined text-[14px]">{message.type === "success" ? "check_circle" : "error"}</span>
<span>{message.text}</span>
</div>
)}
{message && <div className={`flex items-center gap-2 px-2 py-1.5 rounded text-xs ${message.type === "success" ? "bg-green-500/10 text-green-600" : "bg-red-500/10 text-red-600"}`}><span className="material-symbols-outlined text-[14px]">{message.type === "success" ? "check_circle" : "error"}</span><span>{message.text}</span></div>}
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
<Button variant="primary" size="sm" onClick={handleApply} disabled={!selectedModel} loading={applying} className="w-full sm:w-auto">
<span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply
</Button>
<Button variant="outline" size="sm" onClick={handleReset} disabled={!grokStatus?.has9Router} loading={restoring} className="w-full sm:w-auto">
<span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset
</Button>
<Button variant="ghost" size="sm" onClick={() => setShowManualConfigModal(true)} className="w-full sm:w-auto">
<span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>Manual Config
</Button>
<Button variant="primary" size="sm" onClick={handleApply} disabled={!selectedModel} loading={applying} className="w-full sm:w-auto"><span className="material-symbols-outlined text-[14px] mr-1">save</span>Apply</Button>
<Button variant="outline" size="sm" onClick={handleReset} disabled={!grokStatus?.has9Router} loading={restoring} className="w-full sm:w-auto"><span className="material-symbols-outlined text-[14px] mr-1">restore</span>Reset</Button>
<Button variant="ghost" size="sm" onClick={() => setShowManualConfigModal(true)} className="w-full sm:w-auto"><span className="material-symbols-outlined text-[14px] mr-1">content_copy</span>Manual Config</Button>
</div>
</>
)}
</div>
)}
{modalOpen && (
{modelTarget && (
<ModelSelectModal
isOpen={modalOpen}
onClose={() => setModalOpen(false)}
isOpen={Boolean(modelTarget)}
onClose={() => setModelTarget(null)}
onSelect={handleModelSelect}
selectedModel={selectedModel}
selectedModel={modelTarget === "main" ? selectedModel : subagentModels[modelTarget] || ""}
activeProviders={activeProviders}
modelAliases={modelAliases}
title="Select Model for Grok Build"
title={modelTarget === "main" ? "Select Main Model for Grok Build" : `Select ${SUBAGENT_TYPES.find((type) => type.id === modelTarget)?.label || "Subagent"} Model`}
/>
)}
<ManualConfigModal
isOpen={showManualConfigModal}
onClose={() => setShowManualConfigModal(false)}
title="Grok Build - Manual Configuration"
configs={getManualConfigs()}
/>
<ManualConfigModal isOpen={showManualConfigModal} onClose={() => setShowManualConfigModal(false)} title="Grok Build - Manual Configuration" configs={getManualConfigs()} />
</Card>
);
}

View File

@@ -6,24 +6,16 @@ import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
import os from "os";
import { getCapabilitiesForModel } from "open-sse/providers/capabilities.js";
import {
applyGrokBuildConfig,
GROK_SUBAGENT_TYPES,
parseGrokBuildConfig,
resetGrokBuildConfig,
} from "@/lib/grokBuildConfig";
const execAsync = promisify(exec);
const PROVIDER_NAME = "9router";
const MODEL_SLOT = "9router";
const BUILTIN_DEFAULT = "grok-build";
// [model.9router] ... until 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 previous [models].default
const PREV_DEFAULT_RE = /^# 9router-prev-default = "([^"]*)"[ \t]*\r?\n?/m;
const getGrokDir = () => path.join(os.homedir(), ".grok");
const getGrokConfigPath = () => path.join(getGrokDir(), "config.toml");
const getGrokBinPath = () => path.join(getGrokDir(), "bin", "grok");
@@ -31,21 +23,16 @@ const getGrokBinPath = () => path.join(getGrokDir(), "bin", "grok");
const checkGrokInstalled = async () => {
try {
const isWindows = os.platform() === "win32";
const command = isWindows ? "where grok" : "which grok";
await execAsync(command, { windowsHide: true });
await execAsync(isWindows ? "where grok" : "which grok", { windowsHide: true });
return true;
} catch {
try {
await fs.access(getGrokBinPath());
return true;
} catch {
for (const candidate of [getGrokBinPath(), getGrokConfigPath()]) {
try {
await fs.access(getGrokConfigPath());
await fs.access(candidate);
return true;
} catch {
return false;
}
} catch { /* try next */ }
}
return false;
}
};
@@ -58,99 +45,32 @@ const readConfigToml = async () => {
}
};
const getTomlField = (body, key) => {
const m = body.match(new RegExp(`^[ \\t]*${key}[ \\t]*=[ \\t]*"([^"]*)"`, "m"));
return m ? m[1] : null;
const normalizeContextWindow = (value, model) => {
const explicit = Number(value);
if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
const slash = model.indexOf("/");
const provider = slash > 0 ? model.slice(0, slash) : null;
const modelId = slash > 0 ? model.slice(slash + 1) : model;
return getCapabilitiesForModel(provider, modelId).contextWindow;
};
const parseModelSection = (toml) => {
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) => {
const match = toml.match(MODELS_SECTION_RE);
if (!match) return null;
return getTomlField(match[1] || "", "default");
};
const buildModelSection = (model, baseUrl, apiKey) => {
const lines = [
`[model.${MODEL_SLOT}]`,
`model = "${model}"`,
`base_url = "${baseUrl}"`,
`name = "9Router"`,
`description = "Routed via 9Router gateway"`,
`api_backend = "chat_completions"`,
];
if (apiKey) lines.push(`api_key = "${apiKey}"`);
return `${lines.join("\n")}\n`;
};
const upsertModelSection = (toml, section) => {
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) =>
toml.replace(MODEL_SECTION_RE, "").replace(/\n{3,}/g, "\n\n");
// Set or insert default = "..." inside existing [models], or create the section
const setModelsDefault = (toml, value) => {
const match = toml.match(MODELS_SECTION_RE);
if (match) {
const body = match[1] || "";
let newBody;
if (/^[ \t]*default[ \t]*=/m.test(body)) {
newBody = body.replace(/^[ \t]*default[ \t]*=[ \t]*"[^"]*"/m, `default = "${value}"`);
} else {
newBody = `default = "${value}"\n${body}`;
}
return toml.replace(match[0], `[models]\n${newBody}`);
const normalizeSubagentModels = (value) => {
if (value === undefined) return undefined; // backwards-compatible callers leave current overrides untouched
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
const result = {};
for (const type of GROK_SUBAGENT_TYPES) {
const entry = value[type];
const model = typeof entry === "string" ? entry.trim() : entry?.model?.trim();
if (!model) continue; // blank means inherit the main model
result[type] = {
model,
contextWindow: normalizeContextWindow(entry?.contextWindow, model),
};
}
const block = `[models]\ndefault = "${value}"\n\n`;
return toml.length > 0 ? block + toml : block;
return result;
};
// Remember the previous default once (so re-Apply does not overwrite it with "9router")
const rememberPrevDefault = (toml) => {
if (PREV_DEFAULT_RE.test(toml)) return toml;
const current = parseModelsDefault(toml);
if (!current || current === MODEL_SLOT) return toml;
const marker = `# 9router-prev-default = "${current}"\n`;
// Prefer placing the marker just above [model.9router] if present, else at EOF
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 default points at our slot, restore previous (or built-in) default and drop marker
const clearModelsDefaultIfOurs = (toml) => {
const prevMatch = toml.match(PREV_DEFAULT_RE);
const restoreTo = prevMatch?.[1] || BUILTIN_DEFAULT;
let next = toml.replace(PREV_DEFAULT_RE, "");
const current = parseModelsDefault(next);
if (current === MODEL_SLOT) {
next = setModelsDefault(next, restoreTo);
}
return next;
};
const has9RouterConfig = (modelCfg) => {
if (!modelCfg?.base_url) return false;
return true;
};
const has9RouterConfig = (settings) => Boolean(settings?.model?.base_url);
export async function GET() {
try {
@@ -163,17 +83,11 @@ export async function GET() {
});
}
const toml = await readConfigToml();
const model = parseModelSection(toml);
const defaultModel = parseModelsDefault(toml);
const settings = parseGrokBuildConfig(await readConfigToml());
return NextResponse.json({
installed: true,
settings: {
model,
default: defaultModel,
},
has9Router: has9RouterConfig(model),
settings,
has9Router: has9RouterConfig(settings),
configPath: getGrokConfigPath(),
});
} catch (error) {
@@ -184,29 +98,28 @@ export async function GET() {
export async function POST(request) {
try {
const { baseUrl, apiKey, model } = await request.json();
if (!baseUrl || !model) {
const { baseUrl, apiKey, model, contextWindow, subagentModels } = await request.json();
const selectedModel = typeof model === "string" ? model.trim() : "";
if (!baseUrl || !selectedModel) {
return NextResponse.json({ error: "baseUrl and model are required" }, { status: 400 });
}
const dir = getGrokDir();
await fs.mkdir(dir, { recursive: true });
await fs.mkdir(getGrokDir(), { recursive: true });
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
const keyToWrite = apiKey || "sk_9router";
let toml = await readConfigToml();
toml = rememberPrevDefault(toml);
toml = upsertModelSection(toml, buildModelSection(model, normalizedBaseUrl, keyToWrite));
toml = setModelsDefault(toml, MODEL_SLOT);
const toml = applyGrokBuildConfig(await readConfigToml(), {
baseUrl: normalizedBaseUrl,
apiKey: apiKey || "sk_9router",
model: selectedModel,
contextWindow: normalizeContextWindow(contextWindow, selectedModel),
subagentModels: normalizeSubagentModels(subagentModels),
});
await fs.writeFile(getGrokConfigPath(), toml);
return NextResponse.json({
success: true,
message: "Grok Build settings applied successfully!",
configPath: getGrokConfigPath(),
modelSlot: MODEL_SLOT,
modelSlot: "9router",
});
} catch (error) {
console.log("Error updating grok-build settings:", error);
@@ -217,7 +130,7 @@ export async function POST(request) {
export async function DELETE() {
try {
const configPath = getGrokConfigPath();
let toml = "";
let toml;
try {
toml = await fs.readFile(configPath, "utf-8");
} catch (error) {
@@ -227,13 +140,10 @@ export async function DELETE() {
throw error;
}
toml = removeModelSection(toml);
toml = clearModelsDefaultIfOurs(toml);
await fs.writeFile(configPath, toml);
await fs.writeFile(configPath, resetGrokBuildConfig(toml));
return NextResponse.json({
success: true,
message: `${PROVIDER_NAME} model slot removed from Grok Build`,
message: "9router model slots removed from Grok Build",
});
} catch (error) {
console.log("Error resetting grok-build settings:", error);

View File

@@ -19,12 +19,21 @@ export async function GET() {
})
.map((m) => {
const fullModel = `${m.provider}/${m.model}`;
const providerAlias = getProviderAlias(m.provider) || m.provider;
const routedModel = `${providerAlias}/${m.model}`;
const c = getCapabilitiesForModel(m.provider, m.model);
return {
...m,
fullModel,
routedModel,
alias: modelAliases[fullModel] || m.model,
caps: { vision: c.vision, search: c.search, reasoning: c.reasoning },
caps: {
vision: c.vision,
search: c.search,
reasoning: c.reasoning,
contextWindow: c.contextWindow,
maxOutput: c.maxOutput,
},
};
});

247
src/lib/grokBuildConfig.js Normal file
View File

@@ -0,0 +1,247 @@
export const GROK_MAIN_MODEL_SLOT = "9router";
export const GROK_BUILTIN_DEFAULT = "grok-build";
export const GROK_SUBAGENT_TYPES = ["general-purpose", "explore", "plan"];
const UNSET_SENTINEL = "__9router_unset__";
const MODELS_SECTION = "models";
const SUBAGENT_MODELS_SECTION = "subagents.models";
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const tomlString = (value) => JSON.stringify(String(value));
const sectionRegExp = (section) =>
new RegExp(
`^\\[${escapeRegExp(section)}\\][ \\t]*\\r?\\n((?:(?!\\[)[^\\r\\n]*\\r?\\n?)*)`,
"m",
);
const modelSlot = (type) => `${GROK_MAIN_MODEL_SLOT}-${type}`;
const previousDefaultRegExp = /^# 9router-prev-default = "([^"]*)"[ \t]*\r?\n?/m;
const previousSubagentRegExp = (type) =>
new RegExp(
`^# 9router-prev-subagent-${escapeRegExp(type)} = "([^"]*)"[ \\t]*\\r?\\n?`,
"m",
);
function getSectionField(toml, section, key) {
const match = toml.match(sectionRegExp(section));
if (!match) return null;
const field = match[1].match(
new RegExp(`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[ \\t]*"([^"]*)"`, "m"),
);
return field ? field[1] : null;
}
function getSectionNumber(toml, section, key) {
const match = toml.match(sectionRegExp(section));
if (!match) return null;
const field = match[1].match(
new RegExp(`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[ \\t]*([0-9]+(?:\\.[0-9]+)?)`, "m"),
);
if (!field) return null;
const value = Number(field[1]);
return Number.isFinite(value) ? value : null;
}
function setSectionField(toml, section, key, value) {
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}\n[${section}]\n${line}\n`;
}
const body = match[1] || "";
const fieldRegExp = new RegExp(
`^[ \\t]*${escapeRegExp(key)}[ \\t]*=[ \\t]*"[^"]*"`,
"m",
);
const nextBody = fieldRegExp.test(body)
? body.replace(fieldRegExp, line)
: `${line}\n${body}`;
return toml.replace(match[0], `[${section}]\n${nextBody}`);
}
function deleteSectionField(toml, section, key) {
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}`);
}
function parseModelSection(toml, slot) {
const match = toml.match(sectionRegExp(`model.${slot}`));
if (!match) return null;
const body = match[1] || "";
const contextWindow = getSectionNumber(toml, `model.${slot}`, "context_window");
return {
model: getSectionField(toml, `model.${slot}`, "model"),
base_url: getSectionField(toml, `model.${slot}`, "base_url"),
name: getSectionField(toml, `model.${slot}`, "name"),
api_key: getSectionField(toml, `model.${slot}`, "api_key"),
api_backend: getSectionField(toml, `model.${slot}`, "api_backend"),
context_window: Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : null,
raw: body,
};
}
function buildModelSection({ slot, model, baseUrl, apiKey, contextWindow, name }) {
const lines = [
`[model.${slot}]`,
`model = ${tomlString(model)}`,
`base_url = ${tomlString(baseUrl)}`,
`name = ${tomlString(name)}`,
`description = ${tomlString("Routed via 9Router gateway")}`,
`api_backend = "chat_completions"`,
];
if (apiKey) lines.push(`api_key = ${tomlString(apiKey)}`);
if (Number.isFinite(contextWindow) && contextWindow > 0) {
lines.push(`context_window = ${Math.floor(contextWindow)}`);
}
return `${lines.join("\n")}\n`;
}
function upsertModelSection(toml, config) {
const regexp = sectionRegExp(`model.${config.slot}`);
const section = buildModelSection(config);
if (regexp.test(toml)) return toml.replace(regexp, section);
const prefix = toml.length > 0 && !toml.endsWith("\n") ? `${toml}\n` : toml;
return `${prefix}\n${section}`;
}
function removeModelSection(toml, slot) {
return toml.replace(sectionRegExp(`model.${slot}`), "").replace(/\n{3,}/g, "\n\n");
}
function insertMarker(toml, marker) {
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}`;
}
function rememberPreviousDefault(toml) {
if (previousDefaultRegExp.test(toml)) return toml;
const current = getSectionField(toml, MODELS_SECTION, "default");
if (!current || current === GROK_MAIN_MODEL_SLOT) return toml;
return insertMarker(toml, `# 9router-prev-default = ${tomlString(current)}\n`);
}
function restorePreviousDefault(toml) {
const previous = toml.match(previousDefaultRegExp)?.[1] || GROK_BUILTIN_DEFAULT;
let next = toml.replace(previousDefaultRegExp, "");
if (getSectionField(next, MODELS_SECTION, "default") === GROK_MAIN_MODEL_SLOT) {
next = setSectionField(next, MODELS_SECTION, "default", previous);
}
return next;
}
function rememberPreviousSubagent(toml, type) {
const regexp = previousSubagentRegExp(type);
if (regexp.test(toml)) return toml;
const current = getSectionField(toml, SUBAGENT_MODELS_SECTION, type);
const previous = current == null ? UNSET_SENTINEL : current;
return insertMarker(
toml,
`# 9router-prev-subagent-${type} = ${tomlString(previous)}\n`,
);
}
function restorePreviousSubagent(toml, type) {
const regexp = previousSubagentRegExp(type);
const previous = toml.match(regexp)?.[1] || UNSET_SENTINEL;
let next = toml.replace(regexp, "");
if (getSectionField(next, SUBAGENT_MODELS_SECTION, type) !== modelSlot(type)) {
return next;
}
if (previous === UNSET_SENTINEL) {
return deleteSectionField(next, SUBAGENT_MODELS_SECTION, type);
}
return setSectionField(next, SUBAGENT_MODELS_SECTION, type, previous);
}
export function parseGrokBuildConfig(toml) {
const subagentModels = {};
const subagentMappings = {};
for (const type of GROK_SUBAGENT_TYPES) {
const mapping = getSectionField(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: getSectionField(toml, MODELS_SECTION, "default"),
subagentModels,
subagentMappings,
};
}
/**
* Apply main model and optional per-type subagent overrides while preserving all unrelated TOML.
* `subagentModels === undefined` leaves existing subagent config untouched for API compatibility.
*/
export function applyGrokBuildConfig(
toml,
{ baseUrl, apiKey, model, contextWindow, subagentModels },
) {
let next = rememberPreviousDefault(toml);
next = upsertModelSection(next, {
slot: GROK_MAIN_MODEL_SLOT,
model,
baseUrl,
apiKey,
contextWindow,
name: "9Router",
});
next = setSectionField(next, MODELS_SECTION, "default", GROK_MAIN_MODEL_SLOT);
if (subagentModels && typeof subagentModels === "object") {
for (const type of GROK_SUBAGENT_TYPES) {
const selected = subagentModels[type];
const slot = modelSlot(type);
if (selected?.model) {
next = rememberPreviousSubagent(next, type);
next = upsertModelSection(next, {
slot,
model: selected.model,
baseUrl,
apiKey,
contextWindow: selected.contextWindow,
name: `9Router ${type}`,
});
next = setSectionField(next, SUBAGENT_MODELS_SECTION, type, slot);
} else {
next = restorePreviousSubagent(next, type);
next = removeModelSection(next, slot);
}
}
}
return next;
}
export function resetGrokBuildConfig(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");
}
export function getGrokSubagentSlot(type) {
return GROK_SUBAGENT_TYPES.includes(type) ? modelSlot(type) : null;
}

View File

@@ -13,6 +13,7 @@ function buildMaps(models) {
for (const m of models || []) {
if (!m.caps) continue;
if (m.fullModel) byFull[m.fullModel] = m.caps;
if (m.routedModel) byFull[m.routedModel] = m.caps;
if (m.model) byId[m.model] = m.caps;
}
return { byFull, byId };
@@ -44,7 +45,13 @@ function resolveCaps(byFull, byId, key) {
if (byId[bare]) return byId[bare];
const provider = key.includes("/") ? key.slice(0, key.indexOf("/")) : null;
const c = getCapabilitiesForModel(provider, bare);
return { vision: c.vision, search: c.search, reasoning: c.reasoning };
return {
vision: c.vision,
search: c.search,
reasoning: c.reasoning,
contextWindow: c.contextWindow,
maxOutput: c.maxOutput,
};
}
export function useModelCaps() {

View File

@@ -0,0 +1,176 @@
import { describe, expect, it } from "vitest";
import {
applyGrokBuildConfig,
getGrokSubagentSlot,
parseGrokBuildConfig,
resetGrokBuildConfig,
} from "../../src/lib/grokBuildConfig.js";
const BASE_CONFIG = `[cli]
installer = "internal"
[ui]
yolo = false
[models]
default = "grok-4.5"
default_reasoning_effort = "high"
[subagents]
enabled = true
[subagents.models]
general-purpose = "grok-4.5"
explore = "grok-build"
plan = "grok-4.5"
[mcp_servers.example]
url = "https://example.com/mcp"
enabled = true
`;
const APPLY_INPUT = {
baseUrl: "http://127.0.0.1:20128/v1",
apiKey: "sk-test",
model: "cx/gpt-5.6-sol",
contextWindow: 400000,
subagentModels: {
"general-purpose": { model: "cc/claude-sonnet-5", contextWindow: 1000000 },
explore: { model: "gemini/gemini-3-flash", contextWindow: 1048576 },
},
};
describe("grokBuildConfig", () => {
it("creates independent main and per-type subagent model slots", () => {
const result = applyGrokBuildConfig(BASE_CONFIG, APPLY_INPUT);
const parsed = parseGrokBuildConfig(result);
expect(parsed.default).toBe("9router");
expect(parsed.model).toMatchObject({
model: "cx/gpt-5.6-sol",
base_url: "http://127.0.0.1:20128/v1",
context_window: 400000,
});
expect(parsed.subagentMappings).toMatchObject({
"general-purpose": "9router-general-purpose",
explore: "9router-explore",
plan: "grok-4.5",
});
expect(parsed.subagentModels["general-purpose"]).toMatchObject({
model: "cc/claude-sonnet-5",
context_window: 1000000,
});
expect(parsed.subagentModels.explore).toMatchObject({
model: "gemini/gemini-3-flash",
context_window: 1048576,
});
expect(parsed.subagentModels.plan).toBeNull();
});
it("preserves unrelated config sections", () => {
const result = applyGrokBuildConfig(BASE_CONFIG, APPLY_INPUT);
expect(result).toContain("[cli]\ninstaller = \"internal\"");
expect(result).toContain("[ui]\nyolo = false");
expect(result).toContain("default_reasoning_effort = \"high\"");
expect(result).toContain("[mcp_servers.example]");
expect(result).toContain("url = \"https://example.com/mcp\"");
});
it("is idempotent and updates owned slots without duplicate sections", () => {
let result = applyGrokBuildConfig(BASE_CONFIG, APPLY_INPUT);
result = applyGrokBuildConfig(result, {
...APPLY_INPUT,
model: "cc/claude-opus-4.8",
contextWindow: 1000000,
subagentModels: {
...APPLY_INPUT.subagentModels,
explore: { model: "mimo/mimo", contextWindow: 262144 },
},
});
expect(result.match(/^\[model\.9router\]$/gm)).toHaveLength(1);
expect(result.match(/^\[model\.9router-general-purpose\]$/gm)).toHaveLength(1);
expect(result.match(/^\[model\.9router-explore\]$/gm)).toHaveLength(1);
expect(result.match(/^# 9router-prev-subagent-explore/gm)).toHaveLength(1);
expect(parseGrokBuildConfig(result).model).toMatchObject({
model: "cc/claude-opus-4.8",
context_window: 1000000,
});
expect(parseGrokBuildConfig(result).subagentModels.explore).toMatchObject({
model: "mimo/mimo",
context_window: 262144,
});
});
it("blank override restores previous subagent mapping and removes owned slot", () => {
let result = applyGrokBuildConfig(BASE_CONFIG, APPLY_INPUT);
result = applyGrokBuildConfig(result, {
...APPLY_INPUT,
subagentModels: {
"general-purpose": APPLY_INPUT.subagentModels["general-purpose"],
// explore omitted => inherit / restore previous
},
});
const parsed = parseGrokBuildConfig(result);
expect(parsed.subagentMappings.explore).toBe("grok-build");
expect(parsed.subagentModels.explore).toBeNull();
expect(result).not.toContain("[model.9router-explore]");
expect(parsed.subagentMappings["general-purpose"]).toBe("9router-general-purpose");
});
it("reset restores previous default and all previous subagent mappings", () => {
const applied = applyGrokBuildConfig(BASE_CONFIG, APPLY_INPUT);
const reset = resetGrokBuildConfig(applied);
const parsed = parseGrokBuildConfig(reset);
expect(parsed.default).toBe("grok-4.5");
expect(parsed.model).toBeNull();
expect(parsed.subagentMappings).toEqual({
"general-purpose": "grok-4.5",
explore: "grok-build",
plan: "grok-4.5",
});
expect(reset).not.toContain("[model.9router-");
expect(reset).not.toContain("9router-prev-");
expect(reset).toContain("[mcp_servers.example]");
});
it("removes mappings that were originally unset", () => {
const config = `[models]\ndefault = "grok-build"\n\n[mcp_servers.x]\nenabled = true\n`;
const applied = applyGrokBuildConfig(config, {
...APPLY_INPUT,
subagentModels: {
plan: { model: "cc/claude-sonnet-5", contextWindow: 1000000 },
},
});
const reset = resetGrokBuildConfig(applied);
expect(parseGrokBuildConfig(applied).subagentMappings.plan).toBe("9router-plan");
expect(parseGrokBuildConfig(reset).subagentMappings.plan).toBeNull();
expect(reset).not.toContain("[subagents.models]");
expect(reset).toContain("[mcp_servers.x]");
});
it("legacy callers without subagentModels leave existing overrides untouched", () => {
const applied = applyGrokBuildConfig(BASE_CONFIG, APPLY_INPUT);
const updatedMainOnly = applyGrokBuildConfig(applied, {
baseUrl: APPLY_INPUT.baseUrl,
apiKey: APPLY_INPUT.apiKey,
model: "gemini/gemini-3.1-pro",
contextWindow: 1048576,
});
const parsed = parseGrokBuildConfig(updatedMainOnly);
expect(parsed.model.model).toBe("gemini/gemini-3.1-pro");
expect(parsed.subagentMappings.explore).toBe("9router-explore");
expect(parsed.subagentModels.explore.model).toBe("gemini/gemini-3-flash");
});
it("returns stable slot names only for supported subagent types", () => {
expect(getGrokSubagentSlot("general-purpose")).toBe("9router-general-purpose");
expect(getGrokSubagentSlot("explore")).toBe("9router-explore");
expect(getGrokSubagentSlot("plan")).toBe("9router-plan");
expect(getGrokSubagentSlot("unknown")).toBeNull();
});
});