}
label="Anthropic"
@@ -400,6 +430,18 @@ export function AIRoutingCard() {
onClick={flipToAnthropic}
disabled={applyMode.isPending}
/>
+
}
+ label="Grok"
+ description={
+ hasGrokKey
+ ? "Every agent uses Grok (grok-build-0.1)."
+ : "Save the Grok (xAI) key first."
+ }
+ active={currentMode === "grok"}
+ onClick={flipToGrok}
+ disabled={applyMode.isPending || !hasGrokKey}
+ />
}
label="Ollama"
@@ -545,6 +587,23 @@ export function AIRoutingCard() {
)}
+ {/* Grok (xAI) models */}
+ {catalogGrokOnly.length > 0 && (
+
+
+
+ Grok (xAI)
+
+ {catalogGrokOnly.map(
+ (c: { model_name: string; display_name: string }) => (
+
+ {c.display_name}
+
+ ),
+ )}
+
+ )}
+
{/* Ollama Cloud models */}
{catalogOllamaOnly.length > 0 && (
@@ -659,17 +718,19 @@ function errMsg(e: unknown): string {
function ProviderBadge({
variant,
}: {
- variant: "anthropic" | "ollama" | "self-hosted";
+ variant: "anthropic" | "grok" | "ollama" | "self-hosted";
}) {
const styles: Record = {
anthropic: "bg-blue-500/20 text-blue-700 dark:text-blue-400",
ollama: "bg-violet-500/20 text-violet-700 dark:text-violet-400",
"self-hosted": "bg-purple-500/20 text-purple-700 dark:text-purple-400",
+ grok: "bg-teal-500/20 text-teal-700 dark:text-teal-400",
};
const labels: Record = {
anthropic: "A",
ollama: "O",
"self-hosted": "S",
+ grok: "G",
};
return (
Literal["anthropic", "ollama", "mix", "self_hosted"]:
+ async def derive_mode(
+ self,
+ ) -> Literal["anthropic", "grok", "ollama", "mix", "self_hosted"]:
"""Return the current "mode" label for the Settings UI.
Decision tree matches what `apply_mode` writes:
@@ -327,6 +329,8 @@ class ModelRoutingService(BaseService):
len(assignments) == 1 and assignments[0].scope == AssignmentScope.GLOBAL
)
if only_global:
+ if assignments[0].provider.type == ModelProvider.GROK:
+ return "grok"
if assignments[0].provider.type == ModelProvider.OLLAMA_CLOUD:
return "ollama"
if assignments[0].provider.type == ModelProvider.LOCAL:
@@ -429,6 +433,8 @@ class ModelRoutingService(BaseService):
- "self_hosted": wipe all assignments, enable the LOCAL provider,
and set the GLOBAL default to `default_model` (a self-hosted
model name — not validated against the static catalog).
+ - "grok": wipe all assignments, set the GLOBAL default to a
+ Grok (xAI) model (default grok-build-0.1). Requires the xAI key.
- "mix": apply per-agent map verbatim. Any agent not in the
map falls through to the GLOBAL default — which is whatever it
was (preserves prior state). Self-hosted model names (not in the
@@ -436,6 +442,8 @@ class ModelRoutingService(BaseService):
"""
if mode == "anthropic":
await self._apply_anthropic()
+ elif mode == "grok":
+ await self._apply_grok(default_model)
elif mode == "ollama":
await self._apply_ollama(default_model)
elif mode == "self_hosted":
@@ -445,7 +453,7 @@ class ModelRoutingService(BaseService):
else:
raise ValueError(
f"Unknown mode '{mode}'."
- " Use 'anthropic', 'ollama', 'self_hosted', or 'mix'."
+ " Use 'anthropic', 'grok', 'ollama', 'self_hosted', or 'mix'."
)
async def _apply_anthropic(self) -> None:
@@ -454,6 +462,24 @@ class ModelRoutingService(BaseService):
await self.session.flush()
self.log.info("Mode applied: anthropic (all assignments cleared)")
+ async def _apply_grok(self, default_model: str | None) -> None:
+ """Wipe assignments, set the GLOBAL default to a Grok (xAI) model.
+
+ ``grok-build-0.1`` is in the catalog under the GROK provider, so the
+ upsert resolves to the seeded Grok provider row. Routing to Grok needs
+ the xAI key set (which enables the provider); without it, agents fall
+ back to the Anthropic path at spawn — same contract as Ollama.
+ """
+ await self.session.execute(sa_delete(ModelAssignmentTable))
+ await self.session.flush()
+ model_name = default_model or "grok-build-0.1"
+ await self.upsert_assignment(
+ scope=AssignmentScope.GLOBAL,
+ scope_value=None,
+ model_name=model_name,
+ )
+ self.log.info("Mode applied: grok", default_model=model_name)
+
async def _apply_ollama(self, default_model: str | None) -> None:
"""Wipe assignments, set the GLOBAL default to an Ollama Cloud model."""
await self.session.execute(sa_delete(ModelAssignmentTable))
diff --git a/tests/integration/test_llm_routing.py b/tests/integration/test_llm_routing.py
index 316b51f0..392aca07 100644
--- a/tests/integration/test_llm_routing.py
+++ b/tests/integration/test_llm_routing.py
@@ -37,13 +37,19 @@ async def llm_setup(
type=ModelProvider.ANTHROPIC,
enabled=True,
)
+ grok = ProviderConfigTable(
+ name="grok-test",
+ type=ModelProvider.GROK,
+ enabled=True,
+ base_url="https://api.x.ai/v1",
+ )
ollama = ProviderConfigTable(
name="ollama-test",
type=ModelProvider.OLLAMA_CLOUD,
enabled=True,
base_url="https://ollama.example.com",
)
- db_session.add_all([anthropic, ollama])
+ db_session.add_all([anthropic, grok, ollama])
await db_session.flush()
yield {"svc": ModelRoutingService(db_session)}
@@ -177,6 +183,16 @@ async def test_derive_mode_ollama_when_only_ollama_global(llm_setup: dict) -> No
assert await svc.derive_mode() == "ollama"
+@pytest.mark.asyncio
+async def test_derive_mode_grok_when_only_grok_global(llm_setup: dict) -> None:
+ svc = llm_setup["svc"]
+ grok_model = _first_model_for_type(ModelProvider.GROK)
+ await svc.upsert_assignment(
+ scope=AssignmentScope.GLOBAL, scope_value=None, model_name=grok_model
+ )
+ assert await svc.derive_mode() == "grok"
+
+
@pytest.mark.asyncio
async def test_derive_mode_mix_with_per_agent(llm_setup: dict) -> None:
svc = llm_setup["svc"]
@@ -215,6 +231,16 @@ async def test_apply_mode_ollama_sets_global(llm_setup: dict) -> None:
assert assignments[0].scope == AssignmentScope.GLOBAL
+@pytest.mark.asyncio
+async def test_apply_mode_grok_sets_global(llm_setup: dict) -> None:
+ svc = llm_setup["svc"]
+ await svc.apply_mode(mode="grok")
+ assignments = await svc.list_assignments()
+ assert len(assignments) == 1
+ assert assignments[0].scope == AssignmentScope.GLOBAL
+ assert assignments[0].provider.type == ModelProvider.GROK
+
+
@pytest.mark.asyncio
async def test_apply_mode_mix_requires_per_agent(llm_setup: dict) -> None:
svc = llm_setup["svc"]