diff --git a/backend/apps/agents/manager/configure_provider_env.py b/backend/apps/agents/manager/configure_provider_env.py index 327a2033..a7fa44f5 100644 --- a/backend/apps/agents/manager/configure_provider_env.py +++ b/backend/apps/agents/manager/configure_provider_env.py @@ -1,10 +1,9 @@ """Configure the SDK environment for the run's provider route: set ANTHROPIC/OPENAI/GOOGLE auth env vars (direct key, OpenSwarm Pro proxy, OpenRouter, or 9Router) and pin subagent models, -ensuring 9Router is up where the route needs it. sub_conns is the active-connection list for -subagent-model fallback (empty today).""" +ensuring 9Router is up where the route needs it.""" import os -from typing import Dict, List, Optional +from typing import Dict, Optional from typeguard import typechecked @@ -50,7 +49,6 @@ async def configure_provider_env( resolved_model: object, api_type: Optional[str], global_settings: AppSettings, - sub_conns: List, ) -> None: from backend.apps.nine_router import is_running as nine_router_running from backend.apps.agents.providers.registry import NINEROUTER_MODEL_PREFIXES as NINEROUTER_MODEL_PREFIXES @@ -198,7 +196,9 @@ async def configure_provider_env( "ANTHROPIC_API_KEY": "9router", "ANTHROPIC_BASE_URL": "http://localhost:20128", } - # Pin subagents to whichever lane the user has, else CLI's default Haiku 4.5 hits 9Router with no Claude route and 401s. NOTE: callers pass sub_conns=[] today so this is inert (latent regression from the run/ split; pyright caught the dangling _conns ref). + # Pin subagents to whichever lane the user has, else the CLI's default Haiku 4.5 hits 9Router with no Claude route and every sub-agent 401s while the parent turn works. Fetched live here (fail-open []) so no caller can starve the pin with a stale list again, the run/ split did exactly that and silently killed sub-agents on router routes. + from backend.apps.nine_router import get_providers as p_get_providers + sub_conns = await p_get_providers() active = {c.get("provider") for c in sub_conns if isinstance(c, dict) and c.get("isActive")} sub_model = None diff --git a/backend/apps/agents/manager/run/RunOptions.py b/backend/apps/agents/manager/run/RunOptions.py index 37b7f3da..e0ab25ec 100644 --- a/backend/apps/agents/manager/run/RunOptions.py +++ b/backend/apps/agents/manager/run/RunOptions.py @@ -188,7 +188,7 @@ class RunOptions(AgentManagerProtocol): } # cc/cx/gc/ag/gemini/openrouter prefixes force 9Router; route="api" bypasses to the provider's host directly; otherwise Pro proxy or key. await configure_provider_env( - options_kwargs, session, resolved_model, api_type, global_settings, [] + options_kwargs, session, resolved_model, api_type, global_settings ) if mcp_servers: options_kwargs["mcp_servers"] = mcp_servers diff --git a/backend/tests/test_subagent_model_pin.py b/backend/tests/test_subagent_model_pin.py new file mode 100644 index 00000000..d4a7318e --- /dev/null +++ b/backend/tests/test_subagent_model_pin.py @@ -0,0 +1,45 @@ +"""The sub-agent model pin on the 9Router-direct route must come from LIVE router +connections. The run/ split passed a hardcoded empty list, the pin never fired, and every +sub-agent 401'd ("No credentials for provider: anthropic") while the parent turn worked.""" +import asyncio +from typing import Dict + +from pytest import MonkeyPatch + +import backend.apps.agents.manager.configure_provider_env as cpe +from backend.apps.agents.core.models import AgentSession +from backend.apps.settings.models import AppSettings + + +def run_env_for(connections: list, monkeypatch: MonkeyPatch) -> Dict: + import backend.apps.nine_router as nr_pkg + + async def fake_get_providers() -> list: + return connections + + monkeypatch.setattr(nr_pkg, "is_running", lambda: True) + monkeypatch.setattr(nr_pkg, "get_providers", fake_get_providers) + session = AgentSession(name="t", model="opus-4-8-cc") + options_kwargs: Dict = {} + asyncio.run( + cpe.configure_provider_env( + options_kwargs, session, "cc/claude-opus-4-8", "anthropic", AppSettings() + ) + ) + return options_kwargs.get("env", {}) + + +def test_subagent_pin_set_from_live_claude_connection(monkeypatch: MonkeyPatch) -> None: + env = run_env_for([{"provider": "claude", "isActive": True}], monkeypatch) + assert env.get("CLAUDE_CODE_SUBAGENT_MODEL") == "cc/claude-sonnet-4-6" + assert env.get("ANTHROPIC_SMALL_FAST_MODEL") == "cc/claude-haiku-4-5-20251001" + + +def test_subagent_pin_absent_only_when_no_active_lane(monkeypatch: MonkeyPatch) -> None: + env = run_env_for([], monkeypatch) + assert "CLAUDE_CODE_SUBAGENT_MODEL" not in env + + +def test_subagent_pin_codex_lane(monkeypatch: MonkeyPatch) -> None: + env = run_env_for([{"provider": "codex", "isActive": True}], monkeypatch) + assert env.get("CLAUDE_CODE_SUBAGENT_MODEL") == "cx/gpt-5.4-mini"