What I'm seeing
When model.provider is custom in config.yaml and model.base_url points at a non-OpenRouter, non-OpenAI commercial endpoint (DeepSeek, Groq, Mistral, etc.), the gateway sends the user's OPENAI_API_KEY (or OPENROUTER_API_KEY) to that endpoint.
Manifests as a 401 with a masked key tail that doesn't match anything the user thinks they configured:
Error code: 401 - {'error': {'message': 'Authentication Fails, Your api key: ****ired is invalid', ...}}
The ****ired is the last 4 chars of OPENAI_API_KEY (or whatever else trailing-matched).
Root cause
hermes_cli/runtime_provider.py::_resolve_openrouter_runtime, the fallback chain for custom endpoints:
# When hitting a custom endpoint (e.g. Z.ai, local LLM), prefer
# OPENAI_API_KEY so the OpenRouter key doesn't leak to an unrelated
# provider (issues #420, #560).
_is_ollama_url = base_url_host_matches(base_url, "ollama.com")
api_key_candidates = [
explicit_api_key,
(cfg_api_key if use_config_base_url else ""),
(os.getenv("OLLAMA_API_KEY") if _is_ollama_url else ""),
os.getenv("OPENAI_API_KEY"), # ← runs for *any* non-openrouter URL
os.getenv("OPENROUTER_API_KEY"), # ← same
]
The Ollama path correctly gates on host (_is_ollama_url), but OPENAI_API_KEY and OPENROUTER_API_KEY are added unconditionally. So provider="custom" + base_url="https://api.deepseek.com/v1" ends up sending an OpenAI key to DeepSeek.
Symmetrical to (and predates) the OpenRouter-leak fix in the comment block above — same class of bug, just a different scapegoat key.
Reproduction
# ~/.hermes/config.yaml
model:
provider: "custom"
default: "deepseek-chat"
base_url: "https://api.deepseek.com/v1"
providers: {}
# ~/.hermes/.env
OPENAI_API_KEY=sk-something-from-an-old-experiment
# DEEPSEEK_API_KEY intentionally unset
Send any chat → 401 from DeepSeek complaining about an sk-… key it doesn't recognize.
Suggested fix
Gate OPENAI_API_KEY / OPENROUTER_API_KEY on the resolved host, the same way OLLAMA_API_KEY is already gated. The principle: an env var that's named for a specific provider should only feed an endpoint pointed at that provider:
_is_openrouter_url = base_url_host_matches(base_url, "openrouter.ai")
_is_openai_url = base_url_host_matches(base_url, "openai.com")
_is_ollama_url = base_url_host_matches(base_url, "ollama.com")
api_key_candidates = [
explicit_api_key,
(cfg_api_key if use_config_base_url else ""),
(os.getenv("OLLAMA_API_KEY") if _is_ollama_url else ""),
(os.getenv("OPENAI_API_KEY") if _is_openai_url else ""),
(os.getenv("OPENROUTER_API_KEY") if _is_openrouter_url else ""),
]
For non-OpenAI / non-OpenRouter custom hosts, the chain then ends with cfg_api_key (matching the existing Ollama precedent) — effective_provider == "custom" and not api_key already falls back to "no-key-required" (line 718–719), so local LLMs without a key continue to work.
Bonus: it would be nice to also read a <NAME>_API_KEY derived from the host (e.g. api.deepseek.com → DEEPSEEK_API_KEY) as an extra candidate. Not strictly required to close this bug, but it's what users intuitively expect.
Environment
Reproduced on hermes-agent 0.14.x.
What I'm seeing
When
model.provideriscustominconfig.yamlandmodel.base_urlpoints at a non-OpenRouter, non-OpenAI commercial endpoint (DeepSeek, Groq, Mistral, etc.), the gateway sends the user'sOPENAI_API_KEY(orOPENROUTER_API_KEY) to that endpoint.Manifests as a 401 with a masked key tail that doesn't match anything the user thinks they configured:
The
****iredis the last 4 chars ofOPENAI_API_KEY(or whatever else trailing-matched).Root cause
hermes_cli/runtime_provider.py::_resolve_openrouter_runtime, the fallback chain for custom endpoints:The Ollama path correctly gates on host (
_is_ollama_url), butOPENAI_API_KEYandOPENROUTER_API_KEYare added unconditionally. Soprovider="custom"+base_url="https://api.deepseek.com/v1"ends up sending an OpenAI key to DeepSeek.Symmetrical to (and predates) the OpenRouter-leak fix in the comment block above — same class of bug, just a different scapegoat key.
Reproduction
Send any chat → 401 from DeepSeek complaining about an
sk-…key it doesn't recognize.Suggested fix
Gate
OPENAI_API_KEY/OPENROUTER_API_KEYon the resolved host, the same wayOLLAMA_API_KEYis already gated. The principle: an env var that's named for a specific provider should only feed an endpoint pointed at that provider:For non-OpenAI / non-OpenRouter custom hosts, the chain then ends with
cfg_api_key(matching the existing Ollama precedent) —effective_provider == "custom" and not api_keyalready falls back to"no-key-required"(line 718–719), so local LLMs without a key continue to work.Bonus: it would be nice to also read a
<NAME>_API_KEYderived from the host (e.g.api.deepseek.com→DEEPSEEK_API_KEY) as an extra candidate. Not strictly required to close this bug, but it's what users intuitively expect.Environment
Reproduced on hermes-agent 0.14.x.