diff --git a/hermes_cli/nous_subscription.py b/hermes_cli/nous_subscription.py index 78181aab2b..c83844901f 100644 --- a/hermes_cli/nous_subscription.py +++ b/hermes_cli/nous_subscription.py @@ -9,6 +9,7 @@ from typing import Dict, Iterable, Optional, Set from hermes_cli.auth import get_nous_auth_status from hermes_cli.config import get_env_value, load_config from tools.managed_tool_gateway import is_managed_tool_gateway_ready +from utils import is_truthy_value from tools.tool_backend_helpers import ( fal_key_is_configured, has_direct_modal_credentials, @@ -25,6 +26,13 @@ _DEFAULT_PLATFORM_TOOLSETS = { } +def _uses_gateway(section: object) -> bool: + """Return True when a config section explicitly opts into the gateway.""" + if not isinstance(section, dict): + return False + return is_truthy_value(section.get("use_gateway"), default=False) + + @dataclass(frozen=True) class NousFeatureState: key: str @@ -262,11 +270,11 @@ def get_nous_subscription_features( # use_gateway flags — when True, the user explicitly opted into the # Tool Gateway via `hermes model`, so direct credentials should NOT # prevent gateway routing. - web_use_gateway = bool(web_cfg.get("use_gateway")) - tts_use_gateway = bool(tts_cfg.get("use_gateway")) - browser_use_gateway = bool(browser_cfg.get("use_gateway")) + web_use_gateway = _uses_gateway(web_cfg) + tts_use_gateway = _uses_gateway(tts_cfg) + browser_use_gateway = _uses_gateway(browser_cfg) image_gen_cfg = config.get("image_gen") if isinstance(config.get("image_gen"), dict) else {} - image_use_gateway = bool(image_gen_cfg.get("use_gateway")) + image_use_gateway = _uses_gateway(image_gen_cfg) direct_exa = bool(get_env_value("EXA_API_KEY")) direct_firecrawl = bool(get_env_value("FIRECRAWL_API_KEY") or get_env_value("FIRECRAWL_API_URL")) @@ -601,10 +609,10 @@ def get_gateway_eligible_tools( # no direct keys exist — we only skip the prompt for tools where # use_gateway was explicitly set. opted_in = { - "web": bool((config.get("web") if isinstance(config.get("web"), dict) else {}).get("use_gateway")), - "image_gen": bool((config.get("image_gen") if isinstance(config.get("image_gen"), dict) else {}).get("use_gateway")), - "tts": bool((config.get("tts") if isinstance(config.get("tts"), dict) else {}).get("use_gateway")), - "browser": bool((config.get("browser") if isinstance(config.get("browser"), dict) else {}).get("use_gateway")), + "web": _uses_gateway(config.get("web")), + "image_gen": _uses_gateway(config.get("image_gen")), + "tts": _uses_gateway(config.get("tts")), + "browser": _uses_gateway(config.get("browser")), } unconfigured: list[str] = [] diff --git a/tests/hermes_cli/test_nous_subscription.py b/tests/hermes_cli/test_nous_subscription.py index b7819cfa88..c1deaf7707 100644 --- a/tests/hermes_cli/test_nous_subscription.py +++ b/tests/hermes_cli/test_nous_subscription.py @@ -149,3 +149,46 @@ def test_get_nous_subscription_features_requires_agent_browser_for_browserbase(m assert features.browser.active is False assert features.browser.managed_by_nous is False assert features.browser.current_provider == "Browserbase" + + +def test_get_nous_subscription_features_does_not_treat_quoted_false_as_gateway_opt_in(monkeypatch): + env = {"EXA_API_KEY": "exa-test"} + + monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, "")) + monkeypatch.setattr(ns, "get_nous_auth_status", lambda: {"logged_in": True}) + monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: True) + monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "web") + monkeypatch.setattr(ns, "_has_agent_browser", lambda: False) + monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "") + monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: False) + monkeypatch.setattr(ns, "is_managed_tool_gateway_ready", lambda vendor: vendor == "firecrawl") + + features = ns.get_nous_subscription_features( + {"web": {"backend": "exa", "use_gateway": "false"}} + ) + + assert features.web.available is True + assert features.web.active is True + assert features.web.managed_by_nous is False + assert features.web.direct_override is True + assert features.web.current_provider == "exa" + + +def test_get_gateway_eligible_tools_ignores_quoted_false_opt_in(monkeypatch): + monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda: True) + monkeypatch.setattr( + ns, + "_get_gateway_direct_credentials", + lambda: {"web": True, "image_gen": False, "tts": False, "browser": False}, + ) + + unconfigured, has_direct, already_managed = ns.get_gateway_eligible_tools( + { + "model": {"provider": "nous"}, + "web": {"use_gateway": "false"}, + } + ) + + assert "web" in has_direct + assert "web" not in already_managed + assert set(unconfigured) == {"image_gen", "tts", "browser"} diff --git a/tests/tools/test_tool_backend_helpers.py b/tests/tools/test_tool_backend_helpers.py index abe6d7bd19..014b25c827 100644 --- a/tests/tools/test_tool_backend_helpers.py +++ b/tests/tools/test_tool_backend_helpers.py @@ -22,6 +22,7 @@ from tools.tool_backend_helpers import ( managed_nous_tools_enabled, normalize_browser_cloud_provider, normalize_modal_mode, + prefers_gateway, resolve_modal_backend_state, resolve_openai_audio_api_key, ) @@ -189,6 +190,27 @@ class TestHasDirectModalCredentials: assert has_direct_modal_credentials() is True +# --------------------------------------------------------------------------- +# prefers_gateway +# --------------------------------------------------------------------------- +class TestPrefersGateway: + """Honor bool-ish config values for tool gateway routing.""" + + def test_returns_false_for_quoted_false(self, monkeypatch): + monkeypatch.setattr( + "hermes_cli.config.load_config", + lambda: {"web": {"use_gateway": "false"}}, + ) + assert prefers_gateway("web") is False + + def test_returns_true_for_quoted_true(self, monkeypatch): + monkeypatch.setattr( + "hermes_cli.config.load_config", + lambda: {"web": {"use_gateway": "true"}}, + ) + assert prefers_gateway("web") is True + + # --------------------------------------------------------------------------- # resolve_modal_backend_state # --------------------------------------------------------------------------- diff --git a/tools/tool_backend_helpers.py b/tools/tool_backend_helpers.py index 810a51c63d..b1c5b7600c 100644 --- a/tools/tool_backend_helpers.py +++ b/tools/tool_backend_helpers.py @@ -6,6 +6,8 @@ import os from pathlib import Path from typing import Any, Dict +from utils import is_truthy_value + _DEFAULT_BROWSER_PROVIDER = "local" _DEFAULT_MODAL_MODE = "auto" @@ -115,7 +117,7 @@ def prefers_gateway(config_section: str) -> bool: from hermes_cli.config import load_config section = (load_config() or {}).get(config_section) if isinstance(section, dict): - return bool(section.get("use_gateway")) + return is_truthy_value(section.get("use_gateway"), default=False) except Exception: pass return False