diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 2b93a3f918..2854873b81 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -2021,24 +2021,35 @@ def resolve_vision_provider_client( # _PROVIDER_VISION_MODELS provides per-provider vision model # overrides when the provider has a dedicated multimodal model # that differs from the chat model (e.g. xiaomi → mimo-v2-omni, - # zai → glm-5v-turbo). + # zai → glm-5v-turbo). Nous is the exception: it has a dedicated + # strict vision backend with tier-aware defaults, so it must not + # fall through to the user's text chat model here. # 2. OpenRouter (vision-capable aggregator fallback) # 3. Nous Portal (vision-capable aggregator fallback) # 4. Stop main_provider = _read_main_provider() main_model = _read_main_model() if main_provider and main_provider not in ("auto", ""): - vision_model = _PROVIDER_VISION_MODELS.get(main_provider, main_model) - rpc_client, rpc_model = resolve_provider_client( - main_provider, vision_model, - api_mode=resolved_api_mode) - if rpc_client is not None: - logger.info( - "Vision auto-detect: using main provider %s (%s)", - main_provider, rpc_model or vision_model, - ) - return _finalize( - main_provider, rpc_client, rpc_model or vision_model) + if main_provider == "nous": + sync_client, default_model = _resolve_strict_vision_backend(main_provider) + if sync_client is not None: + logger.info( + "Vision auto-detect: using main provider %s (%s)", + main_provider, default_model or resolved_model or main_model, + ) + return _finalize(main_provider, sync_client, default_model) + else: + vision_model = _PROVIDER_VISION_MODELS.get(main_provider, main_model) + rpc_client, rpc_model = resolve_provider_client( + main_provider, vision_model, + api_mode=resolved_api_mode) + if rpc_client is not None: + logger.info( + "Vision auto-detect: using main provider %s (%s)", + main_provider, rpc_model or vision_model, + ) + return _finalize( + main_provider, rpc_client, rpc_model or vision_model) # Fall back through aggregators (uses their dedicated vision model, # not the user's main model) when main provider has no client. diff --git a/tests/agent/test_auxiliary_main_first.py b/tests/agent/test_auxiliary_main_first.py index 353c6c2ddc..d756d6ffb1 100644 --- a/tests/agent/test_auxiliary_main_first.py +++ b/tests/agent/test_auxiliary_main_first.py @@ -167,7 +167,7 @@ class TestResolveAutoMainFirst: class TestResolveVisionMainFirst: - """Vision auto-detection prefers main provider + main model first.""" + """Vision auto-detection prefers the main provider first.""" def test_openrouter_main_vision_uses_main_model(self, monkeypatch): """OpenRouter main with vision-capable model → aux vision uses main model.""" @@ -200,28 +200,49 @@ class TestResolveVisionMainFirst: assert mock_resolve.call_args.args[0] == "openrouter" assert mock_resolve.call_args.args[1] == "anthropic/claude-sonnet-4.6" - def test_nous_main_vision_uses_main_model(self): - """Nous Portal main → aux vision uses main model, not free-tier MiMo-V2-Omni.""" + def test_nous_main_vision_uses_paid_nous_vision_backend(self): + """Paid Nous main → aux vision uses the dedicated Nous vision backend.""" with patch( "agent.auxiliary_client._read_main_provider", return_value="nous", ), patch( "agent.auxiliary_client._read_main_model", return_value="openai/gpt-5", ), patch( - "agent.auxiliary_client.resolve_provider_client" - ) as mock_resolve, patch( "agent.auxiliary_client._resolve_task_provider_model", return_value=("auto", None, None, None, None), + ), patch( + "agent.auxiliary_client._resolve_strict_vision_backend", + return_value=(MagicMock(), "google/gemini-3-flash-preview"), ): - mock_client = MagicMock() - mock_resolve.return_value = (mock_client, "openai/gpt-5") - from agent.auxiliary_client import resolve_vision_provider_client provider, client, model = resolve_vision_provider_client() assert provider == "nous" - assert model == "openai/gpt-5" + assert client is not None + assert model == "google/gemini-3-flash-preview" + + def test_nous_main_vision_uses_free_tier_nous_vision_backend(self): + """Free-tier Nous main → aux vision uses MiMo omni, not the text main model.""" + with patch( + "agent.auxiliary_client._read_main_provider", return_value="nous", + ), patch( + "agent.auxiliary_client._read_main_model", + return_value="xiaomi/mimo-v2-pro", + ), patch( + "agent.auxiliary_client._resolve_task_provider_model", + return_value=("auto", None, None, None, None), + ), patch( + "agent.auxiliary_client._resolve_strict_vision_backend", + return_value=(MagicMock(), "xiaomi/mimo-v2-omni"), + ): + from agent.auxiliary_client import resolve_vision_provider_client + + provider, client, model = resolve_vision_provider_client() + + assert provider == "nous" + assert client is not None + assert model == "xiaomi/mimo-v2-omni" def test_exotic_provider_with_vision_override_preserved(self): """xiaomi → mimo-v2-omni override still wins over main_model."""