fix: web gate follows the dispatcher when the web-xai plugin resolves xai (review follow-up)

With the bundled web-xai plugin enabled (default), the registry's single-eligible
walk returned the xai provider and lit check_web_api_key while _get_backend never
autodetects xai, so web_search/web_extract registered and failed exactly as
#116175 describes. The gate now treats such a pick the way the dispatcher does:
servable only if the keyless tier would be; the keyless walk is shared via
_keyless_backend().
This commit is contained in:
teknium1
2026-09-20 01:13:25 -07:00
committed by Teknium
parent 5e131ff9cc
commit f04a23165a
2 changed files with 51 additions and 7 deletions

View File

@@ -916,3 +916,35 @@ class TestSiblingProvidersEnvResolution:
from agent.web_search_provider import get_provider_env
assert get_provider_env("WSP_TEST_UNSET_KEY") == ""
def test_xai_only_gate_agrees_with_dispatcher_when_web_xai_plugin_loaded(monkeypatch, tmp_path):
"""With the bundled web-xai plugin registered (the default), the registry resolves xai
as the single eligible search provider while _get_backend never autodetects it. The
gate must follow the dispatcher: keyless off -> no servable backend -> tools stay off
(#116175 review follow-up). Module-level on purpose: TestCheckWebApiKey neutralizes the
registry path with get_active_*_provider -> None."""
from agent import web_search_registry as registry
from plugins.web.xai.provider import XAIWebSearchProvider
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
monkeypatch.setenv("XAI_API_KEY", "xai-test-key")
for k in ("PERPLEXITY_API_KEY", "SEARXNG_URL", "BRAVE_SEARCH_API_KEY", "TAVILY_API_KEY", "EXA_API_KEY"):
monkeypatch.delenv(k, raising=False)
with registry._lock:
saved = dict(registry._providers)
registry._providers.clear()
registry.register_provider(XAIWebSearchProvider())
try:
with patch("tools.web_tools._load_web_config", return_value={}), \
patch("tools.web_tools._ensure_web_plugins_loaded", lambda: None), \
patch("tools.web_tools.check_firecrawl_api_key", return_value=False), \
patch("agent.web_search_registry._keyless_tier_enabled", return_value=False):
from tools.web_tools import _get_backend, check_web_api_key
assert registry.get_active_search_provider().name == "xai"
assert _get_backend() == "firecrawl" # legacy sentinel: nothing servable
assert check_web_api_key() is False
finally:
with registry._lock:
registry._providers.clear()
registry._providers.update(saved)

View File

@@ -131,8 +131,13 @@ def _get_backend() -> str:
if provider.name not in _LEGACY_WEB_BACKENDS and _probe(provider, "is_available"):
return provider.name
# Keyless free tier — strictly last so it never pre-empts a keyed backend. Discovery must run
# first: reachable from contexts that haven't loaded plugins (subprocess runs, delegate children).
return _keyless_backend() or "firecrawl" # default (backward compat)
def _keyless_backend() -> Optional[str]:
"""Keyless free-tier backend name, or None. Strictly the last autodetect rung so it never
pre-empts a keyed backend. Discovery must run first: reachable from contexts that haven't
loaded plugins (subprocess runs, delegate children)."""
try:
_ensure_web_plugins_loaded()
from agent.web_search_registry import _keyless_preference, _keyless_tier_enabled
@@ -143,8 +148,7 @@ def _get_backend() -> str:
return name
except Exception as exc: # noqa: BLE001 — registry optional; never fatal
logger.debug("keyless fallback walk failed: %s", exc)
return "firecrawl" # default (backward compat)
return None
def _get_search_backend() -> str:
@@ -448,9 +452,17 @@ def check_web_api_key() -> bool:
try:
_ensure_web_plugins_loaded()
from agent.web_search_registry import get_active_search_provider, get_active_extract_provider
return _provider_is_ready(get_active_search_provider()) or _provider_is_ready(
get_active_extract_provider()
)
for provider in (get_active_search_provider(), get_active_extract_provider()):
if provider is not None and getattr(provider, "name", None) in _WEB_CHECK_SKIP:
# The registry's single-eligible / legacy walk picked a built-in that _get_backend
# never autodetects (the explicit-config case was handled above): the dispatcher
# would route to the keyless tier instead, so gate on exactly that.
if _keyless_backend() is not None:
return True
continue
if _provider_is_ready(provider):
return True
return False
except Exception as exc: # noqa: BLE001 — registry optional; never fatal
logger.debug("web provider registry availability check failed: %s", exc)
return False