fix(anthropic): keep OAuth rotation on accepted native proxies

The anthropic.com-only refresh guard stopped token rotation on hosts the
resolver itself accepts as native Anthropic (*.claude.com, /anthropic
proxies), which already receive the Anthropic token at startup. Long
sessions there hit 401 and the recovery refresh was refused too.

Official hosts (anthropic.com, claude.com) rotate as before; any other
host rotates only when the current key is already an Anthropic
credential (sk-ant- or OAuth), so the #17829 case (custom key swapped
for ANTHROPIC_API_KEY / the OAuth token) stays blocked. Azure keeps its
static-key exclusion. Also corrects the _resolve_openrouter_runtime
docstring, which still claimed OPENAI_BASE_URL is never consulted.
This commit is contained in:
teknium1
2026-09-23 18:24:02 +00:00
committed by Teknium
parent fd1f47d8f4
commit 8aa1e450b8
3 changed files with 38 additions and 7 deletions

View File

@@ -872,16 +872,21 @@ class ClientLifecycleMixin:
def _try_refresh_anthropic_client_credentials(self) -> bool:
# Only native Anthropic rotates OAuth tokens; other anthropic_messages providers (MiniMax, Alibaba, ...)
# and Azure use static keys — a refresh would pick up the ~/.claude OAuth token and break auth.
# Any other host under provider 'anthropic' (a URL-bearing alias, #28660) holds no Anthropic
# credential either: refreshing would ship ANTHROPIC_API_KEY / the OAuth token to it. Match
# the hostname, not a substring, so ``proxy.example/anthropic.com`` stays foreign.
anthropic_base_url = getattr(self, "_anthropic_base_url", "") or ""
if (
self.api_mode != "anthropic_messages" or not hasattr(self, "_anthropic_api_key")
or self.provider != "anthropic"
or (anthropic_base_url and not base_url_host_matches(anthropic_base_url, "anthropic.com"))
or self.provider != "anthropic" or base_url_host_matches(anthropic_base_url, "azure.com")
):
return False
# Off the official hosts (a /anthropic proxy the resolver accepts, or a URL-bearing alias,
# #28660) rotate only a credential the endpoint already holds: swapping a custom key for
# ANTHROPIC_API_KEY / the OAuth token would leak it (#17829). Hostname match, not substring,
# so ``proxy.example/anthropic.com`` stays foreign.
official_host = not anthropic_base_url or any(
base_url_host_matches(anthropic_base_url, host) for host in ("anthropic.com", "claude.com"))
current_key = str(self._anthropic_api_key or "")
if not official_host and not (current_key.startswith("sk-ant-") or getattr(self, "_is_anthropic_oauth", False)):
return False
try:
from agent.anthropic_credentials import resolve_anthropic_token
new_token = resolve_anthropic_token(model=self.model)

View File

@@ -118,8 +118,9 @@ def _resolve_openrouter_runtime(
) -> Dict[str, Any]:
"""Terminal resolver: OpenRouter, or a bare/aliased ``custom`` endpoint. base_url precedence:
explicit > CUSTOM_BASE_URL > trusted ``model.base_url`` > OPENROUTER_BASE_URL > default.
OPENAI_BASE_URL is deliberately NOT consulted — config.yaml is the single source of truth for
endpoint URLs. OpenRouter contexts prefer OPENROUTER_API_KEY; custom endpoints never receive the
OPENAI_BASE_URL never picks the endpoint (config.yaml is the single source of truth for endpoint
URLs); it is read only to keep an OPENAI_API_KEY bound to another host out of the OpenRouter
fallback. OpenRouter contexts prefer OPENROUTER_API_KEY; custom endpoints never receive the
OpenRouter key and only get env keys gated on their authoritative hosts."""
rp = _rp()
model_cfg = rp._get_model_config()

View File

@@ -78,6 +78,7 @@ class TestOAuthFlagOnRefresh:
@pytest.mark.parametrize("base_url", [
"https://llmbox.bytedance.net",
"http://127.0.0.1:8080/anthropic.com", # substring spoof: the host is still foreign
"https://llmbox.bytedance.net/anthropic", # accepted proxy shape, but holds a custom key
])
def test_third_party_endpoint_skips_refresh(self, agent, base_url):
"""provider == 'anthropic' on a third-party endpoint must not refresh: the refresh
@@ -101,6 +102,30 @@ class TestOAuthFlagOnRefresh:
assert agent._anthropic_api_key == "custom-api-key"
assert agent._is_anthropic_oauth is False
@pytest.mark.parametrize("base_url", [
"https://api.claude.com",
"https://llm.corp.example/anthropic",
])
def test_accepted_native_proxy_keeps_rotating_anthropic_token(self, agent, base_url):
"""Hosts the resolver accepts as native Anthropic already hold the Anthropic token, so
blocking the refresh would strand an expiring OAuth token (401 with no recovery)."""
old, new = "sk-ant-oat01-old-token-aaaaaaaa", "sk-ant-oat01-new-token-bbbbbbbb"
agent.api_mode = "anthropic_messages"
agent.provider = "anthropic"
agent._anthropic_api_key = old
agent._anthropic_base_url = base_url
agent._anthropic_client = MagicMock()
agent._is_anthropic_oauth = True
with (
patch("agent.anthropic_credentials.resolve_anthropic_token", return_value=new),
patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()),
):
result = agent._try_refresh_anthropic_client_credentials()
assert result is True
assert agent._anthropic_api_key == new
class TestOAuthFlagOnCredentialSwap: