From 9c9d9113a8ae8f85aaf1ebff74ddd1fdf0b5b9a8 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:01:47 -0700 Subject: [PATCH] fix(auth): auto-detect OpenRouter credential from the pool, not just env (#42263) resolve_provider() auto-detection only checked OPENROUTER_API_KEY/ OPENAI_API_KEY env vars, never the credential pool. A key added via `hermes auth add openrouter` (manual pool entry, no env var) was invisible: the provider failed to resolve or resolved with an empty api_key, so requests went out with no Authorization header and OpenRouter returned "HTTP 401: Missing Authentication header" while `hermes auth list` showed the credential. Closes #42130. - auth.py: check load_pool("openrouter").has_credentials() after the env check - dump.py: `debug share` shows 'openrouter set (auth pool)' instead of the misleading 'not set' when the key lives in the pool - add regression tests (pool credential auto-detects; empty pool still raises) --- hermes_cli/auth.py | 15 ++++ hermes_cli/dump.py | 11 +++ .../test_resolve_provider_openrouter_pool.py | 76 +++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 tests/hermes_cli/test_resolve_provider_openrouter_pool.py 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")