Skip to content

Named custom providers with api_mode: anthropic_messages fail with 404 in auxiliary tasks #15033

Description

@SMouuu

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High — major feature broken, no workaroundarea/configConfig system, migrations, profilescomp/agentCore agent runtime: loop, agent_init, prompt builder, context-compression, responses endpointtype/bugSomething isn't working

    Type

    No type

    Fields

    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