Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions hermes_cli/runtime_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -1866,9 +1866,9 @@ def resolve_runtime_provider(

# Anthropic (native Messages API)
if provider == "anthropic":
# Allow base URL override from config.yaml model.base_url, but only
# when the configured provider is anthropic β€” otherwise a non-Anthropic
# base_url (e.g. Codex endpoint) would leak into Anthropic requests.
# Allow base URL / api_key overrides from config.yaml model.* β€” but
# only when the configured provider is anthropic, otherwise a
# non-Anthropic base_url or key would leak into Anthropic requests.
cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
cfg_base_url = ""
if cfg_provider == "anthropic":
Expand All @@ -1894,6 +1894,7 @@ def resolve_runtime_provider(
# Azure Foundry guide and read by most Hermes-compatible importers).
# Matches the config.yaml examples in website/docs/guides/azure-foundry.md.
token = ""
source = "env"
for hint_key in ("key_env", "api_key_env"):
env_var = str(model_cfg.get(hint_key) or "").strip()
if env_var:
Expand All @@ -1904,6 +1905,8 @@ def resolve_runtime_provider(
# setups that want to avoid env-var juggling).
if not token:
token = str(model_cfg.get("api_key") or "").strip()
if token:
source = "config"
# Finally fall back to the historical fixed names.
if not token:
token = (
Expand All @@ -1917,8 +1920,18 @@ def resolve_runtime_provider(
"config.yaml model section at a custom env var."
)
else:
from agent.anthropic_adapter import resolve_anthropic_token
token = resolve_anthropic_token()
cfg_api_key = (
str(model_cfg.get("api_key") or "").strip()
if cfg_provider == "anthropic"
else ""
)
if cfg_api_key:
token = cfg_api_key
source = "config"
else:
from agent.anthropic_adapter import resolve_anthropic_token
token = resolve_anthropic_token()
source = "env"
if not token:
raise AuthError(
"No Anthropic credentials found. Set ANTHROPIC_TOKEN or ANTHROPIC_API_KEY, "
Expand All @@ -1929,7 +1942,7 @@ def resolve_runtime_provider(
"api_mode": "anthropic_messages",
"base_url": base_url,
"api_key": token,
"source": "env",
"source": source,
"requested_provider": requested_provider,
}

Expand Down
42 changes: 42 additions & 0 deletions tests/hermes_cli/test_runtime_provider_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -3416,3 +3416,45 @@ def test_resolve_named_custom_runtime_pool_result_includes_extra_headers(monkeyp
}
assert resolved["api_key"] == "pooled-key"
assert resolved["source"] == "pool:lmstudio-pool"


# --- Anthropic config.yaml api_key tests (#9105) ---

def _patch_anthropic_short_circuit(monkeypatch):
"""Bypass everything before the provider-specific anthropic branch."""
monkeypatch.setattr(rp, "resolve_requested_provider", lambda *a, **k: "anthropic")
monkeypatch.setattr(rp, "_resolve_named_custom_runtime", lambda **k: None)
monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "anthropic")
monkeypatch.setattr(rp, "_resolve_explicit_runtime", lambda **k: None)
# Force the pool path to yield nothing so we fall through to the
# anthropic-specific code block.
monkeypatch.setattr(rp, "load_pool", lambda *a: (_ for _ in ()).throw(Exception("no pool")))


def test_anthropic_config_api_key_is_used(monkeypatch):
"""Non-Azure branch: config.yaml model.api_key must be honoured."""
_patch_anthropic_short_circuit(monkeypatch)
monkeypatch.setattr(rp, "_get_model_config", lambda: {"provider": "anthropic", "api_key": "***"})
resolved = rp.resolve_runtime_provider()
assert resolved["api_key"] == "***"
assert resolved["source"] == "config"


def test_anthropic_config_api_key_falls_back_to_env(monkeypatch):
"""When no config api_key, fall back to resolve_anthropic_token()."""
_patch_anthropic_short_circuit(monkeypatch)
monkeypatch.setattr(rp, "_get_model_config", lambda: {"provider": "anthropic"})
monkeypatch.setattr("agent.anthropic_adapter.resolve_anthropic_token", lambda: "sk-ant-env")
resolved = rp.resolve_runtime_provider()
assert resolved["api_key"] == "sk-ant-env"
assert resolved["source"] == "env"


def test_anthropic_config_api_key_no_cross_provider_leak(monkeypatch):
"""A config api_key from a *different* provider must not leak in."""
_patch_anthropic_short_circuit(monkeypatch)
monkeypatch.setattr(rp, "_get_model_config", lambda: {"provider": "openrouter", "api_key": "sk-or-leak"})
monkeypatch.setattr("agent.anthropic_adapter.resolve_anthropic_token", lambda: "sk-ant-env")
resolved = rp.resolve_runtime_provider()
assert resolved["api_key"] == "sk-ant-env"
assert resolved["api_key"] != "sk-or-leak"
Loading