mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(grok): guard interactive roles from GROK routes (interim)
intake (prompter) and secretary run a held-open chat session driven by the Claude Agent SDK. GROK has no interactive runtime yet, and a GROK route for those slugs would be spawned with the route creds injected as ANTHROPIC_* against api.x.ai/v1 — the wrong protocol — producing a silent, empty reply (the blank intake we observed). Downgrade a GROK route for intake-1/secretary-1 to the Anthropic default with a logged warning. The one-shot delivery roles route to GROK unchanged. This guard is replaced by the real interactive fork once the opencode interactive driver lands.
This commit is contained in:
+32
-1
@@ -116,6 +116,12 @@ class _ResolvedAssignment:
|
|||||||
model_name: str
|
model_name: str
|
||||||
|
|
||||||
|
|
||||||
|
# The two human-facing interactive roles (held-open chat sessions). Kept as a
|
||||||
|
# literal here — not imported from runtime.orchestrator — to avoid a service →
|
||||||
|
# orchestrator import cycle; the slugs are stable seed identities.
|
||||||
|
_INTERACTIVE_AGENT_SLUGS = frozenset({"intake-1", "secretary-1"})
|
||||||
|
|
||||||
|
|
||||||
class ModelRoutingService(BaseService):
|
class ModelRoutingService(BaseService):
|
||||||
"""Resolves per-agent routes from `model_assignments` + legacy fallback."""
|
"""Resolves per-agent routes from `model_assignments` + legacy fallback."""
|
||||||
|
|
||||||
@@ -134,9 +140,34 @@ class ModelRoutingService(BaseService):
|
|||||||
if resolved is not None and resolved.provider.enabled:
|
if resolved is not None and resolved.provider.enabled:
|
||||||
route = await self._route_from_resolved(resolved, agent_slug)
|
route = await self._route_from_resolved(resolved, agent_slug)
|
||||||
if route is not None:
|
if route is not None:
|
||||||
return route
|
return self._guard_interactive(route, agent_slug, role)
|
||||||
return self._legacy_route(role)
|
return self._legacy_route(role)
|
||||||
|
|
||||||
|
def _guard_interactive(
|
||||||
|
self, route: AgentRoute, agent_slug: str, role: str
|
||||||
|
) -> AgentRoute:
|
||||||
|
"""Keep the human-facing interactive roles off GROK for now.
|
||||||
|
|
||||||
|
intake (prompter) and secretary run a held-open chat the human types
|
||||||
|
into, driven by the Claude Agent SDK. GROK has no interactive runtime
|
||||||
|
yet, and the interactive spawn injects the route's creds as
|
||||||
|
``ANTHROPIC_*`` against ``api.x.ai/v1`` — the wrong protocol — yielding a
|
||||||
|
silent, empty reply. Until the opencode interactive driver lands,
|
||||||
|
downgrade a GROK route for these slugs to the Anthropic default. (The
|
||||||
|
one-shot delivery roles route to GROK unchanged.)
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
route.provider_type == ModelProvider.GROK
|
||||||
|
and agent_slug in _INTERACTIVE_AGENT_SLUGS
|
||||||
|
):
|
||||||
|
self.log.warning(
|
||||||
|
"GROK route for an interactive role downgraded to Anthropic "
|
||||||
|
"(no Grok interactive runtime yet)",
|
||||||
|
agent_slug=agent_slug,
|
||||||
|
)
|
||||||
|
return self._legacy_route(role)
|
||||||
|
return route
|
||||||
|
|
||||||
async def _resolve_assignment(
|
async def _resolve_assignment(
|
||||||
self, agent_slug: str, role: str
|
self, agent_slug: str, role: str
|
||||||
) -> _ResolvedAssignment | None:
|
) -> _ResolvedAssignment | None:
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""The interactive-role routing guard keeps intake/secretary off GROK.
|
||||||
|
|
||||||
|
GROK has no interactive runtime yet, and the interactive spawn would inject the
|
||||||
|
route's creds as ANTHROPIC_* against api.x.ai/v1 (wrong protocol → silent empty
|
||||||
|
reply). Until the opencode interactive driver lands, a GROK route for those
|
||||||
|
slugs is downgraded to the Anthropic default. The one-shot delivery roles route
|
||||||
|
to GROK unchanged. DB-free: exercises `_guard_interactive` directly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
from roboco.models.base import ModelProvider
|
||||||
|
from roboco.services.llm import AgentRoute, ModelRoutingService
|
||||||
|
|
||||||
|
|
||||||
|
def _svc() -> ModelRoutingService:
|
||||||
|
svc = ModelRoutingService.__new__(ModelRoutingService)
|
||||||
|
svc.log = structlog.get_logger()
|
||||||
|
return svc
|
||||||
|
|
||||||
|
|
||||||
|
def _grok_route() -> AgentRoute:
|
||||||
|
return AgentRoute(
|
||||||
|
provider_id=None,
|
||||||
|
provider_type=ModelProvider.GROK,
|
||||||
|
base_url="https://api.x.ai/v1",
|
||||||
|
auth_token="xai-key",
|
||||||
|
model_name="grok-build-0.1",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_grok_interactive_role_downgrades_to_anthropic() -> None:
|
||||||
|
svc = _svc()
|
||||||
|
for slug in ("intake-1", "secretary-1"):
|
||||||
|
route = svc._guard_interactive(_grok_route(), slug, "prompter")
|
||||||
|
assert route.provider_type == ModelProvider.ANTHROPIC
|
||||||
|
# No xAI creds leak onto the Claude SDK path.
|
||||||
|
assert route.base_url is None
|
||||||
|
assert route.auth_token is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_grok_delivery_role_is_left_on_grok() -> None:
|
||||||
|
svc = _svc()
|
||||||
|
route = svc._guard_interactive(_grok_route(), "be-dev-1", "developer")
|
||||||
|
assert route.provider_type == ModelProvider.GROK
|
||||||
|
assert route.base_url == "https://api.x.ai/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_grok_interactive_route_is_unchanged() -> None:
|
||||||
|
svc = _svc()
|
||||||
|
anthropic = AgentRoute(
|
||||||
|
provider_id=None,
|
||||||
|
provider_type=ModelProvider.ANTHROPIC,
|
||||||
|
base_url=None,
|
||||||
|
auth_token=None,
|
||||||
|
model_name="claude-opus-4-6",
|
||||||
|
)
|
||||||
|
route = svc._guard_interactive(anthropic, "intake-1", "prompter")
|
||||||
|
assert route is anthropic
|
||||||
Reference in New Issue
Block a user