mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
feat(auth) normalise the way in which we check whether a user has free/paid access to nous portal so we can expose behaviour and error messages accordingly.
This commit is contained in:
parent
0bf9b867cf
commit
406901b27d
32 changed files with 2470 additions and 181 deletions
|
|
@ -66,6 +66,7 @@ from tools.managed_tool_gateway import resolve_managed_tool_gateway
|
|||
from tools.tool_backend_helpers import (
|
||||
fal_key_is_configured,
|
||||
managed_nous_tools_enabled,
|
||||
nous_tool_gateway_unavailable_message,
|
||||
prefers_gateway,
|
||||
)
|
||||
|
||||
|
|
@ -452,12 +453,22 @@ def _submit_fal_request(model: str, arguments: Dict[str, Any]):
|
|||
# of a raw HTTP error from httpx.
|
||||
status = _extract_http_status(exc)
|
||||
if status is not None and 400 <= status < 500:
|
||||
gateway_message = ""
|
||||
if status in {401, 402, 403}:
|
||||
gateway_message = (
|
||||
"\n\n"
|
||||
+ nous_tool_gateway_unavailable_message(
|
||||
"managed FAL image generation",
|
||||
force_fresh=True,
|
||||
)
|
||||
)
|
||||
raise ValueError(
|
||||
f"Nous Subscription gateway rejected model '{model}' "
|
||||
f"(HTTP {status}). This model may not yet be enabled on "
|
||||
f"the Nous Portal's FAL proxy. Either:\n"
|
||||
f" • Set FAL_KEY in your environment to use FAL.ai directly, or\n"
|
||||
f" • Pick a different model via `hermes tools` → Image Generation."
|
||||
f"{gateway_message}"
|
||||
) from exc
|
||||
raise
|
||||
|
||||
|
|
@ -767,6 +778,11 @@ def _build_no_backend_setup_message() -> str:
|
|||
)
|
||||
else:
|
||||
lines.append(" - FAL_KEY environment variable is not set")
|
||||
gateway_message = nous_tool_gateway_unavailable_message(
|
||||
"managed FAL image generation",
|
||||
)
|
||||
if gateway_message:
|
||||
lines.append(f" - {gateway_message}")
|
||||
lines.append("")
|
||||
lines.append("To enable image generation, do one of:")
|
||||
lines.append(
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ from tools.tool_backend_helpers import (
|
|||
coerce_modal_mode,
|
||||
has_direct_modal_credentials,
|
||||
managed_nous_tools_enabled,
|
||||
nous_tool_gateway_unavailable_message,
|
||||
resolve_modal_backend_state,
|
||||
)
|
||||
|
||||
|
|
@ -1118,13 +1119,19 @@ 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 "
|
||||
"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."
|
||||
"Nous Tool Gateway access is not currently available and no direct "
|
||||
"Modal credentials/config were found. "
|
||||
+ nous_tool_gateway_unavailable_message(
|
||||
"managed Modal execution",
|
||||
)
|
||||
+ " Choose TERMINAL_MODAL_MODE=direct/auto to use direct Modal credentials."
|
||||
)
|
||||
if modal_state["mode"] == "managed":
|
||||
raise ValueError(
|
||||
"Modal backend is configured for managed mode, but the managed tool gateway is unavailable."
|
||||
"Modal backend is configured for managed mode, but the managed tool gateway is unavailable. "
|
||||
+ nous_tool_gateway_unavailable_message(
|
||||
"managed Modal execution",
|
||||
)
|
||||
)
|
||||
if modal_state["mode"] == "direct":
|
||||
raise ValueError(
|
||||
|
|
@ -2214,16 +2221,21 @@ def check_terminal_requirements() -> bool:
|
|||
if modal_state["managed_mode_blocked"]:
|
||||
logger.error(
|
||||
"Modal backend selected with TERMINAL_MODAL_MODE=managed, but "
|
||||
"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."
|
||||
"Nous Tool Gateway access is not currently available and no direct "
|
||||
"Modal credentials/config were found. %s Choose "
|
||||
"TERMINAL_MODAL_MODE=direct/auto to use direct Modal credentials.",
|
||||
nous_tool_gateway_unavailable_message(
|
||||
"managed Modal execution",
|
||||
),
|
||||
)
|
||||
return False
|
||||
if modal_state["mode"] == "managed":
|
||||
logger.error(
|
||||
"Modal backend selected with TERMINAL_MODAL_MODE=managed, but the managed "
|
||||
"tool gateway is unavailable. Configure the managed gateway or choose "
|
||||
"TERMINAL_MODAL_MODE=direct/auto."
|
||||
"tool gateway is unavailable. %s",
|
||||
nous_tool_gateway_unavailable_message(
|
||||
"managed Modal execution",
|
||||
),
|
||||
)
|
||||
return False
|
||||
elif modal_state["mode"] == "direct":
|
||||
|
|
|
|||
|
|
@ -15,28 +15,49 @@ _VALID_MODAL_MODES = {"auto", "direct", "managed"}
|
|||
|
||||
|
||||
def managed_nous_tools_enabled() -> bool:
|
||||
"""Return True when the user has an active paid Nous subscription.
|
||||
"""Return True when the user has paid Nous Portal service access.
|
||||
|
||||
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.
|
||||
Tool Gateway availability fails closed on unknown/error entitlement. We
|
||||
intentionally catch all exceptions and return False — never block startup.
|
||||
"""
|
||||
try:
|
||||
from hermes_cli.auth import get_nous_auth_status
|
||||
from hermes_cli.nous_account import get_nous_portal_account_info
|
||||
|
||||
status = get_nous_auth_status()
|
||||
if not status.get("logged_in"):
|
||||
account_info = get_nous_portal_account_info()
|
||||
if not account_info.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
|
||||
return account_info.paid_service_access is True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def nous_tool_gateway_unavailable_message(
|
||||
capability: str = "the Nous Tool Gateway",
|
||||
*,
|
||||
force_fresh: bool = False,
|
||||
) -> str:
|
||||
"""Return account-aware guidance for an unavailable Nous Tool Gateway path."""
|
||||
try:
|
||||
from hermes_cli.nous_account import (
|
||||
format_nous_portal_entitlement_message,
|
||||
get_nous_portal_account_info,
|
||||
)
|
||||
|
||||
account_info = get_nous_portal_account_info(force_fresh=force_fresh)
|
||||
message = format_nous_portal_entitlement_message(
|
||||
account_info,
|
||||
capability=capability,
|
||||
)
|
||||
if message:
|
||||
return message
|
||||
except Exception:
|
||||
pass
|
||||
return (
|
||||
f"{capability} is unavailable. Run `hermes model` to refresh your "
|
||||
"Nous Portal login and billing status."
|
||||
)
|
||||
|
||||
|
||||
def normalize_browser_cloud_provider(value: object | None) -> str:
|
||||
"""Return a normalized browser provider key."""
|
||||
provider = str(value or _DEFAULT_BROWSER_PROVIDER).strip().lower()
|
||||
|
|
|
|||
|
|
@ -38,7 +38,11 @@ from urllib.parse import urljoin
|
|||
|
||||
from utils import is_truthy_value
|
||||
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,
|
||||
nous_tool_gateway_unavailable_message,
|
||||
resolve_openai_audio_api_key,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -1643,7 +1647,12 @@ def _resolve_openai_audio_client_config() -> tuple[str, str]:
|
|||
if managed_gateway is None:
|
||||
message = "Neither stt.openai.api_key in config nor VOICE_TOOLS_OPENAI_KEY/OPENAI_API_KEY is set"
|
||||
if managed_nous_tools_enabled():
|
||||
message += ", and the managed OpenAI audio gateway is unavailable"
|
||||
message += (
|
||||
". "
|
||||
+ nous_tool_gateway_unavailable_message(
|
||||
"managed OpenAI audio for transcription",
|
||||
)
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
return managed_gateway.nous_user_token, urljoin(
|
||||
|
|
|
|||
|
|
@ -69,7 +69,12 @@ def get_env_value(name, default=None):
|
|||
value = _get_env_value(name)
|
||||
return default if value is None else value
|
||||
from tools.managed_tool_gateway import resolve_managed_tool_gateway
|
||||
from tools.tool_backend_helpers import managed_nous_tools_enabled, prefers_gateway, resolve_openai_audio_api_key
|
||||
from tools.tool_backend_helpers import (
|
||||
managed_nous_tools_enabled,
|
||||
nous_tool_gateway_unavailable_message,
|
||||
prefers_gateway,
|
||||
resolve_openai_audio_api_key,
|
||||
)
|
||||
from tools.xai_http import hermes_xai_user_agent
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -2206,8 +2211,13 @@ def _resolve_openai_audio_client_config() -> tuple[str, str]:
|
|||
managed_gateway = resolve_managed_tool_gateway("openai-audio")
|
||||
if managed_gateway is None:
|
||||
message = "Neither VOICE_TOOLS_OPENAI_KEY nor OPENAI_API_KEY is set"
|
||||
if managed_nous_tools_enabled():
|
||||
message += ", and the managed OpenAI audio gateway is unavailable"
|
||||
if managed_nous_tools_enabled() or prefers_gateway("tts"):
|
||||
message += (
|
||||
". "
|
||||
+ nous_tool_gateway_unavailable_message(
|
||||
"managed OpenAI audio for TTS",
|
||||
)
|
||||
)
|
||||
raise ValueError(message)
|
||||
|
||||
return managed_gateway.nous_user_token, urljoin(
|
||||
|
|
|
|||
|
|
@ -110,7 +110,11 @@ from tools.managed_tool_gateway import ( # noqa: F401 — backward-compat names
|
|||
read_nous_access_token as _read_nous_access_token,
|
||||
resolve_managed_tool_gateway,
|
||||
)
|
||||
from tools.tool_backend_helpers import managed_nous_tools_enabled, prefers_gateway # noqa: F401
|
||||
from tools.tool_backend_helpers import ( # noqa: F401
|
||||
managed_nous_tools_enabled,
|
||||
nous_tool_gateway_unavailable_message,
|
||||
prefers_gateway,
|
||||
)
|
||||
from tools.url_safety import is_safe_url
|
||||
from tools.website_policy import check_website_access
|
||||
import sys
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue