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.
Named custom providers with
api_mode: anthropic_messagesfail with 404 "Unsupported Anthropic API endpoint" in auxiliary tasksSummary
When a named custom provider in
config.yamlunderproviders:declaresapi_mode: anthropic_messages, auxiliary task calls (session_search / skills_hub / approval / flush_memories / etc.) incorrectly route to anAsyncOpenAIclient instead of theAnthropicAuxiliaryClient. TheAsyncOpenAIclient 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
/v1/messagesReproduction
config.yaml:Minimal repro:
Expected
Client should be
AsyncAnthropicAuxiliaryClient(wrapping the Anthropic SDK), POSTing to{base_url}/v1/messages. Response content"Hi!"or similar.Actual
Direct curl to the same gateway with the native Anthropic path (
POST {base_url}/v1/messages) succeeds — so the upstream gateway works fine. TheAsyncOpenAIclient silently appends/chat/completions→ 404.Root Cause — two separate issues
Bug 1:
hermes_cli/runtime_provider.py::_get_named_custom_providerdropsapi_modeFile:
hermes_cli/runtime_provider.pyaround line 287.When materialising a named custom provider entry, the function returns only four fields —
name,base_url,api_key,model— and silently dropsapi_mode:Bug 2:
agent/auxiliary_client.py::resolve_provider_clientignoresapi_modefor named custom providersFile:
agent/auxiliary_client.pyaround line 1748 (the "Named custom providers" branch):Compare with the older
_try_custom_endpointpath in the same file (around line 1155-1175) which does handleanthropic_messagescorrectly:The logic exists — it's just not reused from the Named-custom-providers branch.
Suggested Fix
Patch
_get_named_custom_providerto preserveapi_modePatch
resolve_provider_clientto honourapi_modeon the named-custom branchImpact
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 tochat_completionsmode, losing native Anthropic features like prompt caching semantics,thinking/reasoning params, and native tool use schema.Workaround
Configure the provider with
api_mode: chat_completionsagainst the gateway's OpenAI-compatible endpoint. Cost: loses protocol-native Anthropic features.