From 80f76edfaff785544e36f7dd35c6299479a0d292 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:01:34 -0700 Subject: [PATCH] fix(web): rescue eligibility asks the provider whether the ring was walked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- tests/tools/test_web_keyless_rescue.py | 10 +++++- tools/web_tools_rescue.py | 35 ++++++++++++------- .../docs/user-guide/features/web-search.md | 2 +- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/tests/tools/test_web_keyless_rescue.py b/tests/tools/test_web_keyless_rescue.py index f624c93826..3901703434 100644 --- a/tests/tools/test_web_keyless_rescue.py +++ b/tests/tools/test_web_keyless_rescue.py @@ -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): diff --git a/tools/web_tools_rescue.py b/tools/web_tools_rescue.py index 43e2682f75..1e9a65efaa 100644 --- a/tools/web_tools_rescue.py +++ b/tools/web_tools_rescue.py @@ -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 diff --git a/website/docs/user-guide/features/web-search.md b/website/docs/user-guide/features/web-search.md index 582496d98c..b6d1449fdc 100644 --- a/website/docs/user-guide/features/web-search.md +++ b/website/docs/user-guide/features/web-search.md @@ -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"`.