diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 021905c3ec0..cfd4ad5f8a6 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -1561,6 +1561,21 @@ def resolve_provider( if has_usable_secret(os.getenv("OPENAI_API_KEY")) or has_usable_secret(os.getenv("OPENROUTER_API_KEY")): return "openrouter" + # Auto-detect an OpenRouter credential added via `hermes auth add openrouter` + # (manual pool entry, no env var). Without this, a key that only lives in + # the credential pool is invisible to auto-detection — the user sees + # `hermes auth list` showing the credential while requests go out with no + # Authorization header ("HTTP 401: Missing Authentication header"). The + # env-var check above only covers keys exported as OPENROUTER_API_KEY / + # OPENAI_API_KEY. See issue #42130. + try: + from agent.credential_pool import load_pool as _load_pool + + if _load_pool("openrouter").has_credentials(): + return "openrouter" + except Exception as e: + logger.debug("Could not check OpenRouter credential pool: %s", e) + # Auto-detect API-key providers by checking their env vars for pid, pconfig in PROVIDER_REGISTRY.items(): if pconfig.auth_type != "api_key": diff --git a/hermes_cli/dump.py b/hermes_cli/dump.py index 98de32bcdea..16d6f6069f9 100644 --- a/hermes_cli/dump.py +++ b/hermes_cli/dump.py @@ -318,6 +318,17 @@ def run_dump(args): display = _redact(val) else: display = "set" if val else "not set" + # A credential added via `hermes auth add openrouter` lives in the + # credential pool, not as an env var — surface it so the dump doesn't + # misleadingly read "not set" while `hermes auth list` shows it (#42130). + if not val and label == "openrouter": + try: + from agent.credential_pool import load_pool as _load_pool + + if _load_pool("openrouter").has_credentials(): + display = "set (auth pool)" + except Exception: + pass lines.append(f" {label:<20} {display}") # Features summary diff --git a/tests/hermes_cli/test_resolve_provider_openrouter_pool.py b/tests/hermes_cli/test_resolve_provider_openrouter_pool.py new file mode 100644 index 00000000000..a60cc1e81cb --- /dev/null +++ b/tests/hermes_cli/test_resolve_provider_openrouter_pool.py @@ -0,0 +1,76 @@ +"""Regression tests for issue #42130. + +A credential added via `hermes auth add openrouter` lives in the credential +pool, NOT as an OPENROUTER_API_KEY env var. Before the fix, resolve_provider() +auto-detection only checked env vars, so such a credential was invisible: +the provider failed to resolve (AuthError) or resolved without a key, and +requests went out with no Authorization header — OpenRouter's +"HTTP 401: Missing Authentication header". + +These tests lock in that auto-detection consults the OpenRouter pool. +""" + +import uuid + +import pytest + + +@pytest.fixture(autouse=True) +def _clean_inference_env(monkeypatch): + """Strip credential-shaped env vars so the pool is the only source.""" + for key in ( + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + "ANTHROPIC_API_KEY", + "ANTHROPIC_TOKEN", + "CLAUDE_CODE_OAUTH_TOKEN", + "NOUS_API_KEY", + "HERMES_INFERENCE_PROVIDER", + ): + monkeypatch.delenv(key, raising=False) + + +def _seed_openrouter_pool(token: str = "sk-or-FAKEKEY123") -> None: + """Mimic `hermes auth add openrouter ` — a manual pool entry.""" + from agent.credential_pool import ( + AUTH_TYPE_API_KEY, + SOURCE_MANUAL, + PooledCredential, + load_pool, + ) + + pool = load_pool("openrouter") + pool.add_entry( + PooledCredential( + provider="openrouter", + id=uuid.uuid4().hex[:6], + label="api-key-1", + auth_type=AUTH_TYPE_API_KEY, + priority=0, + source=SOURCE_MANUAL, + access_token=token, + base_url="https://openrouter.ai/api/v1", + ) + ) + + +def test_auto_detects_openrouter_from_pool(tmp_path, monkeypatch): + """With only a pool credential (no env var), auto-detection finds it.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + (tmp_path / "hermes").mkdir(parents=True, exist_ok=True) + _seed_openrouter_pool() + + from hermes_cli.auth import resolve_provider + + assert resolve_provider("auto") == "openrouter" + + +def test_no_credentials_still_raises(tmp_path, monkeypatch): + """Empty pool + no env var must still fail to resolve — no false positive.""" + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + (tmp_path / "hermes").mkdir(parents=True, exist_ok=True) + + from hermes_cli.auth import AuthError, resolve_provider + + with pytest.raises(AuthError): + resolve_provider("auto")