[eric] web: subscription-OAuth WebSearch 401s on token rotation, so keep the reliable DuckDuckGo fallback for those users

This commit is contained in:
ciregenz
2026-06-03 15:29:20 -07:00
parent 3d0dcca22f
commit 33f6e4598e
3 changed files with 73 additions and 10 deletions
+16 -10
View File
@@ -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,
)
)
+18
View File
@@ -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:
@@ -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