Merge branch 'main' of github.com:NousResearch/hermes-agent into feat/ink-refactor

This commit is contained in:
Brooklyn Nicholson
2026-04-16 14:48:33 -05:00
49 changed files with 1055 additions and 229 deletions

View File

@@ -10,7 +10,7 @@ import requests
from tools.browser_providers.base import CloudBrowserProvider
from tools.managed_tool_gateway import resolve_managed_tool_gateway
from tools.tool_backend_helpers import managed_nous_tools_enabled
from tools.tool_backend_helpers import managed_nous_tools_enabled, prefers_gateway
logger = logging.getLogger(__name__)
_pending_create_keys: Dict[str, str] = {}
@@ -75,7 +75,7 @@ class BrowserUseProvider(CloudBrowserProvider):
def _get_config_or_none(self) -> Optional[Dict[str, Any]]:
api_key = os.environ.get("BROWSER_USE_API_KEY")
if api_key:
if api_key and not prefers_gateway("browser"):
return {
"api_key": api_key,
"base_url": _BASE_URL,

View File

@@ -39,7 +39,7 @@ from urllib.parse import urlencode
import fal_client
from tools.debug_helpers import DebugSession
from tools.managed_tool_gateway import resolve_managed_tool_gateway
from tools.tool_backend_helpers import managed_nous_tools_enabled
from tools.tool_backend_helpers import managed_nous_tools_enabled, prefers_gateway
logger = logging.getLogger(__name__)
@@ -87,8 +87,9 @@ _managed_fal_client_lock = threading.Lock()
def _resolve_managed_fal_gateway():
"""Return managed fal-queue gateway config when direct FAL credentials are absent."""
if os.getenv("FAL_KEY"):
"""Return managed fal-queue gateway config when the user prefers the gateway
or direct FAL credentials are absent."""
if os.getenv("FAL_KEY") and not prefers_gateway("image_gen"):
return None
return resolve_managed_tool_gateway("fal-queue")

View File

@@ -762,8 +762,8 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
if modal_state["managed_mode_blocked"]:
raise ValueError(
"Modal backend is configured for managed mode, but "
"HERMES_ENABLE_NOUS_MANAGED_TOOLS is not enabled and no direct "
"Modal credentials/config were found. Enable the feature flag or "
"a paid Nous subscription is required for the Tool Gateway and no direct "
"Modal credentials/config were found. Log in with `hermes model` or "
"choose TERMINAL_MODAL_MODE=direct/auto."
)
if modal_state["mode"] == "managed":
@@ -1577,8 +1577,8 @@ def check_terminal_requirements() -> bool:
if modal_state["managed_mode_blocked"]:
logger.error(
"Modal backend selected with TERMINAL_MODAL_MODE=managed, but "
"HERMES_ENABLE_NOUS_MANAGED_TOOLS is not enabled and no direct "
"Modal credentials/config were found. Enable the feature flag "
"a paid Nous subscription is required for the Tool Gateway and no direct "
"Modal credentials/config were found. Log in with `hermes model` "
"or choose TERMINAL_MODAL_MODE=direct/auto."
)
return False

View File

@@ -6,7 +6,6 @@ import os
from pathlib import Path
from typing import Any, Dict
from utils import env_var_enabled
_DEFAULT_BROWSER_PROVIDER = "local"
_DEFAULT_MODAL_MODE = "auto"
@@ -14,8 +13,26 @@ _VALID_MODAL_MODES = {"auto", "direct", "managed"}
def managed_nous_tools_enabled() -> bool:
"""Return True when the hidden Nous-managed tools feature flag is enabled."""
return env_var_enabled("HERMES_ENABLE_NOUS_MANAGED_TOOLS")
"""Return True when the user has an active paid Nous subscription.
The Tool Gateway is available to any Nous subscriber who is NOT on
the free tier. We intentionally catch all exceptions and return
False — never block the agent startup path.
"""
try:
from hermes_cli.auth import get_nous_auth_status
status = get_nous_auth_status()
if not status.get("logged_in"):
return False
from hermes_cli.models import check_nous_free_tier
if check_nous_free_tier():
return False # free-tier users don't get gateway access
return True
except Exception:
return False
def normalize_browser_cloud_provider(value: object | None) -> str:
@@ -87,3 +104,18 @@ def resolve_openai_audio_api_key() -> str:
os.getenv("VOICE_TOOLS_OPENAI_KEY", "")
or os.getenv("OPENAI_API_KEY", "")
).strip()
def prefers_gateway(config_section: str) -> bool:
"""Return True when the user opted into the Tool Gateway for this tool.
Reads ``<section>.use_gateway`` from config.yaml. Never raises.
"""
try:
from hermes_cli.config import load_config
section = (load_config() or {}).get(config_section)
if isinstance(section, dict):
return bool(section.get("use_gateway"))
except Exception:
pass
return False

View File

@@ -44,7 +44,7 @@ from hermes_constants import display_hermes_home
logger = logging.getLogger(__name__)
from tools.managed_tool_gateway import resolve_managed_tool_gateway
from tools.tool_backend_helpers import managed_nous_tools_enabled, resolve_openai_audio_api_key
from tools.tool_backend_helpers import managed_nous_tools_enabled, prefers_gateway, resolve_openai_audio_api_key
from tools.xai_http import hermes_xai_user_agent
# ---------------------------------------------------------------------------
@@ -823,9 +823,13 @@ def check_tts_requirements() -> bool:
def _resolve_openai_audio_client_config() -> tuple[str, str]:
"""Return direct OpenAI audio config or a managed gateway fallback."""
"""Return direct OpenAI audio config or a managed gateway fallback.
When ``tts.use_gateway`` is set in config, the Tool Gateway is preferred
even if direct OpenAI credentials are present.
"""
direct_api_key = resolve_openai_audio_api_key()
if direct_api_key:
if direct_api_key and not prefers_gateway("tts"):
return direct_api_key, DEFAULT_OPENAI_BASE_URL
managed_gateway = resolve_managed_tool_gateway("openai-audio")

View File

@@ -59,7 +59,7 @@ from tools.managed_tool_gateway import (
read_nous_access_token as _read_nous_access_token,
resolve_managed_tool_gateway,
)
from tools.tool_backend_helpers import managed_nous_tools_enabled
from tools.tool_backend_helpers import managed_nous_tools_enabled, prefers_gateway
from tools.url_safety import is_safe_url
from tools.website_policy import check_website_access
@@ -165,8 +165,8 @@ def _raise_web_backend_configuration_error() -> None:
)
if managed_nous_tools_enabled():
message += (
" If you have the hidden Nous-managed tools flag enabled, you can also login to Nous "
"(`hermes model`) and provide FIRECRAWL_GATEWAY_URL or TOOL_GATEWAY_DOMAIN."
" With your Nous subscription you can also use the Tool Gateway — "
"run `hermes tools` and select Nous Subscription as the web provider."
)
raise ValueError(message)
@@ -176,8 +176,8 @@ def _firecrawl_backend_help_suffix() -> str:
if not managed_nous_tools_enabled():
return ""
return (
", or, if you have the hidden Nous-managed tools flag enabled, login to Nous and use "
"FIRECRAWL_GATEWAY_URL or TOOL_GATEWAY_DOMAIN"
", or use the Nous Tool Gateway via your subscription "
"(FIRECRAWL_GATEWAY_URL or TOOL_GATEWAY_DOMAIN)"
)
@@ -205,13 +205,14 @@ def _web_requires_env() -> list[str]:
def _get_firecrawl_client():
"""Get or create Firecrawl client.
Direct Firecrawl takes precedence when explicitly configured. Otherwise
Hermes falls back to the Firecrawl tool-gateway for logged-in Nous Subscribers.
When ``web.use_gateway`` is set in config, the Tool Gateway is preferred
even if direct Firecrawl credentials are present. Otherwise direct
Firecrawl takes precedence when explicitly configured.
"""
global _firecrawl_client, _firecrawl_client_config
direct_config = _get_direct_firecrawl_config()
if direct_config is not None:
if direct_config is not None and not prefers_gateway("web"):
kwargs, client_config = direct_config
else:
managed_gateway = resolve_managed_tool_gateway(