hermes - ✅(Solved) Fix fix(provider): ACP sessions with provider="custom" fail to resolve correct credential [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#13489Fetched 2026-04-22 08:06:07
View on GitHub
Comments
1
Participants
2
Timeline
7
Reactions
0
Author
Participants
Timeline (top)
labeled ×4cross-referenced ×2commented ×1

Root Cause

When an ACP client creates a session with provider="custom" and a specific base_url (e.g., a DashScope-compatible endpoint), _get_named_custom_provider() returns None because "custom" is treated as a generic identifier:

PR fix notes

PR #13831: fix(runtime_provider): handle provider='custom' with explicit base_url in ACP sessions

Description (problem / solution / changelog)

Summary

When an ACP client creates a session with provider='custom' and an explicit_base_url (e.g. DashScope-compatible endpoint), the session restore path called _get_named_custom_provider('custom') which returns None (generic identifier). The explicit_base_url was then ignored and the wrong provider credentials were used, causing HTTP 401 errors.

Root Cause

In _resolve_named_custom_runtime(), the early return:

if not custom_provider:
    return None

didn't account for the explicit_base_url parameter. When provider='custom' is used with a raw base URL (no named entry in config.yaml), _get_named_custom_provider('custom') returns None and the function exits before ever looking at explicit_base_url.

Fix

Changed the early return to:

if not custom_provider and not explicit_base_url:
    return None

All custom_provider.get() calls are now guarded against None. The api_key_candidates list is built conditionally so we don't try to read api_key/key_env from a None provider.

Changes

  • hermes_cli/runtime_provider.py: _resolve_named_custom_runtime() now handles provider='custom' with explicit_base_url even when no named custom provider entry exists in config.yaml

Testing

  1. Configure an ACP client to send provider='custom' with a base_url pointing to a DashScope-compatible endpoint
  2. On session restore, _make_agent() calls resolve_runtime_provider(requested='custom')
  3. Previously: 401 from wrong endpoint (OpenRouter credentials sent to DashScope)
  4. Now: correct base_url is used, credentials resolved from explicit_api_key

Closes #13489

Changed files

  • agent/copilot_acp_client.py (modified, +1/-1)
  • agent/prompt_builder.py (modified, +7/-1)
  • hermes_cli/model_normalize.py (modified, +17/-0)
  • hermes_cli/runtime_provider.py (modified, +31/-15)
  • hermes_cli/tools_config.py (modified, +9/-1)
  • run_agent.py (modified, +48/-1)
  • tests/agent/test_prompt_builder.py (modified, +18/-0)
  • ui-tui/src/components/appChrome.tsx (modified, +4/-1)

Code Example

def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, Any]]:
    requested_norm = _normalize_custom_provider_name(requested_provider or "")
    if not requested_norm or requested_norm == "custom":
        return None  # <-- always returns None for "custom"

---

def _get_named_custom_provider(
    requested_provider: str,
    base_url: str | None = None,
) -> Optional[Dict[str, Any]]:
    requested_norm = _normalize_custom_provider_name(requested_provider or "")
    if not requested_norm:
        return None

    if requested_norm == "custom":
        # When provider is generic "custom", try matching by base_url
        if base_url:
            matched = _match_custom_provider_by_base_url(base_url)
            if matched:
                return matched
        return None
RAW_BUFFERClick to expand / collapse

Bug Description

When an ACP client creates a session with provider="custom" and a specific base_url (e.g., a DashScope-compatible endpoint), _get_named_custom_provider() returns None because "custom" is treated as a generic identifier:

https://github.com/NousResearch/hermes-agent/blob/main/hermes_cli/runtime_provider.py#L283

def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, Any]]:
    requested_norm = _normalize_custom_provider_name(requested_provider or "")
    if not requested_norm or requested_norm == "custom":
        return None  # <-- always returns None for "custom"

This causes resolve_runtime_provider() to fall through to resolve_provider("custom"), which resolves to "openrouter" as the default. The wrong API key is then sent to the target endpoint, resulting in HTTP 401 errors.

Reproduction

  1. Configure a custom provider in config.yaml (e.g., dashscope with its own base_url and api_key)
  2. Have an ACP client send provider="custom" with base_url pointing to that custom provider's endpoint
  3. The session's model_config stores {"provider": "custom", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1"}
  4. On session restore, _make_agent() calls resolve_runtime_provider(requested="custom")
  5. _get_named_custom_provider("custom") returns None → falls back to OpenRouter credentials → 401 from the target provider

Expected Behavior

When provider="custom" is paired with a base_url that matches a configured custom provider, the correct credential from that provider should be used.

Suggested Fix

The fix should be in _get_named_custom_provider() in runtime_provider.py. When requested_provider == "custom", instead of returning None, try to match against configured custom providers by base_url:

def _get_named_custom_provider(
    requested_provider: str,
    base_url: str | None = None,
) -> Optional[Dict[str, Any]]:
    requested_norm = _normalize_custom_provider_name(requested_provider or "")
    if not requested_norm:
        return None

    if requested_norm == "custom":
        # When provider is generic "custom", try matching by base_url
        if base_url:
            matched = _match_custom_provider_by_base_url(base_url)
            if matched:
                return matched
        return None

Where _match_custom_provider_by_base_url() iterates over configured custom_providers and returns the first entry whose base_url matches (prefix or host match). This way:

  • Any custom provider works automatically — no hardcoded URL patterns
  • The fix lives in the correct layer (provider resolution, not ACP adapter)
  • Callers that already pass base_url (e.g., ACP _make_agent()) benefit immediately

The function signature change is backward-compatible since base_url defaults to None, preserving existing behavior when not provided.

Environment

  • hermes-agent v0.10.0
  • macOS Darwin 25.4.0
  • ACP client: external desktop agent manager

extent analysis

TL;DR

Modify the _get_named_custom_provider() function to match custom providers by base_url when the provider is specified as "custom".

Guidance

  • Update the _get_named_custom_provider() function to accept an additional base_url parameter, allowing it to match custom providers based on the provided URL.
  • Implement the _match_custom_provider_by_base_url() function to iterate over configured custom providers and return the first match based on the base_url.
  • Ensure the function signature change is backward-compatible by defaulting base_url to None.
  • Verify the fix by testing with a custom provider configuration and a client sending provider="custom" with a matching base_url.

Example

def _match_custom_provider_by_base_url(base_url: str) -> Optional[Dict[str, Any]]:
    for provider in custom_providers:
        if provider["base_url"] == base_url or base_url.startswith(provider["base_url"]):
            return provider
    return None

Notes

The proposed fix assumes that the custom_providers configuration is accessible within the _match_custom_provider_by_base_url() function. Additionally, the base_url matching logic may need to be adjusted based on the specific requirements of the custom providers.

Recommendation

Apply the suggested workaround by modifying the _get_named_custom_provider() function to match custom providers by base_url when the provider is "custom", as this approach allows for flexible and automatic matching of custom providers without requiring hardcoded URL patterns.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix fix(provider): ACP sessions with provider="custom" fail to resolve correct credential [1 pull requests, 1 comments, 2 participants]