mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
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:
parent
95f2919f91
commit
e7d4ade8cf
2 changed files with 105 additions and 0 deletions
|
|
@ -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 —
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue