Merge remote-tracking branch 'origin/main' into ethie/pm-clean
This commit is contained in:
@@ -91,7 +91,7 @@ hermes doctor # Diagnostica cualquier problema
|
||||
Hermes funciona con cualquier proveedor que quieras — eso no cambiará. Pero si prefieres no recopilar cinco claves API separadas para el modelo, búsqueda web, generación de imágenes, TTS y un navegador en la nube, **[Nous Portal](https://portal.nousresearch.com)** las cubre todas bajo una sola suscripción:
|
||||
|
||||
- **Más de 300 modelos** — elige cualquiera con `/model <nombre>`
|
||||
- **Tool Gateway** — búsqueda web (Firecrawl), generación de imágenes (FAL), texto a voz (OpenAI), navegador en la nube (Browser Use), todo enrutado a través de tu suscripción. Sin cuentas adicionales.
|
||||
- **Tool Gateway** — búsqueda web, generación de imágenes (FAL), texto a voz (OpenAI), navegador en la nube (Browser Use), todo enrutado a través de tu suscripción. Sin cuentas adicionales.
|
||||
|
||||
Un comando desde una instalación nueva:
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ hermes doctor # Diagnose any issues
|
||||
Hermes works with whatever provider you want — that's not changing. But if you'd rather not collect five separate API keys for the model, web search, image generation, TTS, and a cloud browser, **[Nous Portal](https://portal.nousresearch.com)** covers all of them under one subscription:
|
||||
|
||||
- **300+ models** — pick any of them with `/model <name>`
|
||||
- **Tool Gateway** — web search (Firecrawl), image generation (FAL), text-to-speech (OpenAI), cloud browser (Browser Use), all routed through your sub. No extra accounts.
|
||||
- **Tool Gateway** — web search, image generation (FAL), text-to-speech (OpenAI), cloud browser (Browser Use), all routed through your sub. No extra accounts.
|
||||
|
||||
One command from a fresh install:
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ hermes doctor # کسی بھی مسئلے کی تشخیص کریں
|
||||
ہرمیس آپ کے پسندیدہ پرووائیڈر کے ساتھ کام کرتا ہے — یہ چیز تبدیل نہیں ہو رہی۔ لیکن اگر آپ ماڈل، ویب سرچ، امیج جنریشن، TTS، اور کلاؤڈ براؤزر کے لیے پانچ الگ الگ API کیز جمع نہیں کرنا چاہتے، تو **[Nous Portal](https://portal.nousresearch.com)** ان سب کو ایک ہی سبسکرپشن کے تحت کور کرتا ہے:
|
||||
|
||||
- **300+ ماڈلز** — ان میں سے کوئی بھی ماڈل `/model <name>` کے ذریعے منتخب کریں
|
||||
- **ٹول گیٹ وے (Tool Gateway)** — ویب سرچ (Firecrawl)، امیج جنریشن (FAL)، ٹیکسٹ ٹو سپیچ (OpenAI)، کلاؤڈ براؤزر (Browser Use)، یہ سب آپ کی سبسکرپشن کے ذریعے چلتے ہیں۔ کسی اضافی اکاؤنٹ کی ضرورت نہیں۔
|
||||
- **ٹول گیٹ وے (Tool Gateway)** — ویب سرچ، امیج جنریشن (FAL)، ٹیکسٹ ٹو سپیچ (OpenAI)، کلاؤڈ براؤزر (Browser Use)، یہ سب آپ کی سبسکرپشن کے ذریعے چلتے ہیں۔ کسی اضافی اکاؤنٹ کی ضرورت نہیں۔
|
||||
|
||||
نئی انسٹالیشن کے بعد بس ایک کمانڈ کی ضرورت ہے:
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ hermes doctor # 诊断问题
|
||||
Hermes 始终允许你使用任意服务商,这点不会改变。但如果你不想为模型、网页搜索、图像生成、TTS、云浏览器分别去申请五个不同的 API Key,**[Nous Portal](https://portal.nousresearch.com)** 用一个订阅就能覆盖全部:
|
||||
|
||||
- **300+ 模型** — 用 `/model <name>` 随时切换
|
||||
- **Tool Gateway** — 网页搜索(Firecrawl)、图像生成(FAL)、文本转语音(OpenAI)、云浏览器(Browser Use),全部通过订阅托管。无需额外注册任何账户。
|
||||
- **Tool Gateway** — 网页搜索、图像生成(FAL)、文本转语音(OpenAI)、云浏览器(Browser Use),全部通过订阅托管。无需额外注册任何账户。
|
||||
|
||||
全新安装时一条命令即可:
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class _FeatureSpec:
|
||||
_FEATURES: Dict[str, _FeatureSpec] = {
|
||||
"web": _FeatureSpec(
|
||||
"Web tools", True, "firecrawl", "firecrawl", ("web", "backend"),
|
||||
"Web search & extract (Firecrawl)", "Firecrawl/Exa/Parallel/Tavily/Perplexity/Keenable key or SearXNG",
|
||||
"Web search & extract", "Firecrawl/Exa/Parallel/Tavily/Perplexity/Keenable key or SearXNG",
|
||||
("PARALLEL_API_KEY", "TAVILY_API_KEY", "PERPLEXITY_API_KEY", "FIRECRAWL_API_KEY", "FIRECRAWL_API_URL"),
|
||||
),
|
||||
"image_gen": _FeatureSpec(
|
||||
|
||||
@@ -12,7 +12,7 @@ SUBSCRIPTION_URL = "https://portal.nousresearch.com/manage-subscription"
|
||||
DOCS_URL = "https://hermes-agent.nousresearch.com/docs/user-guide/features/tool-gateway"
|
||||
# Static `portal tools` catalog — the partners Tool Gateway routes to today: (key, label, partner).
|
||||
_CATALOG = [
|
||||
("web", "Web search & extract", "Firecrawl"),
|
||||
("web", "Web search & extract", "Nous-managed"),
|
||||
("image_gen", "Image generation", "FAL"),
|
||||
("tts", "Text-to-speech", "OpenAI TTS"),
|
||||
("browser", "Browser automation", "Browser Use"),
|
||||
|
||||
@@ -283,9 +283,9 @@ TOOL_CATEGORIES = {
|
||||
# Provider rows come from plugins.web.<vendor> via _plugin_web_search_providers(). Only the two
|
||||
# non-provider firecrawl setup-flow rows live here: managed via Nous subscription, and self-hosted.
|
||||
"providers": [
|
||||
{"name": "Nous Subscription", "badge": "subscription", "tag": "Managed Firecrawl billed to your subscription",
|
||||
{"name": "Nous Subscription", "badge": "subscription", "tag": "Managed web search and extract billed to your subscription",
|
||||
"web_backend": "firecrawl", "env_vars": [], **_NOUS, "managed_nous_feature": "web",
|
||||
"override_env_vars": ["FIRECRAWL_API_KEY", "FIRECRAWL_API_URL"]},
|
||||
"override_env_vars": ["FIRECRAWL_API_KEY", "FIRECRAWL_API_URL", "PERPLEXITY_API_KEY"]},
|
||||
{"name": "Firecrawl Self-Hosted", "badge": "free · self-hosted", "tag": "Run your own Firecrawl instance (Docker)",
|
||||
"web_backend": "firecrawl",
|
||||
"env_vars": [_key("FIRECRAWL_API_URL", "Your Firecrawl instance URL (e.g., http://localhost:3002)")]},
|
||||
|
||||
@@ -835,7 +835,8 @@ def _print_provider_selection(provider: dict, managed_feature, *, reconfigure: b
|
||||
_print_success(f" Browser engine set to: {provider['browser_engine']}")
|
||||
if provider.get("web_backend"):
|
||||
tier = f" ({provider['web_tier']} tier)" if reconfigure and provider.get("web_tier") else ""
|
||||
_print_success(f" Web backend set to: {provider['web_backend']}{tier}")
|
||||
backend = NOUS_MANAGED_PROVIDER if managed_feature else provider["web_backend"]
|
||||
_print_success(f" Web backend set to: {backend}{tier}")
|
||||
if reconfigure and provider.get("computer_use_backend"):
|
||||
_print_success(f" Computer Use backend set to: {provider['computer_use_backend']}")
|
||||
|
||||
|
||||
@@ -18,11 +18,12 @@ Config keys this provider responds to::
|
||||
|
||||
Env vars::
|
||||
|
||||
PERPLEXITY_API_KEY=... # https://www.perplexity.ai/account/api (required)
|
||||
PERPLEXITY_API_KEY=... # required for direct search and extract
|
||||
PERPLEXITY_BASE_URL=... # optional override of https://api.perplexity.ai
|
||||
|
||||
Keyed only — Perplexity has no anonymous tier, so this provider is not a
|
||||
member of the zero-config keyless ring and never resolves without a key.
|
||||
No anonymous tier. The Nous Subscription selection serves search through
|
||||
``perplexity-gateway.<TOOL_GATEWAY_DOMAIN>`` using the Nous token; a direct
|
||||
key takes precedence. Managed extract stays on Firecrawl.
|
||||
|
||||
Extract caveat: Perplexity's only supported page-content route returns the
|
||||
passages of a page relevant to a *query* (elisions marked ``…``), not the
|
||||
@@ -69,8 +70,18 @@ def _missing_key_error() -> str:
|
||||
return f"PERPLEXITY_API_KEY is not set. Get a key at {_KEY_URL}"
|
||||
|
||||
|
||||
def _perplexity_request(endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""POST to the Perplexity API and return the parsed JSON response.
|
||||
def _managed_gateway(token_reader=None):
|
||||
"""Nous Tool Gateway config when web_search is on the managed route, else None."""
|
||||
from tools import managed_tool_gateway as gw
|
||||
from tools.web_tools import _managed_web_search
|
||||
|
||||
if not _managed_web_search():
|
||||
return None
|
||||
return gw.resolve_managed_tool_gateway("perplexity", token_reader=token_reader)
|
||||
|
||||
|
||||
def _perplexity_request(endpoint: str, payload: Dict[str, Any], gateway=None) -> Dict[str, Any]:
|
||||
"""POST to Perplexity or the supplied gateway; return parsed JSON.
|
||||
|
||||
Raises ``ValueError`` when the key is missing or on any non-2xx status,
|
||||
carrying the response body so Perplexity's own error text (invalid key,
|
||||
@@ -79,9 +90,14 @@ def _perplexity_request(endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any
|
||||
from agent.web_search_provider import get_provider_env
|
||||
|
||||
api_key = get_provider_env("PERPLEXITY_API_KEY")
|
||||
if not api_key:
|
||||
headers = _HEADERS
|
||||
if gateway is not None:
|
||||
# Nous-owned key behind the gateway: identify the harness only, not a per-user integration.
|
||||
base_url, api_key, headers = gateway.gateway_origin.rstrip("/"), gateway.nous_user_token, {"User-Agent": _HEADERS["User-Agent"]}
|
||||
elif api_key:
|
||||
base_url = (get_provider_env("PERPLEXITY_BASE_URL") or _DEFAULT_BASE_URL).rstrip("/")
|
||||
else:
|
||||
raise ValueError(_missing_key_error())
|
||||
base_url = (get_provider_env("PERPLEXITY_BASE_URL") or _DEFAULT_BASE_URL).rstrip("/")
|
||||
url = f"{base_url}/{endpoint.lstrip('/')}"
|
||||
logger.info("Perplexity %s request to %s", endpoint, url)
|
||||
|
||||
@@ -92,7 +108,7 @@ def _perplexity_request(endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
**_HEADERS,
|
||||
**headers,
|
||||
},
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
@@ -156,7 +172,7 @@ def _query_for_urls(urls: List[str]) -> str:
|
||||
|
||||
|
||||
class PerplexityWebSearchProvider(WebSearchProvider):
|
||||
"""Perplexity Search API (search) + content snippets (extract), keyed only."""
|
||||
"""Direct or managed search; direct-key content snippets for extract."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -167,10 +183,11 @@ class PerplexityWebSearchProvider(WebSearchProvider):
|
||||
return "Perplexity"
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Return True when ``PERPLEXITY_API_KEY`` is set to a non-empty value."""
|
||||
"""True with a ``PERPLEXITY_API_KEY``, or on the managed route with a likely-usable Nous token."""
|
||||
from agent.web_search_provider import get_provider_env
|
||||
from tools.managed_tool_gateway import peek_nous_access_token
|
||||
|
||||
return bool(get_provider_env("PERPLEXITY_API_KEY"))
|
||||
return bool(get_provider_env("PERPLEXITY_API_KEY")) or _managed_gateway(token_reader=peek_nous_access_token) is not None
|
||||
|
||||
def supports_search(self) -> bool:
|
||||
return True
|
||||
@@ -191,15 +208,24 @@ class PerplexityWebSearchProvider(WebSearchProvider):
|
||||
if is_interrupted():
|
||||
return {"success": False, "error": "Interrupted"}
|
||||
|
||||
logger.info("Perplexity search: '%s' (limit=%d)", query, limit)
|
||||
raw = _perplexity_request(
|
||||
"search",
|
||||
{
|
||||
"query": query,
|
||||
"max_results": max(1, min(limit, _MAX_SEARCH_RESULTS)),
|
||||
"search_context_size": "low",
|
||||
},
|
||||
)
|
||||
from agent.web_search_provider import get_provider_env
|
||||
from tools.web_tools import _managed_web_search
|
||||
|
||||
direct = bool(get_provider_env("PERPLEXITY_API_KEY"))
|
||||
gateway = None if direct else _managed_gateway()
|
||||
if gateway is None and not direct and _managed_web_search():
|
||||
from tools.tool_backend_helpers import NOUS_MANAGED_PROVIDER, selection_error
|
||||
raise ValueError(selection_error(
|
||||
"web", NOUS_MANAGED_PROVIDER, "the Nous Tool Gateway is not available (not entitled or unreachable)"))
|
||||
logger.info("Perplexity search: '%s' (limit=%d%s)", query, limit, ", managed" if gateway else "")
|
||||
payload = {
|
||||
"query": query,
|
||||
"max_results": max(1, min(limit, _MAX_SEARCH_RESULTS)),
|
||||
"search_context_size": "low",
|
||||
}
|
||||
if gateway is not None:
|
||||
payload["search_type"] = "fast"
|
||||
raw = _perplexity_request("search", payload, gateway)
|
||||
return _normalize_search_results(raw)
|
||||
except ValueError as exc:
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
@@ -220,7 +220,7 @@ def test_prompt_enable_tool_gateway_pool_offers_covered_tools_only(monkeypatch):
|
||||
ns.prompt_enable_tool_gateway(config)
|
||||
|
||||
blob = " ".join(captured["items"]).lower()
|
||||
assert "firecrawl" in blob # web offered
|
||||
assert "web search & extract" in blob # web offered
|
||||
assert "video" not in blob # video NOT offered to a pool user
|
||||
|
||||
|
||||
|
||||
20
tests/hermes_cli/test_web_provider_selection.py
Normal file
20
tests/hermes_cli/test_web_provider_selection.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Web setup reports the selected backend without interrupting config writes."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("managed,backend", [(True, "firecrawl"), (False, "perplexity"), (False, "exa")])
|
||||
def test_web_selection_reports_the_backend_it_writes(monkeypatch, managed, backend):
|
||||
import hermes_cli.tools_config_providers as providers
|
||||
|
||||
monkeypatch.setattr(providers, "_nous_provider_gate", lambda *a, **kw: True)
|
||||
messages = []
|
||||
monkeypatch.setattr(providers, "_print_success", messages.append)
|
||||
row = {"name": "Test provider", "web_backend": backend, "env_vars": []}
|
||||
if managed:
|
||||
row["managed_nous_feature"] = "web"
|
||||
config = {}
|
||||
providers._configure_provider(row, config)
|
||||
selected = config["web"]["backend"]
|
||||
assert selected == ("nous" if managed else backend)
|
||||
assert f" Web backend set to: {selected}" in messages
|
||||
136
tests/tools/test_web_managed_routing.py
Normal file
136
tests/tools/test_web_managed_routing.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Real config, dispatch and local HTTP must preserve web billing boundaries."""
|
||||
|
||||
import json
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from threading import Thread
|
||||
|
||||
import pytest
|
||||
import httpx
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def local_gateway(monkeypatch):
|
||||
from tools import managed_tool_gateway
|
||||
|
||||
requests = []
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
||||
requests.append((self.path, dict(self.headers), body))
|
||||
if self.path == "/v2/search":
|
||||
status = 200
|
||||
payload = {"success": True, "data": {"web": [{
|
||||
"title": "Local fixture", "url": "https://example.test", "description": "fixture",
|
||||
}]}}
|
||||
else:
|
||||
status, payload = 503, {"error": "local test outage"}
|
||||
content = json.dumps(payload).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(content)))
|
||||
self.end_headers()
|
||||
self.wfile.write(content)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
base = f"http://127.0.0.1:{server.server_port}"
|
||||
monkeypatch.setenv("TOOL_GATEWAY_USER_TOKEN", "test-nous-token")
|
||||
monkeypatch.setenv("PERPLEXITY_GATEWAY_URL", base + "/perplexity")
|
||||
monkeypatch.setenv("FIRECRAWL_GATEWAY_URL", base)
|
||||
monkeypatch.setenv("PERPLEXITY_BASE_URL", base + "/direct")
|
||||
# Only entitlement is stubbed: route and credential resolution remain real.
|
||||
monkeypatch.setattr(managed_tool_gateway, "managed_nous_tools_enabled", lambda **kw: True)
|
||||
|
||||
# CI omits the firecrawl extra; stand in for the SDK, still over real HTTP.
|
||||
class FirecrawlSDK:
|
||||
def __init__(self, api_key, api_url):
|
||||
self.api_key, self.api_url = api_key, api_url
|
||||
|
||||
def search(self, query, limit):
|
||||
response = httpx.post(
|
||||
f"{self.api_url}/v2/search", json={"query": query, "limit": limit},
|
||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||
)
|
||||
return response.json()
|
||||
|
||||
monkeypatch.setattr("plugins.web.firecrawl.provider.Firecrawl", FirecrawlSDK)
|
||||
monkeypatch.setattr("tools.web_tools._firecrawl_client", None, raising=False)
|
||||
try:
|
||||
yield requests
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"selection,direct_key,expected_paths",
|
||||
[
|
||||
({"backend": "nous"}, False, ["/perplexity/search", "/v2/search"]),
|
||||
({"backend": "nous"}, True, ["/direct/search"]),
|
||||
({"backend": "nous", "search_backend": "perplexity"}, True, ["/direct/search"]),
|
||||
({"search_backend": "perplexity"}, False, []),
|
||||
({"backend": "nous", "search_backend": "perplexity"}, False, []),
|
||||
({"backend": "perplexity"}, False, []),
|
||||
({"backend": "firecrawl", "search_backend": "perplexity"}, False, []),
|
||||
({}, False, ["/perplexity/search", "/v2/search"]),
|
||||
({"extract_backend": "exa"}, False, ["/perplexity/search", "/v2/search"]),
|
||||
],
|
||||
)
|
||||
def test_only_managed_search_may_use_billed_fallback(
|
||||
monkeypatch, tmp_path, local_gateway, selection, direct_key, expected_paths,
|
||||
):
|
||||
from hermes_cli.config import atomic_config_write
|
||||
from tools import web_tools
|
||||
from tests.tools.conftest import register_all_web_providers
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
atomic_config_write(tmp_path / "config.yaml", {
|
||||
"web": {**selection, "keyless_rescue": False, "cache_enabled": True},
|
||||
})
|
||||
if direct_key:
|
||||
monkeypatch.setenv("PERPLEXITY_API_KEY", "test-direct-key")
|
||||
else:
|
||||
monkeypatch.delenv("PERPLEXITY_API_KEY", raising=False)
|
||||
register_all_web_providers()
|
||||
expected = [
|
||||
(path, "Bearer test-direct-key" if path == "/direct/search" else "Bearer test-nous-token")
|
||||
for path in expected_paths
|
||||
]
|
||||
for attempt in (1, 2):
|
||||
result = json.loads(web_tools.web_search_tool("local fixture", limit=3))
|
||||
# A fallback must not be cached: the identical query retries Perplexity.
|
||||
assert [(path, headers["Authorization"]) for path, headers, _ in local_gateway] == expected * attempt
|
||||
assert result["success"] == bool(expected_paths and expected_paths[-1] == "/v2/search")
|
||||
if result["success"]:
|
||||
assert result["data"]["fallback_from"] == "managed_primary"
|
||||
assert "local test outage" in result["data"]["backend_error"]
|
||||
assert web_tools._get_extract_backend() == selection.get("extract_backend", "firecrawl")
|
||||
for path, headers, body in local_gateway:
|
||||
if path != "/v2/search":
|
||||
managed = path == "/perplexity/search"
|
||||
assert body.get("search_type") == ("fast" if managed else None)
|
||||
assert body["search_context_size"] == "low"
|
||||
assert headers.get("X-Pplx-Integration") == (None if managed else "hermes-agent")
|
||||
|
||||
|
||||
def test_unentitled_managed_search_names_the_gateway(monkeypatch, tmp_path, local_gateway):
|
||||
from hermes_cli.config import atomic_config_write
|
||||
from tools import managed_tool_gateway, web_tools
|
||||
from tests.tools.conftest import register_all_web_providers
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
atomic_config_write(tmp_path / "config.yaml", {"web": {"backend": "nous", "keyless_rescue": False}})
|
||||
monkeypatch.delenv("PERPLEXITY_API_KEY", raising=False)
|
||||
monkeypatch.setattr(managed_tool_gateway, "managed_nous_tools_enabled", lambda **kw: False)
|
||||
register_all_web_providers()
|
||||
|
||||
error = json.loads(web_tools.web_search_tool("local fixture", limit=3))["error"]
|
||||
assert "Nous Tool Gateway" in error and "hermes tools" in error
|
||||
assert "PERPLEXITY_API_KEY" not in error
|
||||
assert local_gateway == []
|
||||
@@ -20,7 +20,7 @@ from plugins.web.firecrawl.provider import _is_tool_gateway_ready, check_firecra
|
||||
from tools.debug_helpers import DebugSession
|
||||
from tools.tool_backend_helpers import NOUS_MANAGED_PROVIDER, read_selection, selection_exists
|
||||
from tools.url_safety import async_is_safe_url
|
||||
from tools.web_tools_rescue import _rescue_eligible, _rescue_search
|
||||
from tools.web_tools_rescue import _managed_search_fallback, _rescue_eligible, _rescue_search
|
||||
from tools.web_tools_truncate import _effective_char_limit, _trim_results, _truncate_results, convert_base64_images_to_links
|
||||
from tools.web_tools_extract import (
|
||||
_extract_safe_urls, _merge_in_order, _no_provider_error, _resolve_extract_provider, _result_entry,
|
||||
@@ -151,9 +151,22 @@ def _keyless_backend() -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def _managed_web_search() -> bool:
|
||||
"""True when web_search is on the managed Nous route: the stored ``nous`` selection, or a
|
||||
never-configured install whose autodetect lands on the gateway. A stored vendor selection never is."""
|
||||
if _configured_backend("search_backend"):
|
||||
return False
|
||||
selected = read_selection("web")
|
||||
if selected is not None:
|
||||
return selected == NOUS_MANAGED_PROVIDER
|
||||
return _get_backend() == "firecrawl" and not (_has_env("FIRECRAWL_API_KEY") or _has_env("FIRECRAWL_API_URL")) and _is_tool_gateway_ready()
|
||||
|
||||
|
||||
def _get_search_backend() -> str:
|
||||
"""Backend for web_search: ``web.search_backend`` (strict, no probe) > ``web.backend`` > autodetect."""
|
||||
return _configured_backend("search_backend") or _get_backend()
|
||||
"""Backend for web_search: ``web.search_backend`` (strict, no probe) > ``web.backend`` > autodetect.
|
||||
The managed Nous route serves search from Perplexity (extract stays on Firecrawl); managed Firecrawl
|
||||
is the per-call fallback, see ``_memoized_search``."""
|
||||
return _configured_backend("search_backend") or ("perplexity" if _managed_web_search() else _get_backend())
|
||||
|
||||
|
||||
def _get_extract_backend() -> str:
|
||||
@@ -190,7 +203,7 @@ _BUILTIN_AVAILABILITY = {
|
||||
"firecrawl": lambda: check_firecrawl_api_key(),
|
||||
"tavily": lambda: _has_env("TAVILY_API_KEY")
|
||||
or any(_configured_backend(k) == "tavily" for k in ("backend", "search_backend", "extract_backend")),
|
||||
"perplexity": lambda: _has_env("PERPLEXITY_API_KEY"),
|
||||
"perplexity": lambda: _has_env("PERPLEXITY_API_KEY") or _managed_web_search(),
|
||||
"searxng": lambda: _has_env("SEARXNG_URL"),
|
||||
"brave-free": lambda: _has_env("BRAVE_SEARCH_API_KEY"),
|
||||
"ddgs": lambda: _ddgs_package_importable(),
|
||||
@@ -226,7 +239,7 @@ def _web_requires_env() -> list[str]:
|
||||
Contract: set var -> tool sees it; extras are harmless for the not-logged-in."""
|
||||
return [
|
||||
"EXA_API_KEY", "PARALLEL_API_KEY", "TAVILY_API_KEY", "PERPLEXITY_API_KEY", "KEENABLE_API_KEY", "FIRECRAWL_API_KEY",
|
||||
"FIRECRAWL_API_URL", "FIRECRAWL_GATEWAY_URL", "TOOL_GATEWAY_DOMAIN", "TOOL_GATEWAY_SCHEME",
|
||||
"FIRECRAWL_API_URL", "FIRECRAWL_GATEWAY_URL", "PERPLEXITY_GATEWAY_URL", "TOOL_GATEWAY_DOMAIN", "TOOL_GATEWAY_SCHEME",
|
||||
"TOOL_GATEWAY_USER_TOKEN",
|
||||
]
|
||||
|
||||
@@ -328,14 +341,25 @@ def _memoized_search(provider, query: str, limit: int) -> dict:
|
||||
fetch_limit = bucket_limit(limit)
|
||||
try:
|
||||
resp = provider.search(query, fetch_limit)
|
||||
except Exception as exc: # noqa: BLE001 — candidate for rescue
|
||||
if not _rescue_eligible(provider):
|
||||
except Exception as exc: # noqa: BLE001 — candidate for fallback / rescue
|
||||
served = _served_after_failure(str(exc), fetch_limit)
|
||||
if served is None:
|
||||
raise
|
||||
return _rescue_search(provider.name, str(exc), query, fetch_limit), True
|
||||
if not resp.get("success") and _rescue_eligible(provider):
|
||||
return _rescue_search(provider.name, str(resp.get("error", "")), query, fetch_limit), True
|
||||
return served, True
|
||||
if not resp.get("success"):
|
||||
served = _served_after_failure(str(resp.get("error", "")), fetch_limit)
|
||||
if served is not None:
|
||||
return served, True
|
||||
return resp, False
|
||||
|
||||
def _served_after_failure(error: str, fetch_limit: int) -> Optional[dict]:
|
||||
"""Managed Firecrawl for a failed managed Perplexity call, else the one-shot keyless rescue when
|
||||
eligible; None means the vendor's own failure stands."""
|
||||
fallback = _managed_search_fallback(provider, error, query, fetch_limit)
|
||||
if fallback is not None:
|
||||
return fallback
|
||||
return _rescue_search(provider.name, error, query, fetch_limit) if _rescue_eligible(provider) else None
|
||||
|
||||
response_data = search_memo.lookup(provider.name, query, limit)
|
||||
if response_data is None:
|
||||
with search_memo.flight_lock(provider.name, query, limit):
|
||||
|
||||
@@ -47,6 +47,30 @@ def _ring_vendor_keyless(name: str) -> bool:
|
||||
return use_keyless(name, get_provider_env(key_var) if key_var else "")
|
||||
|
||||
|
||||
def _managed_search_fallback(provider, original_error: str, query: str, limit: int):
|
||||
"""Try managed Firecrawl for this call only; None leaves the original error for keyless rescue."""
|
||||
from agent.web_search_provider import get_provider_env
|
||||
from tools.web_tools import _managed_web_search
|
||||
if (getattr(provider, "name", "") != "perplexity"
|
||||
or get_provider_env("PERPLEXITY_API_KEY") or not _managed_web_search()):
|
||||
return None
|
||||
logger.warning("web_search managed Perplexity failed (%s); serving this call from managed Firecrawl", (original_error or "")[:200])
|
||||
try:
|
||||
from agent.web_search_registry import get_provider
|
||||
resp = get_provider("firecrawl").search(query, limit)
|
||||
except Exception as exc: # noqa: BLE001 — fallback is best-effort
|
||||
resp = {"success": False, "error": str(exc)}
|
||||
if not resp.get("success"):
|
||||
logger.warning("managed Firecrawl fallback failed too: %s", str(resp.get("error", ""))[:200])
|
||||
return None
|
||||
resp.setdefault("data", {}).update(
|
||||
fallback_from="managed_primary",
|
||||
backend_error=f"Primary managed search failed this call ({(original_error or 'unknown error')[:300]}); "
|
||||
"result served by the managed fallback. The next call will use the primary again.",
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
def _rescue_eligible(provider) -> bool:
|
||||
"""True when a failed call on *provider* should get a one-shot rescue.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ This guide walks you through running Hermes Agent on a [Nous Portal](https://por
|
||||
- A web browser on the machine you're setting up (or SSH port forwarding — see [OAuth over SSH](./oauth-over-ssh.md))
|
||||
- About 5 minutes
|
||||
|
||||
You do **not** need: an OpenAI key, an Anthropic key, a Firecrawl account, a FAL account, a Browser Use account, or any other per-vendor credential. That's the whole point.
|
||||
You do **not** need: an OpenAI key, an Anthropic key, a web search account, a FAL account, a Browser Use account, or any other per-vendor credential. That's the whole point.
|
||||
|
||||
## 1. Get a subscription
|
||||
|
||||
@@ -91,7 +91,7 @@ Try something that exercises both the model and the Tool Gateway:
|
||||
Hey, search the web for "Hermes Agent release notes" and summarize the top 3 hits.
|
||||
```
|
||||
|
||||
You should see Hermes call `web_search` (Firecrawl-backed, through the gateway) and respond with a summary. If the search runs and the response makes sense, you're done — the Portal is wired up end to end.
|
||||
You should see Hermes call `web_search` (through the gateway) and respond with a summary. If the search runs and the response makes sense, you're done — the Portal is wired up end to end.
|
||||
|
||||
## 5. Pick the model you actually want
|
||||
|
||||
@@ -257,7 +257,7 @@ hermes auth logout nous # wipes the local refresh token
|
||||
| Without Portal | With Portal |
|
||||
|----------------|-------------|
|
||||
| 1× OpenRouter / Anthropic / OpenAI key in `.env` | 1× OAuth refresh token, no `.env` keys |
|
||||
| 1× Firecrawl key for web | Web routed through gateway |
|
||||
| 1× web search key | Web routed through gateway |
|
||||
| 1× FAL key for image gen | Image gen routed through gateway |
|
||||
| 1× Browser Use / Browserbase key for browser | Browser routed through gateway |
|
||||
| 1× OpenAI key for TTS / voice mode | TTS routed through gateway |
|
||||
|
||||
@@ -54,13 +54,13 @@ The same subscription unlocks the [Tool Gateway](../user-guide/features/tool-gat
|
||||
|
||||
| Tool | Partner | What it does |
|
||||
|------|---------|--------------|
|
||||
| **Web search & extract** | Firecrawl | Agent-grade search and full-page extraction. No Firecrawl API key, no rate limit babysitting. |
|
||||
| **Web search & extract** | Nous-managed | Agent-grade search and full-page extraction. No search API key, no rate limit babysitting. |
|
||||
| **Image generation** | FAL | Nine models under one endpoint: FLUX 2 Klein 9B, FLUX 2 Pro, Z-Image Turbo, Nano Banana Pro (Gemini 3 Pro Image), GPT Image 1.5, GPT Image 2, Ideogram V3, Recraft V4 Pro, Qwen Image. |
|
||||
| **Text-to-speech** | OpenAI TTS | High-quality TTS without a separate OpenAI key. Enables [voice mode](../user-guide/features/voice-mode.md) across messaging platforms. |
|
||||
| **Cloud browser automation** | Browser Use | Headless Chromium sessions for `browser_navigate`, `browser_click`, `browser_type`, `browser_vision`. No Browserbase account needed. |
|
||||
| **Cloud terminal sandbox** | Modal | Serverless terminal sandboxes for code execution (optional add-on). |
|
||||
|
||||
Without the gateway, hooking each of those up means a Firecrawl account, a FAL account, a Browser Use account, an OpenAI key, and a Modal account — five separate signups, five separate dashboards, five separate top-up flows. With the gateway, all of it routes through one subscription.
|
||||
Without the gateway, hooking each of those up means a web search account, a FAL account, a Browser Use account, an OpenAI key, and a Modal account — five separate signups, five separate dashboards, five separate top-up flows. With the gateway, all of it routes through one subscription.
|
||||
|
||||
You can also enable just specific gateway tools (e.g. web search but not image generation) — see [Mixing the gateway with your own backends](#mixing-the-gateway-with-your-own-backends) below.
|
||||
|
||||
@@ -70,7 +70,7 @@ Because everything routes through one OAuth-authenticated Portal session, you do
|
||||
|
||||
### Cross-platform parity
|
||||
|
||||
[Native Windows](../user-guide/windows-native.md) makes per-tool API key setup its rough edge — installing a Firecrawl account, a FAL account, a Browser Use account, an OpenAI key from Windows is the highest-friction part of getting a useful agent. A Portal subscription smooths that out: one OAuth covers the model and every gateway tool, so Windows users get the same experience as macOS/Linux without manually configuring four backends.
|
||||
[Native Windows](../user-guide/windows-native.md) makes per-tool API key setup its rough edge — installing a web search account, a FAL account, a Browser Use account, an OpenAI key from Windows is the highest-friction part of getting a useful agent. A Portal subscription smooths that out: one OAuth covers the model and every gateway tool, so Windows users get the same experience as macOS/Linux without manually configuring four backends.
|
||||
|
||||
## A note on Hermes 4
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ sidebar_position: 2
|
||||
|
||||
**One subscription. Every tool built in.**
|
||||
|
||||
The Tool Gateway is included with every paid [Nous Portal](https://portal.nousresearch.com) subscription. It routes Hermes' tool calls — web search, image generation, text-to-speech, and cloud browser automation — through infrastructure Nous already runs, so you don't have to sign up with Firecrawl, FAL, OpenAI, Browser Use, or anyone else just to make your agent useful.
|
||||
The Tool Gateway is included with every paid [Nous Portal](https://portal.nousresearch.com) subscription. It routes Hermes' tool calls — web search, image generation, text-to-speech, and cloud browser automation — through infrastructure Nous already runs, so you don't have to sign up with a web search vendor, FAL, OpenAI, Browser Use, or anyone else just to make your agent useful.
|
||||
|
||||
<div style={{display: 'flex', gap: '1rem', flexWrap: 'wrap', margin: '1.5rem 0'}}>
|
||||
<a href="https://portal.nousresearch.com/manage-subscription" style={{background: 'var(--ifm-color-primary)', color: 'white', padding: '0.75rem 1.5rem', borderRadius: '6px', textDecoration: 'none', fontWeight: 'bold'}}>Start or manage subscription →</a>
|
||||
@@ -19,7 +19,7 @@ The Tool Gateway is included with every paid [Nous Portal](https://portal.nousre
|
||||
|
||||
| | Tool | What you get |
|
||||
|---|---|---|
|
||||
| 🔍 | **Web search & extract** | Agent-grade web search and full-page extraction via Firecrawl. No rate limits to worry about — the gateway handles scaling. |
|
||||
| 🔍 | **Web search & extract** | Agent-grade web search and full-page extraction, managed by Nous. No rate limits to worry about — the gateway handles scaling. |
|
||||
| 🎨 | **Image generation** | Nine models under one endpoint: **FLUX 2 Klein 9B**, **FLUX 2 Pro**, **Z-Image Turbo**, **Nano Banana Pro** (Gemini 3 Pro Image), **GPT Image 1.5**, **GPT Image 2**, **Ideogram V3**, **Recraft V4 Pro**, **Qwen Image**. Pick per-generation with a flag, or let Hermes default to FLUX 2 Klein. |
|
||||
| 🔊 | **Text-to-speech** | OpenAI TTS voices wired into the `text_to_speech` tool. Drop voice notes into Telegram, generate audio for pipelines, narrate anything. |
|
||||
| 🌐 | **Cloud browser automation** | Headless Chromium sessions via Browser Use. `browser_navigate`, `browser_click`, `browser_type`, `browser_vision` — all the agent-driving primitives, no Browserbase account required. |
|
||||
@@ -31,7 +31,7 @@ All four are pay-as-you-use billed against your Nous subscription. Use any combi
|
||||
Building an agent that can actually *do things* means stitching together 5+ API subscriptions — each with their own signup, rate limits, billing, and quirks. The gateway collapses that into one account:
|
||||
|
||||
- **One bill.** Pay Nous; we handle the rest.
|
||||
- **One signup.** No Firecrawl, FAL, Browser Use, or OpenAI audio accounts to manage.
|
||||
- **One signup.** No web search, FAL, Browser Use, or OpenAI audio accounts to manage.
|
||||
- **One key.** Your Nous Portal OAuth covers every tool.
|
||||
- **Same quality.** Same backends the direct-key route uses — just fronted by us.
|
||||
|
||||
@@ -99,7 +99,7 @@ The gateway is per-tool. Turn it on for just what you want:
|
||||
|
||||
- **All tools through Nous** — easiest; one subscription, done.
|
||||
- **Gateway for web + images, bring your own TTS** — keep your ElevenLabs voice, let Nous handle the rest.
|
||||
- **Gateway only for things you don't have keys for** — "I already pay for Browserbase, but I don't want a Firecrawl account" works fine.
|
||||
- **Gateway only for things you don't have keys for** — "I already pay for Browserbase, but I don't want a web search account" works fine.
|
||||
|
||||
Switch any tool at any time via:
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ A fresh install with **no web credentials at all** gets working `web_search` and
|
||||
**Choosing free vs paid explicitly:** in `hermes tools`, Exa, Parallel, and Keenable each appear as two rows — **Free (keyless)** and **Paid (API key)**. Picking Free pins that vendor's anonymous endpoint (even if you later add a key); picking Paid pins the keyed path (a missing key then errors instead of silently downgrading to the free tier). The selection is stored as `web.provider_tier.<name>: free|paid`; leave it unset for auto (key present → paid, otherwise the keyless ring).
|
||||
|
||||
:::tip Nous Subscribers
|
||||
If you have a paid [Nous Portal](https://portal.nousresearch.com) subscription, web search and extract are available through the **[Tool Gateway](tool-gateway.md)** via managed Firecrawl — no API key needed. New installs can run `hermes setup --portal` to log in and turn on all gateway tools at once; existing installs can flip just web via `hermes tools`.
|
||||
If you have a paid [Nous Portal](https://portal.nousresearch.com) subscription, web search and extract are available through the **[Tool Gateway](tool-gateway.md)** as managed web search — no API key needed. New installs can run `hermes setup --portal` to log in and turn on all gateway tools at once; existing installs can flip just web via `hermes tools`.
|
||||
:::
|
||||
|
||||
---
|
||||
@@ -440,7 +440,8 @@ If no shared backend has **ever** been selected (no `web.backend` written by you
|
||||
| `PERPLEXITY_API_KEY` | perplexity |
|
||||
| `EXA_API_KEY` | exa |
|
||||
| `PARALLEL_API_KEY` | parallel |
|
||||
| `FIRECRAWL_API_KEY` or `FIRECRAWL_API_URL` (or the Nous Tool Gateway is ready) | firecrawl |
|
||||
| `FIRECRAWL_API_KEY` or `FIRECRAWL_API_URL` | firecrawl |
|
||||
| Nous Tool Gateway ready (Portal subscription) | managed web search via the Tool Gateway |
|
||||
| `SEARXNG_URL` | searxng |
|
||||
| `BRAVE_SEARCH_API_KEY` | brave-free |
|
||||
| `ddgs` package importable | ddgs |
|
||||
|
||||
@@ -14,7 +14,7 @@ description: "完整操作指南:订阅、配置、切换模型、启用 gatew
|
||||
- 在你正在配置的机器上有可用的浏览器(或 SSH 端口转发——参见 [OAuth over SSH](./oauth-over-ssh.md))
|
||||
- 约 5 分钟时间
|
||||
|
||||
你**不需要**:OpenAI 密钥、Anthropic 密钥、Firecrawl 账号、FAL 账号、Browser Use 账号,或任何其他按供应商分配的凭证。这正是 Portal 的意义所在。
|
||||
你**不需要**:OpenAI 密钥、Anthropic 密钥、网页搜索账号、FAL 账号、Browser Use 账号,或任何其他按供应商分配的凭证。这正是 Portal 的意义所在。
|
||||
|
||||
## 1. 获取订阅
|
||||
|
||||
@@ -91,7 +91,7 @@ hermes chat
|
||||
Hey, search the web for "Hermes Agent release notes" and summarize the top 3 hits.
|
||||
```
|
||||
|
||||
你应该看到 Hermes 调用 `web_search`(通过 gateway 由 Firecrawl 提供支持)并返回摘要。如果搜索正常执行且响应内容合理,说明配置完成——Portal 已端到端连通。
|
||||
你应该看到 Hermes 调用 `web_search`(通过 gateway)并返回摘要。如果搜索正常执行且响应内容合理,说明配置完成——Portal 已端到端连通。
|
||||
|
||||
## 5. 选择你实际需要的模型
|
||||
|
||||
@@ -254,7 +254,7 @@ hermes auth logout nous # 清除本地 refresh token
|
||||
| 不使用 Portal | 使用 Portal |
|
||||
|----------------|-------------|
|
||||
| 1 个 OpenRouter / Anthropic / OpenAI 密钥写入 `.env` | 1 个 OAuth refresh token,无需 `.env` 密钥 |
|
||||
| 1 个 Firecrawl 密钥用于网页搜索 | 网页搜索通过 gateway 路由 |
|
||||
| 1 个网页搜索密钥 | 网页搜索通过 gateway 路由 |
|
||||
| 1 个 FAL 密钥用于图像生成 | 图像生成通过 gateway 路由 |
|
||||
| 1 个 Browser Use / Browserbase 密钥用于浏览器 | 浏览器通过 gateway 路由 |
|
||||
| 1 个 OpenAI 密钥用于 TTS / 语音模式 | TTS 通过 gateway 路由 |
|
||||
|
||||
@@ -50,13 +50,13 @@ Portal 代理了来自整个生态系统的精选 agentic 模型目录——统
|
||||
|
||||
| 工具 | 合作方 | 功能说明 |
|
||||
|------|---------|--------------|
|
||||
| **网页搜索与抓取** | Firecrawl | Agent 级搜索与整页内容提取。无需 Firecrawl API 密钥,无需管理速率限制。 |
|
||||
| **网页搜索与抓取** | Nous 托管 | Agent 级搜索与整页内容提取。无需搜索 API 密钥,无需管理速率限制。 |
|
||||
| **图像生成** | FAL | 单一端点下的九个模型:FLUX 2 Klein 9B、FLUX 2 Pro、Z-Image Turbo、Nano Banana Pro(Gemini 3 Pro Image)、GPT Image 1.5、GPT Image 2、Ideogram V3、Recraft V4 Pro、Qwen Image。 |
|
||||
| **文字转语音** | OpenAI TTS | 无需独立 OpenAI 密钥的高质量 TTS。在各消息平台上启用[语音模式](../user-guide/features/voice-mode.md)。 |
|
||||
| **云端浏览器自动化** | Browser Use | 用于 `browser_navigate`、`browser_click`、`browser_type`、`browser_vision` 的无头 Chromium 会话。无需 Browserbase 账号。 |
|
||||
| **云端终端沙箱** | Modal | 用于代码执行的无服务器终端沙箱(可选附加项)。 |
|
||||
|
||||
不使用 gateway 的话,接入上述每项服务意味着:一个 Firecrawl 账号、一个 FAL 账号、一个 Browser Use 账号、一个 OpenAI 密钥、一个 Modal 账号——五次独立注册、五个独立控制台、五套独立充值流程。使用 gateway 后,所有内容通过一个订阅统一路由。
|
||||
不使用 gateway 的话,接入上述每项服务意味着:一个网页搜索账号、一个 FAL 账号、一个 Browser Use 账号、一个 OpenAI 密钥、一个 Modal 账号——五次独立注册、五个独立控制台、五套独立充值流程。使用 gateway 后,所有内容通过一个订阅统一路由。
|
||||
|
||||
你也可以只启用特定的 gateway 工具(例如只开启网页搜索,不开启图像生成)——详见下方[将 gateway 与自有后端混用](#mixing-the-gateway-with-your-own-backends)。
|
||||
|
||||
@@ -66,7 +66,7 @@ Portal 代理了来自整个生态系统的精选 agentic 模型目录——统
|
||||
|
||||
### 跨平台一致性
|
||||
|
||||
[原生 Windows](../user-guide/windows-native.md) 上,逐个配置 API 密钥是其最大痛点——在 Windows 上分别安装 Firecrawl 账号、FAL 账号、Browser Use 账号、OpenAI 密钥,是整个 agent 配置过程中摩擦最高的部分。Portal 订阅消除了这一问题:一次 OAuth 覆盖模型和所有 gateway 工具,Windows 用户无需手动配置四个后端,即可获得与 macOS/Linux 相同的体验。
|
||||
[原生 Windows](../user-guide/windows-native.md) 上,逐个配置 API 密钥是其最大痛点——在 Windows 上分别安装网页搜索账号、FAL 账号、Browser Use 账号、OpenAI 密钥,是整个 agent 配置过程中摩擦最高的部分。Portal 订阅消除了这一问题:一次 OAuth 覆盖模型和所有 gateway 工具,Windows 用户无需手动配置四个后端,即可获得与 macOS/Linux 相同的体验。
|
||||
|
||||
## 关于 Hermes 4 的说明
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Nous Tool Gateway(工具网关)"
|
||||
description: "通过 Nous 订阅统一使用网页搜索、文生图、语音合成与浏览器自动化,无需单独申请 Firecrawl、FAL、OpenAI、Browser Use 等 API Key"
|
||||
description: "通过 Nous 订阅统一使用网页搜索、文生图、语音合成与浏览器自动化,无需单独申请网页搜索、FAL、OpenAI、Browser Use 等 API Key"
|
||||
sidebar_label: "Tool Gateway"
|
||||
sidebar_position: 2
|
||||
---
|
||||
@@ -11,13 +11,13 @@ sidebar_position: 2
|
||||
Tool Gateway 包含在付费 Nous Portal 订阅中。**[管理订阅 →](https://portal.nousresearch.com/manage-subscription)**
|
||||
:::
|
||||
|
||||
**Tool Gateway** 让已付费的 [Nous Portal](https://portal.nousresearch.com) 用户通过同一份订阅,直接使用网页搜索、文生图、语音合成(TTS)与浏览器自动化,而**不必**再分别注册 Firecrawl、FAL、OpenAI、Browser Use 等服务的 API Key。
|
||||
**Tool Gateway** 让已付费的 [Nous Portal](https://portal.nousresearch.com) 用户通过同一份订阅,直接使用网页搜索、文生图、语音合成(TTS)与浏览器自动化,而**不必**再分别注册网页搜索、FAL、OpenAI、Browser Use 等服务的 API Key。
|
||||
|
||||
## 包含能力
|
||||
|
||||
| 工具 | 作用 | 若不用网关,可改用 |
|
||||
|------|------|---------------------|
|
||||
| **网页搜索与抓取** | 通过 Firecrawl 搜索并抽取页面内容 | `FIRECRAWL_API_KEY`、`EXA_API_KEY`、`PARALLEL_API_KEY`、`TAVILY_API_KEY` |
|
||||
| **网页搜索与抓取** | 由 Nous 托管的搜索与页面内容抽取 | `FIRECRAWL_API_KEY`、`EXA_API_KEY`、`PARALLEL_API_KEY`、`TAVILY_API_KEY` |
|
||||
| **文生图** | 通过 FAL 生成图像(8 个模型:FLUX 2 Klein/Pro、GPT-Image、Nano Banana Pro、Ideogram、Recraft V4 Pro、Qwen、Z-Image) | `FAL_KEY` |
|
||||
| **语音合成** | 通过 OpenAI TTS 将文字转为语音 | `VOICE_TOOLS_OPENAI_KEY`、`ELEVENLABS_API_KEY` |
|
||||
| **浏览器自动化** | 通过 Browser Use 控制云端浏览器 | `BROWSER_USE_API_KEY`、`BROWSERBASE_API_KEY` |
|
||||
@@ -49,7 +49,7 @@ Your Nous subscription includes the Tool Gateway.
|
||||
text-to-speech, and browser automation through your Nous subscription.
|
||||
No need to sign up for separate API keys — just pick the tools you want.
|
||||
|
||||
○ Web search & extract (Firecrawl) — not configured
|
||||
○ Web search & extract — not configured
|
||||
○ Image generation (FAL) — not configured
|
||||
○ Text-to-speech (OpenAI TTS) — not configured
|
||||
○ Browser automation (Browser Use) — not configured
|
||||
@@ -97,7 +97,7 @@ browser:
|
||||
|
||||
当某工具类别的选择键为 `nous` 时,运行时会把 API 调用路由到 Nous Tool Gateway,而不是使用直连 Key:
|
||||
|
||||
1. **网页工具** — `web_search` / `web_extract` 走网关的 Firecrawl 端点
|
||||
1. **网页工具** — `web_search` / `web_extract` 走网关的托管搜索端点
|
||||
2. **文生图** — `image_generate` 走网关的 FAL 端点
|
||||
3. **TTS** — `text_to_speech` 走网关的 OpenAI Audio 端点
|
||||
4. **浏览器** — `browser_navigate` 等走网关的 Browser Use 端点
|
||||
|
||||
@@ -35,7 +35,7 @@ Brave Search、DDGS 和 xAI 均为**仅搜索**——如果同时需要 `web_ext
|
||||
**按能力拆分:** 搜索和提取可分别使用不同的提供商——例如搜索使用 SearXNG(免费),提取使用 Firecrawl。详见下方[按能力配置](#per-capability-configuration)。
|
||||
|
||||
:::tip Nous 订阅用户
|
||||
如果您拥有付费 [Nous Portal](https://portal.nousresearch.com) 订阅,网页搜索和提取可通过 **[Tool Gateway](tool-gateway.md)** 使用托管的 Firecrawl——无需 API 密钥。新安装可运行 `hermes setup --portal` 登录并一次性开启所有 gateway 工具;现有安装可通过 `hermes tools` 单独开启网页功能。
|
||||
如果您拥有付费 [Nous Portal](https://portal.nousresearch.com) 订阅,网页搜索和提取可通过 **[Tool Gateway](tool-gateway.md)** 使用托管网页搜索——无需 API 密钥。新安装可运行 `hermes setup --portal` 登录并一次性开启所有 gateway 工具;现有安装可通过 `hermes tools` 单独开启网页功能。
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user