From f304bc63b802f217739f71c201193f4812c2e22a Mon Sep 17 00:00:00 2001 From: aashizpoudel Date: Sat, 21 Mar 2026 12:55:42 -0700 Subject: [PATCH] fix: ignore placeholder provider keys in provider activation checks Add has_usable_secret() to reject empty, short (<4 char), and common placeholder API key values (changeme, your_api_key, placeholder, etc.) throughout the auth/runtime resolution chain. Update list_available_providers() to use provider-specific auth status via get_auth_status() instead of resolve_runtime_provider(), preventing cross-provider key fallback from making providers appear available when they aren't actually configured. Preserve keyless custom endpoint support by checking via base URL. Cherry-picked from PR #2121 by aashizpoudel. --- hermes_cli/auth.py | 33 +++++++++++++++++++++++--- hermes_cli/models.py | 11 +++++---- hermes_cli/runtime_provider.py | 42 +++++++++++++++++++--------------- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 293f91e02..cc58eb1a8 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -278,6 +278,33 @@ def _try_gh_cli_token() -> Optional[str]: return None +_PLACEHOLDER_SECRET_VALUES = { + "*", + "**", + "***", + "changeme", + "your_api_key", + "your-api-key", + "placeholder", + "example", + "dummy", + "null", + "none", +} + + +def has_usable_secret(value: Any, *, min_length: int = 4) -> bool: + """Return True when a configured secret looks usable, not empty/placeholder.""" + if not isinstance(value, str): + return False + cleaned = value.strip() + if len(cleaned) < min_length: + return False + if cleaned.lower() in _PLACEHOLDER_SECRET_VALUES: + return False + return True + + def _resolve_api_key_provider_secret( provider_id: str, pconfig: ProviderConfig ) -> tuple[str, str]: @@ -297,7 +324,7 @@ def _resolve_api_key_provider_secret( for env_var in pconfig.api_key_env_vars: val = os.getenv(env_var, "").strip() - if val: + if has_usable_secret(val): return val, env_var return "", "" @@ -688,7 +715,7 @@ def resolve_provider( except Exception as e: logger.debug("Could not detect active auth provider: %s", e) - if os.getenv("OPENAI_API_KEY") or os.getenv("OPENROUTER_API_KEY"): + if has_usable_secret(os.getenv("OPENAI_API_KEY")) or has_usable_secret(os.getenv("OPENROUTER_API_KEY")): return "openrouter" # Auto-detect API-key providers by checking their env vars @@ -701,7 +728,7 @@ def resolve_provider( if pid == "copilot": continue for env_var in pconfig.api_key_env_vars: - if os.getenv(env_var, "").strip(): + if has_usable_secret(os.getenv(env_var, "")): return pid return "openrouter" diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 495c0ca70..4874ce512 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -300,12 +300,15 @@ def list_available_providers() -> list[dict[str, str]]: # Check if this provider has credentials available has_creds = False try: + from hermes_cli.auth import get_auth_status, has_usable_secret if pid == "custom": - has_creds = bool(_get_custom_base_url()) + custom_base_url = _get_custom_base_url() or os.getenv("OPENAI_BASE_URL", "") + has_creds = bool(custom_base_url.strip()) + elif pid == "openrouter": + has_creds = has_usable_secret(os.getenv("OPENROUTER_API_KEY", "")) else: - from hermes_cli.runtime_provider import resolve_runtime_provider - runtime = resolve_runtime_provider(requested=pid) - has_creds = bool(runtime.get("api_key")) + status = get_auth_status(pid) + has_creds = bool(status.get("logged_in") or status.get("configured")) except Exception: pass result.append({ diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index 8c2979b6b..daac5cfd0 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -15,6 +15,7 @@ from hermes_cli.auth import ( resolve_codex_runtime_credentials, resolve_api_key_provider_credentials, resolve_external_process_provider_credentials, + has_usable_secret, ) from hermes_cli.config import load_config from hermes_constants import OPENROUTER_BASE_URL @@ -188,12 +189,13 @@ def _resolve_named_custom_runtime( if not base_url: return None - api_key = ( - (explicit_api_key or "").strip() - or custom_provider.get("api_key", "") - or os.getenv("OPENAI_API_KEY", "").strip() - or os.getenv("OPENROUTER_API_KEY", "").strip() - ) + api_key_candidates = [ + (explicit_api_key or "").strip(), + str(custom_provider.get("api_key", "") or "").strip(), + os.getenv("OPENAI_API_KEY", "").strip(), + os.getenv("OPENROUTER_API_KEY", "").strip(), + ] + api_key = next((candidate for candidate in api_key_candidates if has_usable_secret(candidate)), "") return { "provider": "openrouter", @@ -257,21 +259,23 @@ def _resolve_openrouter_runtime( # provider (issues #420, #560). _is_openrouter_url = "openrouter.ai" in base_url if _is_openrouter_url: - api_key = ( - explicit_api_key - or os.getenv("OPENROUTER_API_KEY") - or os.getenv("OPENAI_API_KEY") - or "" - ) + api_key_candidates = [ + explicit_api_key, + os.getenv("OPENROUTER_API_KEY"), + os.getenv("OPENAI_API_KEY"), + ] else: # Custom endpoint: use api_key from config when using config base_url (#1760). - api_key = ( - explicit_api_key - or (cfg_api_key if use_config_base_url else "") - or os.getenv("OPENAI_API_KEY") - or os.getenv("OPENROUTER_API_KEY") - or "" - ) + api_key_candidates = [ + explicit_api_key, + (cfg_api_key if use_config_base_url else ""), + os.getenv("OPENAI_API_KEY"), + os.getenv("OPENROUTER_API_KEY"), + ] + api_key = next( + (str(candidate or "").strip() for candidate in api_key_candidates if has_usable_secret(candidate)), + "", + ) source = "explicit" if (explicit_api_key or explicit_base_url) else "env/config"