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
20 changes: 17 additions & 3 deletions hermes_cli/runtime_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,14 +467,21 @@ def _resolve_explicit_runtime(
cfg_base_url = str(model_cfg.get("base_url") or "").strip().rstrip("/")
base_url = explicit_base_url or cfg_base_url or "https://api.anthropic.com"
api_key = explicit_api_key
if not api_key:
# Honour model.api_key from config.yaml before falling back to env.
for k in ("api_key", "api"):
v = model_cfg.get(k)
if isinstance(v, str) and v.strip():
api_key = v.strip()
break
if not api_key:
from agent.anthropic_adapter import resolve_anthropic_token

api_key = resolve_anthropic_token()
if not api_key:
raise AuthError(
"No Anthropic credentials found. Set ANTHROPIC_TOKEN or ANTHROPIC_API_KEY, "
"run 'claude setup-token', or authenticate with 'claude /login'."
"add api_key to config.yaml, run 'claude setup-token', or authenticate with 'claude /login'."
)
return {
"provider": "anthropic",
Expand Down Expand Up @@ -745,12 +752,19 @@ def resolve_runtime_provider(

# Anthropic (native Messages API)
if provider == "anthropic":
# Honour model.api_key from config.yaml (consistent with other providers).
cfg_api_key = ""
for k in ("api_key", "api"):
v = model_cfg.get(k)
if isinstance(v, str) and v.strip():
cfg_api_key = v.strip()
break
from agent.anthropic_adapter import resolve_anthropic_token
token = resolve_anthropic_token()
token = cfg_api_key or resolve_anthropic_token()
if not token:
raise AuthError(
"No Anthropic credentials found. Set ANTHROPIC_TOKEN or ANTHROPIC_API_KEY, "
"run 'claude setup-token', or authenticate with 'claude /login'."
"add api_key to config.yaml, run 'claude setup-token', or authenticate with 'claude /login'."
)
# Allow base URL override from config.yaml model.base_url, but only
# when the configured provider is anthropic — otherwise a non-Anthropic
Expand Down
65 changes: 65 additions & 0 deletions tests/hermes_cli/test_runtime_provider_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,71 @@ def _unexpected_anthropic_token():
assert resolved.get("credential_pool") is None


def test_resolve_runtime_provider_anthropic_respects_config_api_key(monkeypatch):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers the normal resolver only. Please add a regression for the _resolve_explicit_runtime path as well: pass explicit_base_url with no explicit_api_key, provide model.api_key, and assert it wins without calling resolve_anthropic_token.

"""Anthropic provider should use model.api_key from config.yaml when set,
without requiring ANTHROPIC_API_KEY or ANTHROPIC_TOKEN env vars."""

def _unexpected_anthropic_token():
raise AssertionError("resolve_anthropic_token should not be called when config api_key is set")

class _Pool:
def has_credentials(self):
return False

monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "anthropic")
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {
"provider": "anthropic",
"base_url": "https://proxy.example.com/anthropic",
"api_key": "config-anthropic-key",
},
)
monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool())
monkeypatch.setattr(
"agent.anthropic_adapter.resolve_anthropic_token",
_unexpected_anthropic_token,
)
# Ensure no env vars are set
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)

resolved = rp.resolve_runtime_provider(requested="anthropic")

assert resolved["provider"] == "anthropic"
assert resolved["api_mode"] == "anthropic_messages"
assert resolved["api_key"] == "config-anthropic-key"
assert resolved["base_url"] == "https://proxy.example.com/anthropic"


def test_resolve_runtime_provider_anthropic_config_api_field(monkeypatch):
"""Anthropic provider should also accept 'api' field from config.yaml."""

class _Pool:
def has_credentials(self):
return False

monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "anthropic")
monkeypatch.setattr(
rp,
"_get_model_config",
lambda: {
"provider": "anthropic",
"base_url": "https://proxy.example.com/anthropic",
"api": "config-api-field-key",
},
)
monkeypatch.setattr(rp, "load_pool", lambda provider: _Pool())
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.delenv("ANTHROPIC_TOKEN", raising=False)

resolved = rp.resolve_runtime_provider(requested="anthropic")

assert resolved["provider"] == "anthropic"
assert resolved["api_key"] == "config-api-field-key"


def test_resolve_runtime_provider_falls_back_when_pool_empty(monkeypatch):
class _Pool:
def has_credentials(self):
Expand Down