fix(anthropic): ignore stale non-Anthropic base_url across all resolution paths

A config left with `provider: anthropic` but a leftover
`base_url: https://openrouter.ai/api/v1` (e.g. after a provider switch)
would route Anthropic OAuth/setup-token traffic to OpenRouter and 404.

Add `_anthropic_base_url_override_ok()` and gate the three native-Anthropic
resolution branches (pool, explicit, native) on it. The guard honors a
configured `model.base_url` only when it plausibly speaks the Anthropic
Messages protocol — official `*.anthropic.com` / `*.claude.com` hosts, Azure
Foundry endpoints, and `/anthropic`-suffixed or Kimi `/coding` proxies — and
falls back to `https://api.anthropic.com` otherwise. Aggregator URLs like
openrouter.ai / api.openai.com are treated as stale.

Reconstructed from @clovericbot's PR #3661 onto current main: the original
patched one branch with an anthropic-only allow-list, which would have broken
Azure-via-anthropic; widened to all three sites and made Azure/proxy-safe.
This commit is contained in:
奥森木 2026-06-28 14:12:06 -07:00 committed by Teknium
parent 95f2919f91
commit e7d4ade8cf
2 changed files with 105 additions and 0 deletions

View file

@ -172,6 +172,43 @@ def _host_derived_api_key(base_url: str) -> str:
return (_getenv(env_name, "") or "").strip()
def _anthropic_base_url_override_ok(base_url: str) -> bool:
"""Decide whether a configured ``model.base_url`` may back native Anthropic.
Native ``provider: anthropic`` resolution honors ``model.base_url`` so users
can point at Anthropic-compatible endpoints (official Anthropic/Claude hosts,
Azure Foundry, MiniMax/Zhipu/LiteLLM-style ``/anthropic`` proxies, Kimi's
``/coding`` route). But a config can carry a *stale* non-Anthropic URL e.g.
``provider: anthropic`` left with ``base_url: https://openrouter.ai/api/v1``
after a provider switch which would route Anthropic OAuth/setup-token
traffic to an OpenAI-compatible aggregator and 404. Ignore those.
Returns True only when the URL plausibly speaks the Anthropic Messages
protocol; otherwise the caller falls back to ``https://api.anthropic.com``.
"""
candidate = (base_url or "").strip()
if not candidate:
return False
hostname = (base_url_hostname(candidate) or "").lower()
if not hostname:
return False
# Official Anthropic / Claude hosts.
if hostname == "api.anthropic.com" or hostname.endswith(".anthropic.com") or hostname.endswith(".claude.com"):
return True
# Azure Foundry Anthropic endpoints (handled specially downstream).
if hostname.endswith(".azure.com"):
return True
# Anthropic-compatible proxies conventionally expose the native Messages
# protocol under a ``/anthropic`` suffix, and Kimi under ``/coding`` — same
# signal _detect_api_mode_for_url() uses to pick anthropic_messages.
if _detect_api_mode_for_url(candidate) == "anthropic_messages":
return True
# Bare api.kimi.com without the /coding path is not an Anthropic endpoint.
return False
def _auto_detect_local_model(base_url: str) -> str:
"""Query a local server for its model name when only one model is loaded."""
if not base_url:
@ -344,6 +381,8 @@ def _resolve_runtime_from_pool_entry(
cfg_base_url = ""
if cfg_provider == "anthropic":
cfg_base_url = str(model_cfg.get("base_url") or "").strip().rstrip("/")
if not _anthropic_base_url_override_ok(cfg_base_url):
cfg_base_url = ""
base_url = cfg_base_url or base_url or "https://api.anthropic.com"
elif provider == "openrouter":
base_url = base_url or OPENROUTER_BASE_URL
@ -1247,6 +1286,8 @@ def _resolve_explicit_runtime(
cfg_base_url = ""
if cfg_provider == "anthropic":
cfg_base_url = str(model_cfg.get("base_url") or "").strip().rstrip("/")
if not _anthropic_base_url_override_ok(cfg_base_url):
cfg_base_url = ""
base_url = explicit_base_url or cfg_base_url or "https://api.anthropic.com"
api_key = explicit_api_key
if not api_key:
@ -1697,6 +1738,8 @@ def resolve_runtime_provider(
cfg_base_url = ""
if cfg_provider == "anthropic":
cfg_base_url = (model_cfg.get("base_url") or "").strip().rstrip("/")
if not _anthropic_base_url_override_ok(cfg_base_url):
cfg_base_url = ""
base_url = cfg_base_url or "https://api.anthropic.com"
# For Microsoft Foundry endpoints, use ANTHROPIC_API_KEY directly —

View file

@ -76,6 +76,68 @@ def test_resolve_runtime_provider_anthropic_pool_respects_config_base_url(monkey
assert resolved["base_url"] == "https://proxy.example.com/anthropic"
def test_resolve_runtime_provider_anthropic_ignores_stale_aggregator_base_url(monkeypatch):
"""A leftover OpenRouter base_url under provider: anthropic must not hijack
Anthropic OAuth traffic fall back to the official Anthropic host."""
class _Entry:
access_token = "pool-token"
source = "manual"
base_url = "https://api.anthropic.com"
class _Pool:
def has_credentials(self):
return True
def select(self):
return _Entry()
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "anthropic")
monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool())
for stale in (
"https://openrouter.ai/api/v1",
"https://api.openai.com/v1",
):
monkeypatch.setattr(
rp,
"_get_model_config",
lambda stale=stale: {"provider": "anthropic", "base_url": stale},
)
resolved = rp.resolve_runtime_provider(requested="anthropic")
assert resolved["provider"] == "anthropic"
assert resolved["api_mode"] == "anthropic_messages"
assert resolved["base_url"] == "https://api.anthropic.com", stale
def test_resolve_runtime_provider_anthropic_keeps_azure_base_url(monkeypatch):
"""Azure Foundry Anthropic endpoints are not anthropic.com hosts but are a
legitimate override they must survive the stale-URL guard."""
class _Entry:
access_token = "pool-token"
source = "manual"
base_url = "https://api.anthropic.com"
class _Pool:
def has_credentials(self):
return True
def select(self):
return _Entry()
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "anthropic")
monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool())
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {"provider": "anthropic", "base_url": "https://myhost.azure.com/anthropic"},
)
resolved = rp.resolve_runtime_provider(requested="anthropic")
assert resolved["base_url"] == "https://myhost.azure.com/anthropic"
def test_resolve_runtime_provider_anthropic_explicit_override_skips_pool(monkeypatch):
def _unexpected_pool(provider):
raise AssertionError(f"load_pool should not be called for {provider}")