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
24 changes: 23 additions & 1 deletion hermes_cli/auth_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,30 @@ def _provider_base_url(provider: str) -> str:
if cp_config:
return str(cp_config.get("base_url") or "").strip()
return ""

pconfig = PROVIDER_REGISTRY.get(provider)
return pconfig.inference_base_url if pconfig else ""
registry_base_url = pconfig.inference_base_url if pconfig else ""

# Manual credentials should follow the active model endpoint. Some
# providers expose multiple OpenAI-compatible hosts for the same API-key
# shape (for example Xiaomi MiMo's standard API endpoint vs Token Plan
# endpoint). If `hermes auth add <provider>` persists the registry default
# into the credential pool, runtime resolution can override a deliberate
# `model.base_url` setting and send a valid key to the wrong host.
try:
from hermes_cli.config import load_config

config = load_config()
model_cfg = config.get("model") if isinstance(config, dict) else None
if isinstance(model_cfg, dict):
configured_provider = _normalize_provider(str(model_cfg.get("provider") or ""))
configured_base_url = str(model_cfg.get("base_url") or "").strip().rstrip("/")
if configured_provider == provider and configured_base_url:
return configured_base_url
except Exception:
pass

return registry_base_url


def _oauth_default_label(provider: str, count: int) -> str:
Expand Down
88 changes: 88 additions & 0 deletions tests/hermes_cli/test_auth_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,94 @@ class _Args:
assert entry["access_token"] == "sk-or-manual"


def test_auth_add_api_key_uses_config_base_url_for_active_provider(tmp_path, monkeypatch):
"""Manual API keys should inherit model.base_url for the active provider.

Xiaomi MiMo Token Plan users configure ``model.base_url`` to the Token Plan
endpoint. If ``hermes auth add xiaomi`` stores the provider registry
default instead, credential-pool runtime resolution silently sends the key
to the wrong endpoint and marks an otherwise-valid key as 401/exhausted.
"""
hermes_home = tmp_path / "hermes"
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("XIAOMI_API_KEY", raising=False)
monkeypatch.delenv("XIAOMI_BASE_URL", raising=False)
_write_auth_store(tmp_path, {"version": 1, "providers": {}})
token_plan_url = "https://token-plan-ams.xiaomimimo.com/v1"
(hermes_home / "config.yaml").write_text(
yaml.safe_dump(
{
"model": {
"provider": "xiaomi",
"name": "mimo-v2.5-pro",
"base_url": token_plan_url,
}
}
)
)

from hermes_cli.auth_commands import auth_add_command

class _Args:
provider = "xiaomi"
auth_type = "api-key"
api_key = "sk-xiaomi-token-plan"
label = "token-plan"

auth_add_command(_Args())

payload = json.loads((hermes_home / "auth.json").read_text())
entries = payload["credential_pool"]["xiaomi"]
entry = next(item for item in entries if item["source"] == "manual")
assert entry["base_url"] == token_plan_url


def test_auth_add_api_key_ignores_config_base_url_for_other_provider(tmp_path, monkeypatch):
"""A manual key for a provider that is NOT the active one must not inherit
the configured ``model.base_url``.

This locks in the negative branch of the fix: with Xiaomi's Token Plan URL
configured as the active endpoint, adding an Anthropic key must fall back to
Anthropic's registry endpoint — never Xiaomi's host. Without this guard the
"narrow fix" guarantee (only the same-provider case follows the config)
would be untested, since the existing OpenRouter test configures no foreign
``model.base_url`` and so never exercises ``configured_provider != provider``.
"""
hermes_home = tmp_path / "hermes"
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("XIAOMI_API_KEY", raising=False)
monkeypatch.delenv("XIAOMI_BASE_URL", raising=False)
_write_auth_store(tmp_path, {"version": 1, "providers": {}})
token_plan_url = "https://token-plan-ams.xiaomimimo.com/v1"
(hermes_home / "config.yaml").write_text(
yaml.safe_dump(
{
"model": {
"provider": "xiaomi",
"name": "mimo-v2.5-pro",
"base_url": token_plan_url,
}
}
)
)

from hermes_cli.auth_commands import auth_add_command

class _Args:
provider = "anthropic"
auth_type = "api-key"
api_key = "sk-ant-manual"
label = "anthropic-manual"

auth_add_command(_Args())

payload = json.loads((hermes_home / "auth.json").read_text())
entries = payload["credential_pool"]["anthropic"]
entry = next(item for item in entries if item["source"] == "manual")
# Anthropic must NOT inherit Xiaomi's configured Token Plan endpoint.
assert entry["base_url"] != token_plan_url


def test_auth_add_anthropic_oauth_persists_pool_entry(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
Expand Down