hermes - ✅(Solved) Fix Named custom providers with api_mode: anthropic_messages fail with 404 in auxiliary tasks [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#15033Fetched 2026-04-25 06:24:53
View on GitHub
Comments
1
Participants
2
Timeline
10
Reactions
0
Author
Participants
Timeline (top)
labeled ×4referenced ×3closed ×1commented ×1

When a named custom provider in config.yaml under providers: declares api_mode: anthropic_messages, auxiliary task calls (session_search / skills_hub / approval / flush_memories / etc.) incorrectly route to an AsyncOpenAI client instead of the AnthropicAuxiliaryClient. The AsyncOpenAI client then POSTs to {base_url}/chat/completions, which for Anthropic-compatible gateways doesn't exist, producing a 404.

Tested on Hermes Agent v0.11.0 (2026.4.23).

Error Message

client: AsyncOpenAI base_url: https://example-relay.test/anthropic/ NotFoundError: Error code: 404 - {'error': {'message': 'Unsupported Anthropic API endpoint ...', 'type': 'invalid_request_error', 'code': 404}}

Root Cause

Root Cause — two separate issues

Fix Action

Workaround

Configure the provider with api_mode: chat_completions against the gateway's OpenAI-compatible endpoint. Cost: loses protocol-native Anthropic features.

PR fix notes

PR #15059: fix(aux-client): honor api_mode: anthropic_messages for named custom providers

Description (problem / solution / changelog)

Auxiliary tasks that use a named custom provider with api_mode: anthropic_messages now correctly route through the Anthropic Messages API instead of 404'ing against /chat/completions.

Root cause

Two gaps in the same flow:

  1. _get_named_custom_provider (hermes_cli/runtime_provider.py) — the new-style providers: dict branch dropped the api_mode field when materializing the provider entry. The legacy custom_providers: list branch already propagated it.
  2. resolve_provider_client (agent/auxiliary_client.py, named-custom block ~L1740) — ignored custom_entry["api_mode"] entirely and built a plain OpenAI client every time, only wrapping for Codex.

Changes

  • hermes_cli/runtime_provider.py: both match paths in the providers-dict branch now parse and attach api_mode via _parse_api_mode().
  • agent/auxiliary_client.py: the named-custom branch now mirrors _try_custom_endpoint()'s three-way dispatch:
    • anthropic_messagesAnthropicAuxiliaryClient (async: AsyncAnthropicAuxiliaryClient)
    • codex_responsesCodexAuxiliaryClient
    • otherwise → plain OpenAI Task-level api_mode override still wins over the provider entry's declared mode.
  • Tests: TestProvidersDictApiModeAnthropicMessages class (6 cases) covers propagation, invalid-mode drop, no-mode passthrough, both sync and async client types, and the full auxiliary.<task> chain.

Validation

Result
tests/agent/test_auxiliary_named_custom_providers.py25/25 ✓ (6 new)
Related aux tests (5 files, 190 tests)190/190 ✓

E2E: reproduced the exact config from the issue (Anthropic-compatible gateway + auxiliary.flush_memories.provider: myrelay); get_async_text_auxiliary_client("flush_memories") now returns AsyncAnthropicAuxiliaryClient wrapping the Anthropic SDK, pointing at {base_url}/v1/messages.

Closes #15033

Changed files

  • agent/auxiliary_client.py (modified, +40/-5)
  • hermes_cli/runtime_provider.py (modified, +10/-2)
  • tests/agent/test_auxiliary_named_custom_providers.py (modified, +155/-0)

Code Example

providers:
  myrelay:
    name: myrelay
    base_url: https://example-relay.test/anthropic
    key_env: MYRELAY_API_KEY
    api_mode: anthropic_messages
    default_model: claude-opus-4-7

auxiliary:
  flush_memories:
    provider: myrelay
    model: claude-sonnet-4.6

---

from agent.auxiliary_client import get_async_text_auxiliary_client
import asyncio

async def main():
    client, model = get_async_text_auxiliary_client('flush_memories')
    print('client:', type(client).__name__)
    print('base_url:', client.base_url)
    r = await client.chat.completions.create(
        model=model,
        messages=[{'role':'user','content':'hi'}],
        max_tokens=10,
    )

asyncio.run(main())

---

client: AsyncOpenAI
base_url: https://example-relay.test/anthropic/
NotFoundError: Error code: 404 - {'error': {'message': 'Unsupported Anthropic API endpoint ...', 'type': 'invalid_request_error', 'code': 404}}

---

# hermes_cli/runtime_provider.py
if requested_norm in {ep_name, name_norm, f"custom:{name_norm}"}:
    base_url = entry.get("api") or entry.get("url") or entry.get("base_url") or ""
    if base_url:
        return {
            "name": entry.get("name", ep_name),
            "base_url": base_url.strip(),
            "api_key": resolved_api_key,
            "model": entry.get("default_model", ""),
            # api_mode / default_headers / etc. not forwarded
        }

---

custom_entry = _get_named_custom_provider(provider)
if custom_entry:
    custom_base = custom_entry.get("base_url", "").strip()
    # ... resolves api key ...
    if custom_base:
        final_model = _normalize_resolved_model(...)
        client = OpenAI(api_key=custom_key, base_url=custom_base)   # unconditional OpenAI
        client = _wrap_if_needed(client, final_model, custom_base)  # only wraps for codex_responses
        return (_to_async_client(client, final_model) if async_mode
                else (client, final_model))

---

if custom_mode == "anthropic_messages":
    from agent.anthropic_adapter import build_anthropic_client
    real_client = build_anthropic_client(custom_key, custom_base)
    return (
        AnthropicAuxiliaryClient(real_client, model, custom_key, custom_base, is_oauth=False),
        model,
    )

---

return {
    "name": entry.get("name", ep_name),
    "base_url": base_url.strip(),
    "api_key": resolved_api_key,
    "model": entry.get("default_model", ""),
    "api_mode": (entry.get("api_mode") or entry.get("transport") or "").strip(),
}

---

custom_mode = (custom_entry.get("api_mode") or "").strip().lower()

if custom_mode == "anthropic_messages":
    from agent.anthropic_adapter import build_anthropic_client
    real_client = build_anthropic_client(custom_key, custom_base)
    sync_client = AnthropicAuxiliaryClient(real_client, final_model, custom_key, custom_base, is_oauth=False)
    if async_mode:
        return AsyncAnthropicAuxiliaryClient(sync_client), final_model
    return sync_client, final_model

if custom_mode == "codex_responses":
    real_client = OpenAI(api_key=custom_key, base_url=custom_base)
    return CodexAuxiliaryClient(real_client, final_model), final_model

# Default: chat_completions (existing behaviour)
client = OpenAI(api_key=custom_key, base_url=custom_base)
client = _wrap_if_needed(client, final_model, custom_base)
return (_to_async_client(client, final_model) if async_mode else (client, final_model))
RAW_BUFFERClick to expand / collapse

Named custom providers with api_mode: anthropic_messages fail with 404 "Unsupported Anthropic API endpoint" in auxiliary tasks

Summary

When a named custom provider in config.yaml under providers: declares api_mode: anthropic_messages, auxiliary task calls (session_search / skills_hub / approval / flush_memories / etc.) incorrectly route to an AsyncOpenAI client instead of the AnthropicAuxiliaryClient. The AsyncOpenAI client then POSTs to {base_url}/chat/completions, which for Anthropic-compatible gateways doesn't exist, producing a 404.

Tested on Hermes Agent v0.11.0 (2026.4.23).

Environment

  • Hermes Agent: v0.11.0 (2026.4.23)
  • Python: 3.11.14
  • Platform: Linux
  • Named custom provider target: an Anthropic-compatible gateway exposing /v1/messages

Reproduction

config.yaml:

providers:
  myrelay:
    name: myrelay
    base_url: https://example-relay.test/anthropic
    key_env: MYRELAY_API_KEY
    api_mode: anthropic_messages
    default_model: claude-opus-4-7

auxiliary:
  flush_memories:
    provider: myrelay
    model: claude-sonnet-4.6

Minimal repro:

from agent.auxiliary_client import get_async_text_auxiliary_client
import asyncio

async def main():
    client, model = get_async_text_auxiliary_client('flush_memories')
    print('client:', type(client).__name__)
    print('base_url:', client.base_url)
    r = await client.chat.completions.create(
        model=model,
        messages=[{'role':'user','content':'hi'}],
        max_tokens=10,
    )

asyncio.run(main())

Expected

Client should be AsyncAnthropicAuxiliaryClient (wrapping the Anthropic SDK), POSTing to {base_url}/v1/messages. Response content "Hi!" or similar.

Actual

client: AsyncOpenAI
base_url: https://example-relay.test/anthropic/
NotFoundError: Error code: 404 - {'error': {'message': 'Unsupported Anthropic API endpoint ...', 'type': 'invalid_request_error', 'code': 404}}

Direct curl to the same gateway with the native Anthropic path (POST {base_url}/v1/messages) succeeds — so the upstream gateway works fine. The AsyncOpenAI client silently appends /chat/completions → 404.

Root Cause — two separate issues

Bug 1: hermes_cli/runtime_provider.py::_get_named_custom_provider drops api_mode

File: hermes_cli/runtime_provider.py around line 287.

When materialising a named custom provider entry, the function returns only four fields — name, base_url, api_key, model — and silently drops api_mode:

# hermes_cli/runtime_provider.py
if requested_norm in {ep_name, name_norm, f"custom:{name_norm}"}:
    base_url = entry.get("api") or entry.get("url") or entry.get("base_url") or ""
    if base_url:
        return {
            "name": entry.get("name", ep_name),
            "base_url": base_url.strip(),
            "api_key": resolved_api_key,
            "model": entry.get("default_model", ""),
            # api_mode / default_headers / etc. not forwarded
        }

Bug 2: agent/auxiliary_client.py::resolve_provider_client ignores api_mode for named custom providers

File: agent/auxiliary_client.py around line 1748 (the "Named custom providers" branch):

custom_entry = _get_named_custom_provider(provider)
if custom_entry:
    custom_base = custom_entry.get("base_url", "").strip()
    # ... resolves api key ...
    if custom_base:
        final_model = _normalize_resolved_model(...)
        client = OpenAI(api_key=custom_key, base_url=custom_base)   # unconditional OpenAI
        client = _wrap_if_needed(client, final_model, custom_base)  # only wraps for codex_responses
        return (_to_async_client(client, final_model) if async_mode
                else (client, final_model))

Compare with the older _try_custom_endpoint path in the same file (around line 1155-1175) which does handle anthropic_messages correctly:

if custom_mode == "anthropic_messages":
    from agent.anthropic_adapter import build_anthropic_client
    real_client = build_anthropic_client(custom_key, custom_base)
    return (
        AnthropicAuxiliaryClient(real_client, model, custom_key, custom_base, is_oauth=False),
        model,
    )

The logic exists — it's just not reused from the Named-custom-providers branch.

Suggested Fix

Patch _get_named_custom_provider to preserve api_mode

return {
    "name": entry.get("name", ep_name),
    "base_url": base_url.strip(),
    "api_key": resolved_api_key,
    "model": entry.get("default_model", ""),
    "api_mode": (entry.get("api_mode") or entry.get("transport") or "").strip(),
}

Patch resolve_provider_client to honour api_mode on the named-custom branch

custom_mode = (custom_entry.get("api_mode") or "").strip().lower()

if custom_mode == "anthropic_messages":
    from agent.anthropic_adapter import build_anthropic_client
    real_client = build_anthropic_client(custom_key, custom_base)
    sync_client = AnthropicAuxiliaryClient(real_client, final_model, custom_key, custom_base, is_oauth=False)
    if async_mode:
        return AsyncAnthropicAuxiliaryClient(sync_client), final_model
    return sync_client, final_model

if custom_mode == "codex_responses":
    real_client = OpenAI(api_key=custom_key, base_url=custom_base)
    return CodexAuxiliaryClient(real_client, final_model), final_model

# Default: chat_completions (existing behaviour)
client = OpenAI(api_key=custom_key, base_url=custom_base)
client = _wrap_if_needed(client, final_model, custom_base)
return (_to_async_client(client, final_model) if async_mode else (client, final_model))

Impact

Any user with a named custom provider declaring api_mode: anthropic_messages (e.g. routing through Anthropic-compatible gateways, LiteLLM proxies, self-hosted shims) cannot use that provider for auxiliary tasks. They must fall back to chat_completions mode, losing native Anthropic features like prompt caching semantics, thinking/reasoning params, and native tool use schema.

Workaround

Configure the provider with api_mode: chat_completions against the gateway's OpenAI-compatible endpoint. Cost: loses protocol-native Anthropic features.

extent analysis

TL;DR

The most likely fix is to patch the _get_named_custom_provider and resolve_provider_client functions to preserve and honor the api_mode for named custom providers.

Guidance

  • Verify that the api_mode is being dropped in the _get_named_custom_provider function and add it to the returned dictionary.
  • Update the resolve_provider_client function to check the api_mode for named custom providers and use the correct client based on the mode.
  • Test the changes with a named custom provider that declares api_mode: anthropic_messages to ensure it works as expected.
  • Consider adding error handling for cases where the api_mode is not supported or the provider is not properly configured.

Example

# Patched _get_named_custom_provider function
return {
    "name": entry.get("name", ep_name),
    "base_url": base_url.strip(),
    "api_key": resolved_api_key,
    "model": entry.get("default_model", ""),
    "api_mode": (entry.get("api_mode") or entry.get("transport") or "").strip(),
}

# Patched resolve_provider_client function
custom_mode = (custom_entry.get("api_mode") or "").strip().lower()

if custom_mode == "anthropic_messages":
    from agent.anthropic_adapter import build_anthropic_client
    real_client = build_anthropic_client(custom_key, custom_base)
    sync_client = AnthropicAuxiliaryClient(real_client, final_model, custom_key, custom_base, is_oauth=False)
    if async_mode:
        return AsyncAnthropicAuxiliaryClient(sync_client), final_model
    return sync_client, final_model

Notes

The provided patches assume that the api_mode is being dropped in the _get_named_custom_provider function and that the resolve_provider_client function is not properly handling the api_mode for named custom providers. Additional testing

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 Named custom providers with api_mode: anthropic_messages fail with 404 in auxiliary tasks [1 pull requests, 1 comments, 2 participants]