NousResearch/hermes-agent

Custom providers sharing base_url using wrong credential pool keys

Cluster 9315 · agent. Data as of

1 open item. Fix merged in #21219 on 7 May 2026. Check it covers your case.

The short answer

1open issue
0open PRs
13items grouped
Known?
Yes. 13 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #21219. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #21219. That is separate from the fix event.
Still open
1 issue and 0 PRs.

Nearly all members concern custom providers that share a base_url receiving the wrong API key from the credential pool, including explicit api_key precedence and keeping named provider keys isolated. The group includes both issue reports and proposed fixes, and appears to center on one theme.

Open issues1

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

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #21219 fix(credential_pool): resolve key mix-up when custom providers share base_url (salvage #19094)

    merged to main

    files: agent/credential_pool.py, hermes_cli/runtime_provider.py, tests/agent/test_credential_pool.py

    Show the diff
    • agent/credential_pool.py
      @@ -305,14 +305,29 @@ def _iter_custom_providers(config: Optional[dict] = None):
               yield _normalize_custom_pool_name(name), entry
       
       
      -def get_custom_provider_pool_key(base_url: str) -> Optional[str]:
      +def get_custom_provider_pool_key(base_url: str, provider_name: Optional[str] = None) -> Optional[str]:
           """Look up the custom_providers list in config.yaml and return 'custom:<name>' for a matching base_url.
       
      +    When provider_name is given, prefer matching by name first (solving the case where
      +    multiple custom providers share the same base_url but have different API keys).
      +    Falls back to base_url matching when no name match is found.
      +
           Returns None if no match is found.
           """
           if not base_url:
               return None
           normalized_url = base_url.strip().rstrip("/")
      +
      +    # When a provider name is given, try to match by name first.
      +    # This fixes the P1 bug where two custom providers sharing the same
      +    # base_url always resolve to the first one's credentials.
      +    if provider_name:
      +        normalized_name = _normalize_custom_pool_name(provider_name)
      +        for norm_name, entry in _iter_custom_providers():
      +            if norm_name == normalized_name:
      +                return f"{CUSTOM_POOL_PREFIX}{norm_name}"
      +
      +    # Fall back to base_url matching (original behavior)
           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:
    • hermes_cli/runtime_provider.py
      @@ -319,9 +319,10 @@ def _try_resolve_from_custom_pool(
           base_url: str,
           provider_label: str,
           api_mode_override: Optional[str] = None,
      +    provider_name: Optional[str] = None,
       ) -> Optional[Dict[str, Any]]:
           """Check if a credential pool exists for a custom endpoint and return a runtime dict if so."""
      -    pool_key = get_custom_provider_pool_key(base_url)
      +    pool_key = get_custom_provider_pool_key(base_url, provider_name=provider_name)
           if not pool_key:
               return None
           try:
      @@ -521,7 +522,7 @@ def _resolve_named_custom_runtime(
               return None
       
           # Check if a credential pool exists for this custom endpoint
      -    pool_result = _try_resolve_from_custom_pool(base_url, "custom", custom_provider.get("api_mode"))
      +    pool_result = _try_resolve_from_custom_pool(base_url, "custom", custom_provider.get("api_mode"), provider_name=custom_provider.get("name"))
           if pool_result:
               # Propagate the model name even when using pooled credentials —
               # the pool doesn't know about the custom_providers model field.
      @@ -640,8 +641,11 @@ def _resolve_openrouter_runtime(
       
           # For custom endpoints, check if a credential pool exists
           if effective_provider == "custom" and base_url:
      +        # Pass requested_provider so pool lookup prefers name match over base_url,
      +        # fixing credential mix-ups when multiple custom providers share a base_url.
               pool_result = _try_resolve_from_custom_pool(
                   base_url, effective_provider, _parse_api_mode(model_cfg.get("api_mode")),
      +            provider_name=requested_provider if requested_norm != "custom" else None,
               )
               if pool_result:
                   return pool_result
    • tests/agent/test_credential_pool.py
      @@ -924,6 +924,43 @@ def test_get_custom_provider_pool_key(tmp_path, monkeypatch):
           assert get_custom_provider_pool_key("") is None
       
       
      +def test_get_custom_provider_pool_key_prefers_name_over_base_url(tmp_path, monkeypatch):
      +    """When two custom providers share the same base_url, provider_name resolves to the correct one."""
      +    monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
      +    (tmp_path / "hermes").mkdir(parents=True, exist_ok=True)
      +    import yaml
      +    config_path = tmp_path / "hermes" / "config.yaml"
      +    config_path.write_text(yaml.dump({
      +        "custom_providers": [
      +            {
      +                "name": "provider-a",
      +                "base_url": "http://gateway:8080/v1",
      +                "api_key": "[redacted:secret-assignment]",
      +            },
      +            {
      +                "name": "provider-b",
      +                "base_url": "http://gateway:8080/v1",
      +                "api_key": "[redacted:secret-assignment]",
      +            },
      +        ]
      +    }))
      +
      +    from agent.credential_pool import get_custom_provider_pool_key
      +
      +    # Without provider_name, first match wins (backward compatible)
      +    assert get_custom_provider_pool_key("http://gateway:8080/v1") == "custom:provider-a"
      +
      +    # With provider_name, exact name match wins regardless of order
      +    assert get_custom_provider_pool_key("http://gateway:8080/v1", provider_name="provider-b") == "custom:provider-b"
      +    assert get_custom_provider_pool_key("http://gateway:8080/v1", provider_name="provider-a") == "custom:provider-a"
      +
      +    # Name match with non-matching base_url still works via fallback
      +    assert get_custom_provider_pool_key("http://gateway:8080/v1", provider_name="nonexistent") == "custom:provider-a"
      +
      +    # Empty provider_name is same as None (backward compatible)
      +    assert get_custom_provider_pool_key("http://gateway:8080/v1", provider_name="") == "custom:provider-a"
      +
      +
       def test_list_custom_pool_providers(tmp_path, monkeypatch):
           """list_custom_pool_providers returns custom: pool keys from auth.json."""
           monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))

9 PRs closed without merging

From the close comments: duplicate 4 · done elsewhere 3 · stale or abandoned 1 · superseded by another PR 1

  • #9322 fix: explicit api_key should override pool credential for custom providers (#9315) · stale or abandoned
  • #9386 Fix custom provider API key precedence · superseded by another PR → #21219
  • #10948 fix(runtime): preserve explicit custom API key precedence · duplicate → #9386
  • #14142 Fix: Custom providers sharing same base_url use wrong API credentials · done elsewhere → #14141
  • #14144 fix(runtime): keep named custom provider keys isolated · done elsewhere → #14141
Show 4 more
  • #14797 fix(cli): isolate custom provider credentials when providers share base_url · duplicate
  • #19094 fix(credential_pool): resolve key mix-up when custom providers share base_url · duplicate → #14142, #18804
  • #19122 fix(agent): disambiguate credential pool key lookup when multiple providers share the same base_url (#19083) · duplicate → #14142, #14141
  • #19682 fix(agent): resolve credential pool key ambiguity for shared base_url · done elsewhere → e38ea38, #21219, #19083
Closed issues · 2
  • Closed#14141Custom providers sharing same base_url use wrong API credentials
  • Closed#19083Credential pool key mix-up when multiple custom providers share the same base_url
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.15: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#21219
Closed issues
2
Attempts
9 PRs closed without merging
Fix link checked
7 May 2026

For your agent

The same facts, typed.