Skip to content

[Bug]: Custom provider API key incorrectly overridden by base_url matching #9315

@KyoMio

Description

@KyoMio

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
  1. Start a Hermes session with the config above
  2. 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?

  • I'd like to fix this myself and submit a PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    type/bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions