Bug Description
When two or more custom_providers entries share the same base_url but have different api_key values, Hermes ignores the explicit model.api_key configuration and instead loads the key from the first provider in the list that matches the base_url. The explicit configuration is silently overridden.
Steps to Reproduce
# ~/.hermes/config.yaml
custom_providers:
- name: provider_a
base_url: https://api.example.com/v1
api_key: key_aaaaa
- name: provider_b
base_url: https://api.example.com/v1
api_key: key_bbbbb
model:
provider: custom
base_url: https://api.example.com/v1
api_key: key_bbbbb # Explicitly intended to use provider_b's key
- Start a Hermes session with the config above
- Observe the
api_key that reaches the upstream API (e.g. via provider logs or a debug proxy)
Expected Behavior
Hermes should use key_bbbbb (the value explicitly set in model.api_key).
Actual Behavior
Hermes uses key_aaaaa (from provider_a) because it appears first in the list.
Reversing the order of provider_a and provider_b flips which key is used, confirming the bug is ordering-dependent.
Affected Component
Configuration (config.yaml, .env, hermes setup)
Messaging Platform (if gateway-related)
Discord
Operating System
macOS 26
Python Version
3.11.15
Hermes Version
0.8.0
Relevant Logs / Traceback
Root Cause Analysis (optional)
The call chain that triggers the issue:
resolve_runtime_provider()
→ _resolve_openrouter_runtime()
→ _try_resolve_from_custom_pool(base_url, ...)
→ get_custom_provider_pool_key(base_url) # ← root cause
→ _iter_custom_providers()
for each entry: if entry_url == base_url → return first match
get_custom_provider_pool_key() in agent/credential_pool.py (L302–313) iterates custom_providers in list order and returns the first matching pool key for a given base_url, ignoring which provider was intended:
# credential_pool.py L310-313
for norm_name, entry in _iter_custom_providers():
entry_url = str(entry.get("base_url") or "").strip().rstrip("/")
if entry_url and entry_url == normalized_url:
return f"{CUSTOM_POOL_PREFIX}{norm_name}" # first match wins, regardless of api_key
_seed_custom_pool() (L1270–1329) then seeds the resolved pool with provider_a's api_key. Even though model.api_key = key_bbbbb is also seeded separately (L1297–1327), the pool's select() picks provider_a's entry first due to priority ordering.
The net effect: model.api_key is silently overridden by the first custom_providers entry whose base_url matches.
Proposed Fix (optional)
Priority order for key resolution should be:
1. Explicit model.api_key (highest priority — never override)
2. Named provider match via model.provider (custom:provider_b)
3. base_url match (fallback, only when no explicit key is set)
Concrete options:
Option A (minimal change) — Pass explicit_api_key into _try_resolve_from_custom_pool. If explicit_api_key is present and non-empty, skip the pool entirely and use it directly:
def _try_resolve_from_custom_pool(base_url, provider_label, api_mode_override=None, explicit_api_key=None):
if explicit_api_key:
return None # caller already has a key — don't override it
...
Option B (preferred) — In _seed_custom_pool, give the model_config source a higher priority than config:<name> entries, so the explicit model.api_key wins the pool.select() call.
Option C (robust) — get_custom_provider_pool_key should match by provider name first (via model.provider = "custom:provider_b") before falling back to URL matching.
Are you willing to submit a PR for this?
Bug Description
When two or more
custom_providersentries share the samebase_urlbut have differentapi_keyvalues, Hermes ignores the explicitmodel.api_keyconfiguration and instead loads the key from the first provider in the list that matches thebase_url. The explicit configuration is silently overridden.Steps to Reproduce
api_keythat reaches the upstream API (e.g. via provider logs or a debug proxy)Expected Behavior
Hermes should use
key_bbbbb(the value explicitly set inmodel.api_key).Actual Behavior
Hermes uses
key_aaaaa(fromprovider_a) because it appears first in the list.Reversing the order of
provider_aandprovider_bflips which key is used, confirming the bug is ordering-dependent.Affected Component
Configuration (config.yaml, .env, hermes setup)
Messaging Platform (if gateway-related)
Discord
Operating System
macOS 26
Python Version
3.11.15
Hermes Version
0.8.0
Relevant Logs / Traceback
Root Cause Analysis (optional)
The call chain that triggers the issue:
get_custom_provider_pool_key()inagent/credential_pool.py(L302–313) iteratescustom_providersin list order and returns the first matching pool key for a givenbase_url, ignoring which provider was intended:_seed_custom_pool()(L1270–1329) then seeds the resolved pool withprovider_a'sapi_key. Even thoughmodel.api_key = key_bbbbbis also seeded separately (L1297–1327), the pool'sselect()picksprovider_a's entry first due to priority ordering.The net effect:
model.api_keyis silently overridden by the firstcustom_providersentry whosebase_urlmatches.Proposed Fix (optional)
Priority order for key resolution should be:
Concrete options:
Option A (minimal change) — Pass
explicit_api_keyinto_try_resolve_from_custom_pool. Ifexplicit_api_keyis present and non-empty, skip the pool entirely and use it directly:Option B (preferred) — In
_seed_custom_pool, give themodel_configsource a higher priority thanconfig:<name>entries, so the explicitmodel.api_keywins thepool.select()call.Option C (robust) —
get_custom_provider_pool_keyshould match by provider name first (viamodel.provider = "custom:provider_b") before falling back to URL matching.Are you willing to submit a PR for this?