Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion agent/chat_completion_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,16 @@ def try_activate_fallback(agent, reason: "FailoverReason | None" = None) -> bool
_fb_is_azure = agent._is_azure_openai_url(fb_base_url)
if fb_provider == "openai-codex":
fb_api_mode = "codex_responses"
elif fb_provider == "anthropic" or fb_base_url.rstrip("/").lower().endswith("/anthropic"):
elif (
fb_provider == "anthropic"
or fb_base_url.rstrip("/").lower().endswith("/anthropic")
or base_url_hostname(fb_base_url) == "api.anthropic.com"
):
# Custom providers (e.g. cron-anthropic) point at the native
# api.anthropic.com host with no "/anthropic" path suffix, so the
# name/suffix checks above miss them and they default to
# chat_completions → POST /v1/chat/completions → 404. Match the
# host the same way determine_api_mode() does on the primary path.
fb_api_mode = "anthropic_messages"
elif _fb_is_azure:
# Azure OpenAI serves gpt-5.x on /chat/completions — does NOT
Expand Down
29 changes: 29 additions & 0 deletions tests/run_agent/test_provider_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,35 @@ def test_resolves_key_env_for_fallback_provider(self):
assert agent._try_activate_fallback() is True
assert mock_rpc.call_args.kwargs["explicit_api_key"] == "env-secret"

def test_anthropic_host_custom_provider_uses_anthropic_messages(self):
"""A custom provider on the native api.anthropic.com host (no
"/anthropic" path suffix, name != "anthropic") must resolve to the
anthropic_messages wire protocol — not default to chat_completions,
which POSTs /v1/chat/completions and 404s. Mirrors the primary-path
determine_api_mode() host check."""
fbs = [
{
"provider": "cron-anthropic",
"model": "claude-sonnet-4-6",
"base_url": "https://api.anthropic.com",
"key_env": "MY_FALLBACK_KEY",
}
]
agent = _make_agent(fallback_model=fbs)
with (
patch.dict("os.environ", {"MY_FALLBACK_KEY": "env-secret"}, clear=False),
patch(
"agent.auxiliary_client.resolve_provider_client",
return_value=(
_mock_client(base_url="https://api.anthropic.com"),
"claude-sonnet-4-6",
),
),
patch("hermes_cli.model_normalize.normalize_model_for_provider", side_effect=lambda m, p: m),
):
assert agent._try_activate_fallback() is True
assert agent.api_mode == "anthropic_messages"


# ── Pool-rotation vs fallback gating (#11314) ────────────────────────────

Expand Down