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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user