# NousResearch/hermes-agent · Custom providers sharing base_url using wrong credential pool keys

## Open now

- 1 issue, 0 PRs open on GitHub.
- #9315: \[Bug\]: Custom provider API key incorrectly overridden by base_url matching

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.

## TL;DR

- 13 member(s): 1 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 9 PR(s) closed without merging.
- A fix was merged to main in PR #21219. Verify it addresses your case.
- **Recommended:** read PR #21219 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-21219`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Open related issues

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

## Closed / duplicate issues

- #14141: Custom providers sharing same base_url use wrong API credentials
- #19083: Credential pool key mix-up when multiple custom providers share the same base_url

## Prior & competing PRs

- **#14142: Fix: Custom providers sharing same base_url use wrong API credentials**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#14141`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#21219: fix(credential_pool): resolve key mix-up when custom providers share base_url (salvage #19094)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/credential_pool.py`, `hermes_cli/runtime_provider.py`, `tests/agent/test_credential_pool.py`
  - fix hunks:
  - `agent/credential_pool.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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:
    ```

    ```diff
    @@ -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.
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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"))
    ```
- **#9322: fix: explicit api_key should override pool credential for custom providers (#9315)**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#9386: Fix custom provider API key precedence**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#21219`
- **#10948: fix(runtime): preserve explicit custom API key precedence**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#9386`
- **#14144: fix(runtime): keep named custom provider keys isolated**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#14141`
- **#14797: fix(cli): isolate custom provider credentials when providers share base_url**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`
- **#19094: fix(credential_pool): resolve key mix-up when custom providers share base_url**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#14142`, `#18804`
- **#19122: fix(agent): disambiguate credential pool key lookup when multiple providers share the same base_url (#19083)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#14142`, `#14141`
- **#19682: fix(agent): resolve credential pool key ambiguity for shared base_url**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `e38ea38079b8683fba48a245c19ff5a2a8f50d39`, `#21219`, `#19083`

## Fix status

- Merged to main in PR #21219 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-07`.
- Subsystem: `agent`
