fix(web): rescue eligibility asks the provider whether the ring was walked

Follow-up to the cherry-picked gateway fix: instead of re-inferring "keyless
mode" from the key env var (wrong for Firecrawl, whose managed-gateway and
self-hosted routes bypass the ring without a key), `_rescue_eligible` asks the
ring vendor's own predicate — `_use_keyless_ring()` for Firecrawl, `use_keyless`
for the others. That covers the persisted `nous` selection the contributor fix
handled AND the legacy never-configured fallback onto a ready gateway, plus
`FIRECRAWL_API_URL`. A ring vendor that actually walked the ring stays
ineligible (its failure means the ring already failed). Docs mention the
gateway route is rescued.
This commit is contained in:
teknium1
2026-09-15 12:01:34 -07:00
committed by Teknium
parent 911eea567f
commit 80f76edfaf
3 changed files with 32 additions and 15 deletions

View File

@@ -91,14 +91,22 @@ class TestEligibility:
assert web_tools_rescue._rescue_eligible(KeenableWebSearchProvider()) is False
def test_gateway_selected_ring_vendor_is_eligible_without_direct_key(self, monkeypatch):
# The persisted Nous route uses its subscriber token, not the keyless ring.
# The persisted Nous route uses its subscriber token, not the keyless ring — eligible.
# The same keyless Firecrawl selected directly DID walk the ring — not eligible.
monkeypatch.setattr(
"agent.web_search_provider.get_provider_env", lambda name: ""
)
monkeypatch.setattr("plugins.web.firecrawl.provider._env", lambda name: "")
monkeypatch.setattr("plugins.web.firecrawl.provider._is_tool_gateway_ready", lambda: True)
monkeypatch.setattr(
"tools.tool_backend_helpers.read_selection", lambda kind: "nous"
)
assert web_tools_rescue._rescue_eligible(_GatewayFirecrawlBoomProvider()) is True
monkeypatch.setattr(
"tools.tool_backend_helpers.read_selection", lambda kind: "firecrawl"
)
monkeypatch.setattr("plugins.web.keyless_mcp._web_config_selects", lambda name: name == "firecrawl")
assert web_tools_rescue._rescue_eligible(_GatewayFirecrawlBoomProvider()) is False
def test_non_ring_backend_is_eligible(self):
class _SearxProvider(_KeyedBoomProvider):

View File

@@ -30,27 +30,36 @@ def _keyless_rescue_enabled() -> bool:
return False
def _ring_vendor_keyless(name: str) -> bool:
"""Did *name*'s own provider route this call through the anonymous keyless ring?
Mirrors the predicate each ring provider evaluates before calling, so eligibility reflects what
actually happened. Firecrawl owns extra routes that bypass the ring without a key — the managed
Nous Tool Gateway (persisted ``nous`` selection, or the legacy never-configured fallback when the
gateway is ready) and a self-hosted ``FIRECRAWL_API_URL`` — so it is asked directly.
"""
if name == "firecrawl":
from plugins.web.firecrawl.provider import _use_keyless_ring
return _use_keyless_ring()
from agent.web_search_provider import get_provider_env
from plugins.web.keyless_mcp import use_keyless
key_var = _RING_KEY_VARS.get(name, "")
return use_keyless(name, get_provider_env(key_var) if key_var else "")
def _rescue_eligible(provider) -> bool:
"""True when a failed call on *provider* should get a one-shot rescue.
Eligible: a keyed/configured path — any non-ring backend, a ring vendor in keyed mode, or a ring
vendor selected through the persisted Nous Tool Gateway route. A ring vendor selected directly in
keyless mode is NOT eligible: its failure means the ring was already walked.
Eligible: any call that did NOT go through the keyless ring — a non-ring backend, a ring vendor
in keyed mode, or a ring vendor routed through the managed gateway / a self-hosted instance. A
ring vendor that walked the ring is NOT eligible: its failure means the ring already failed.
"""
if not _keyless_rescue_enabled() or provider is None:
return False
try:
from plugins.web.keyless_mcp import _KEYLESS_RING, use_keyless
from plugins.web.keyless_mcp import _KEYLESS_RING
name = getattr(provider, "name", "")
if name not in _KEYLESS_RING:
return True
from tools.tool_backend_helpers import NOUS_MANAGED_PROVIDER, read_selection
if read_selection("web") == NOUS_MANAGED_PROVIDER:
# The gateway uses the subscriber token, so this call has not walked the anonymous ring.
return True
from agent.web_search_provider import get_provider_env
key_var = _RING_KEY_VARS.get(name, "")
return not use_keyless(name, get_provider_env(key_var) if key_var else "")
return name not in _KEYLESS_RING or not _ring_vendor_keyless(name)
except Exception as exc: # noqa: BLE001 — rescue is best-effort
logger.debug("rescue eligibility check failed: %s", exc)
return False

View File

@@ -425,7 +425,7 @@ If no backend has **ever** been selected (no `web.backend` / per-capability key
**Keyless free-tier ring:** when *no* credential above is present, requests rotate across the ring vendors' public free tiers (Exa, Parallel, Firecrawl, Keenable) so web tools work on a fresh install with zero setup — and a rate-limited request fails over to the next vendor in the ring automatically. Pin one vendor in `hermes tools` to stop the rotation (the ring is then only used as failover succession on throttles). All free tiers are vendor-rate-limited under burst load; sustained normal usage goes through fine. Set `web.keyless_fallback: false` to turn the tier off — with it off and no credentials, web tools are unavailable until a provider is configured.
**One-shot keyless rescue for keyed backends:** when your chosen/keyed backend fails a call (bad key, outage, upstream 5xx), that single call automatically retries on the keyless free-tier ring instead of erroring — the result notes which vendor served it and why (`rescued_from` / `backend_error`). The failover is never sticky: the very next `web_search`/`web_extract` call attempts your chosen backend again. Disable with `web.keyless_rescue: false` (also off whenever `keyless_fallback` is off).
**One-shot keyless rescue for keyed backends:** when your chosen/keyed backend — including the Nous Tool Gateway route (`web.backend: nous`) — fails a call (bad key, outage, unreachable gateway, upstream 5xx), that single call automatically retries on the keyless free-tier ring instead of erroring — the result notes which vendor served it and why (`rescued_from` / `backend_error`). The failover is never sticky: the very next `web_search`/`web_extract` call attempts your chosen backend again. Disable with `web.keyless_rescue: false` (also off whenever `keyless_fallback` is off).
xAI Web Search is **not** in the auto-detection chain — having `XAI_API_KEY` set (or being signed in via xAI Grok OAuth) does not automatically route web traffic through xAI, since those credentials are also used for inference / TTS / image gen and the user may want a different backend for web. Opt in explicitly with `web.backend: "xai"`.