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)
This commit is contained in:
Teknium 2026-06-08 10:01:47 -07:00 committed by GitHub
parent de80d28f38
commit 9c9d9113a8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 102 additions and 0 deletions

View file

@ -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":

View file

@ -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

View file

@ -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 <token>` — 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")