feat: web search/extract now work keyless on fresh installs via Parallel + Exa free tiers
With zero web credentials configured, web_search/web_extract previously resolved to the nonfunctional firecrawl sentinel and errored. Now the backend resolution walks a strictly-last keyless tier: Parallel's and Exa's public anonymous MCP endpoints (the same free tiers opencode ships as its default search path). - plugins/web/keyless_mcp.py: minimal JSON-RPC tools/call client for mcp.exa.ai + search.parallel.ai (SSE + plain JSON parsing, typed errors, per-process random session id, no user identifiers) - WebSearchProvider.is_keyless_available(): separate weaker tier that never leaks into is_available(), so keyed setups are never pre-empted - Exa/Parallel providers: route to keyless endpoints when their key is absent; keyed SDK path unchanged - registry + _get_backend(): keyless walk (parallel -> exa) strictly after every keyed/importable candidate; check_web_api_key() lights the tools up on zero-credential installs - web.keyless_fallback config key (default true) to disable the tier - docs: web-search.md + configuration.md E2E-verified against both live endpoints from an isolated HERMES_HOME (search + extract via the real dispatchers, disable-flag negative path).
This commit is contained in:
+33
-1
@@ -267,6 +267,32 @@ def _get_backend() -> str:
|
||||
except Exception as exc: # noqa: BLE001 — a broken provider is skipped
|
||||
logger.debug("web provider %r.is_available() raised: %s", provider.name, exc)
|
||||
|
||||
# Keyless free-tier walk — zero credentials anywhere. Providers with a
|
||||
# public anonymous endpoint (Parallel, Exa — see
|
||||
# plugins/web/keyless_mcp.py) can still serve, unless the user disabled
|
||||
# the tier via ``web.keyless_fallback: false``. Strictly last so it
|
||||
# never pre-empts any keyed/importable backend above. Discovery must
|
||||
# run first — this path is reachable from contexts that haven't loaded
|
||||
# plugins yet (subprocess agent runs, delegate children, scripts).
|
||||
try:
|
||||
_ensure_web_plugins_loaded()
|
||||
from agent.web_search_registry import _KEYLESS_PREFERENCE, _keyless_tier_enabled
|
||||
|
||||
if _keyless_tier_enabled():
|
||||
for name in _KEYLESS_PREFERENCE:
|
||||
provider = _registered_web_provider(name)
|
||||
if provider is None:
|
||||
continue
|
||||
try:
|
||||
if provider.is_keyless_available():
|
||||
return name
|
||||
except Exception as exc: # noqa: BLE001 — skip broken provider
|
||||
logger.debug(
|
||||
"web provider %r.is_keyless_available() raised: %s", name, exc
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 — registry optional; never fatal
|
||||
logger.debug("keyless fallback walk failed: %s", exc)
|
||||
|
||||
return "firecrawl" # default (backward compat)
|
||||
|
||||
|
||||
@@ -1075,8 +1101,14 @@ def check_web_api_key() -> bool:
|
||||
# Any plugin-registered provider the registry considers active for either
|
||||
# capability. Delegating to the registry's own availability-filtered
|
||||
# resolvers keeps a single authority for "is a custom provider usable"
|
||||
# rather than re-implementing the walk here.
|
||||
# rather than re-implementing the walk here. This also covers the
|
||||
# keyless free tier (Parallel/Exa anonymous MCP endpoints): the registry
|
||||
# walk falls back to keyless-capable providers when nothing is keyed,
|
||||
# so a zero-credential install still lights the web tools up. Discovery
|
||||
# must run first — check_fn fires at tool-registration time, before any
|
||||
# dispatch has populated the registry.
|
||||
try:
|
||||
_ensure_web_plugins_loaded()
|
||||
from agent.web_search_registry import (
|
||||
get_active_search_provider,
|
||||
get_active_extract_provider,
|
||||
|
||||
Reference in New Issue
Block a user