From 33f6e4598e2dacc8fabcaebe0e0e0ab72e9bceb7 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 3 Jun 2026 15:29:20 -0700 Subject: [PATCH] [eric] web: subscription-OAuth WebSearch 401s on token rotation, so keep the reliable DuckDuckGo fallback for those users --- backend/apps/agents/agent_manager.py | 26 ++++++++----- backend/apps/agents/tools/web.py | 18 +++++++++ backend/tests/test_web_search_reliability.py | 39 ++++++++++++++++++++ 3 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 backend/tests/test_web_search_reliability.py diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index fe4cd393..9cdf2778 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -1213,18 +1213,22 @@ class AgentManager: getattr(global_settings, "connection_mode", "own_key") == "openswarm-pro" or bool(getattr(global_settings, "anthropic_api_key", None)) ) - # Both 9Router provider ids `claude` (subscription OAuth) and - # `anthropic` (direct API / Pro proxy) satisfy this check. - _9r_has_anthropic = False + # Collect the active 9Router anthropic-family provider ids so the + # web-search reliability check (below) can distinguish a STABLE + # credential (direct `anthropic`) from subscription OAuth + # (`claude`/`claude-code`), whose hosted-WebSearch delegation 401s on + # token rotation. Subscription OAuth must NOT suppress the DDG fallback. + _9r_provider_ids: list[str] = [] try: from backend.apps.nine_router import get_providers as _9r_providers _conns = await _9r_providers() - _9r_has_anthropic = any( - isinstance(c, dict) + _9r_provider_ids = [ + c.get("provider") + for c in _conns + if isinstance(c, dict) and c.get("provider") in ("claude", "claude-code", "anthropic") and c.get("isActive") - for c in _conns - ) + ] except Exception: pass @@ -1258,12 +1262,14 @@ class AgentManager: # Post-fix: non-Claude primaries always register openswarm-web, # which cascades Gemini-native → OpenAI-native → subscriptions # → DDG, only falling to Anthropic if everything else missing. + from backend.apps.agents.tools.web import anthropic_web_search_is_reliable _has_anthropic_path = ( not _is_custom_session and _primary_is_claude - and ( - bool(getattr(global_settings, "anthropic_api_key", None)) - or _9r_has_anthropic + and anthropic_web_search_is_reliable( + has_direct_anthropic_key=bool(getattr(global_settings, "anthropic_api_key", None)), + is_pro=(getattr(global_settings, "connection_mode", "own_key") == "openswarm-pro"), + provider_ids=_9r_provider_ids, ) ) diff --git a/backend/apps/agents/tools/web.py b/backend/apps/agents/tools/web.py index 371bbe23..f50755a4 100644 --- a/backend/apps/agents/tools/web.py +++ b/backend/apps/agents/tools/web.py @@ -18,6 +18,24 @@ _USER_AGENT = ( "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" ) +# 9router provider ids that authenticate with a STABLE token. Subscription OAuth +# ('claude'/'claude-code') is deliberately excluded: the CLI's built-in WebSearch +# delegates to Haiku via the subscription's rotating OAuth token, which 401s +# intermittently ("Invalid bearer token, reset after 2m"). Only stable creds are +# reliable enough to suppress our free DuckDuckGo fallback. +_STABLE_ANTHROPIC_PROVIDERS = ("anthropic",) + + +def anthropic_web_search_is_reliable(*, has_direct_anthropic_key: bool, + is_pro: bool, provider_ids) -> bool: + """Whether the Anthropic-hosted WebSearch path is reliable enough to suppress + the DuckDuckGo fallback. A subscription-OAuth-only user is NOT reliable (its + web-search delegation 401s on token rotation), so those users keep the free, + always-working DDG path instead of a flaky hosted one.""" + if has_direct_anthropic_key or is_pro: + return True + return any(p in _STABLE_ANTHROPIC_PROVIDERS for p in (provider_ids or [])) + def _truncate(text: str, limit: int = _MAX_OUTPUT_BYTES) -> str: if len(text) > limit: diff --git a/backend/tests/test_web_search_reliability.py b/backend/tests/test_web_search_reliability.py new file mode 100644 index 00000000..08e4b698 --- /dev/null +++ b/backend/tests/test_web_search_reliability.py @@ -0,0 +1,39 @@ +"""WebSearch path reliability: subscription OAuth must fall back to DuckDuckGo. + +The 401 'Invalid bearer token (reset after 2m)' the user hit comes from the CLI's +built-in WebSearch delegating to Haiku via a subscription's rotating OAuth token. +So a subscription-OAuth-only user must NOT be treated as having a reliable hosted +search path, they keep the free, always-working DDG fallback instead. +""" + +from backend.apps.agents.tools.web import anthropic_web_search_is_reliable as ok + + +def test_direct_api_key_is_reliable(): + assert ok(has_direct_anthropic_key=True, is_pro=False, provider_ids=[]) is True + + +def test_pro_is_reliable(): + assert ok(has_direct_anthropic_key=False, is_pro=True, provider_ids=[]) is True + + +def test_direct_anthropic_9router_provider_is_reliable(): + assert ok(has_direct_anthropic_key=False, is_pro=False, provider_ids=["anthropic"]) is True + + +def test_subscription_oauth_only_is_NOT_reliable(): + # the bug: this used to count as reliable -> suppressed DDG -> 401s on rotation + assert ok(has_direct_anthropic_key=False, is_pro=False, provider_ids=["claude"]) is False + assert ok(has_direct_anthropic_key=False, is_pro=False, provider_ids=["claude-code"]) is False + assert ok(has_direct_anthropic_key=False, is_pro=False, provider_ids=["claude", "claude-code"]) is False + + +def test_nothing_is_not_reliable(): + assert ok(has_direct_anthropic_key=False, is_pro=False, provider_ids=[]) is False + assert ok(has_direct_anthropic_key=False, is_pro=False, provider_ids=None) is False + + +def test_mixed_subscription_plus_direct_is_reliable(): + # if the user ALSO has a stable direct anthropic connection, hosted search is fine + assert ok(has_direct_anthropic_key=False, is_pro=False, + provider_ids=["claude", "anthropic"]) is True