fix(web): per-capability backend key no longer reroutes the other capability

Fixes #113017. _get_backend() treated any web selection — including
per-capability keys (web.search_backend / web.extract_backend) — as a stored
shared selection and short-circuited to firecrawl with no ladder. Setting only
web.extract_backend therefore silently rerouted web_search off autodetect.
The no-ladder firecrawl branch now requires a shared selection
(read_selection: use_gateway); per-capability keys name only their own
capability, and the other keeps its normal cascade.

Test: new tests/tools/test_web_backend_capability_isolation.py — red on base
(search resolved to firecrawl), green after fix; managed use_gateway still
resolves firecrawl with no ladder.
This commit is contained in:
Kevin Rajan
2026-09-16 11:29:13 -05:00
committed by Teknium
parent 5457c48df2
commit 010a45097e
2 changed files with 52 additions and 4 deletions

View File

@@ -0,0 +1,46 @@
"""A per-capability web backend key must not reroute the other capability.
Regression for #113017: ``web.extract_backend: keenable`` (with no
``web.backend``) made ``_get_search_backend()`` resolve to ``firecrawl``
through the shared ``_get_backend()`` fallback, because
``selection_exists("web")`` counts per-capability keys as a stored selection.
"""
from __future__ import annotations
def _ladder_env(monkeypatch, raw_web):
from tools import web_tools
from tools import tool_backend_helpers as helpers
monkeypatch.setattr(web_tools, "_load_web_config", lambda: dict(raw_web))
monkeypatch.setattr(
helpers, "_raw_section",
lambda section: dict(raw_web) if section == "web" else None,
)
# No keyed backends, no gateway: the ladder can only reach ddgs.
for var in (
"TAVILY_API_KEY", "PERPLEXITY_API_KEY", "EXA_API_KEY",
"PARALLEL_API_KEY", "KEENABLE_API_KEY",
"FIRECRAWL_API_KEY", "FIRECRAWL_API_URL",
"BRAVE_SEARCH_API_KEY", "SEARXNG_URL",
):
monkeypatch.delenv(var, raising=False)
monkeypatch.setattr(web_tools, "_ddgs_package_importable", lambda: True)
monkeypatch.setattr(web_tools, "_is_tool_gateway_ready", lambda: False)
def test_extract_only_config_does_not_reroute_search(monkeypatch):
from tools import web_tools
_ladder_env(monkeypatch, {"extract_backend": "keenable"})
# Broken before the fix: search resolved to "firecrawl" through the shared fallback.
assert web_tools._get_search_backend() == "ddgs"
assert web_tools._get_extract_backend() == "keenable"
def test_managed_use_gateway_still_resolves_firecrawl(monkeypatch):
from tools import web_tools
_ladder_env(monkeypatch, {"use_gateway": True})
# Ladder would find ddgs if it ran — the managed selection must not reach it.
assert web_tools._get_backend() == "firecrawl"

View File

@@ -18,7 +18,7 @@ _firecrawl_client = _firecrawl_client_config = _parallel_client = _async_paralle
from plugins.web.firecrawl.provider import _is_tool_gateway_ready, check_firecrawl_api_key
from tools.debug_helpers import DebugSession
from tools.tool_backend_helpers import NOUS_MANAGED_PROVIDER, selection_exists
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_truncate import _effective_char_limit, _trim_results, _truncate_results, convert_base64_images_to_links
@@ -100,13 +100,15 @@ def _probe(provider, method: str, context: str = "") -> Optional[bool]:
def _get_backend() -> str:
"""Shared web backend name. A stored ``web.backend`` is returned as-is — no availability probe, no
fallback — so a broken selection surfaces the vendor's honest error rather than silently rerouting.
Autodetect runs ONLY when no web selection has ever been stored."""
The managed ``use_gateway`` selection also resolves to firecrawl with no ladder. Autodetect runs
whenever no SHARED web selection was ever stored: per-capability keys (``web.search_backend``,
``web.extract_backend``) name only their own capability and never reroute the other (#113017)."""
configured = _configured_backend()
if configured:
# "nous" (managed subscription) is serviced by firecrawl, routed through the managed Tool Gateway.
return "firecrawl" if configured == NOUS_MANAGED_PROVIDER else configured
if selection_exists("web"):
# Selection exists (use_gateway / per-capability keys) but no shared name: firecrawl, no ladder.
if read_selection("web") is not None:
# Shared selection exists (use_gateway) but no shared name: firecrawl, no ladder.
return "firecrawl"
# Never-configured install. Explicit user credentials beat the managed-gateway probe (a Nous OAuth