fix: fallback entries inherit a named provider's declared transport

A fallback_providers entry naming a `providers.<name>` block (or
`custom:<name>`) without its own api_mode was re-detected from the
resolved host: an Anthropic-Messages proxy on a plain host or a
Responses-only relay behind a generic gateway landed on chat_completions
while resolve_provider_client had already built the declared client
(#33062 bottom thread, #81932 provider-level `transport:`). The hint pass
now reads the named block's api_mode/transport as explicit, and entry-level
`transport:` is accepted as an alias of `api_mode` with the same
canonicalisation the providers block uses (`responses` -> codex_responses).

Fixes #33062
Fixes #81932
This commit is contained in:
teknium1
2026-09-18 23:35:12 -07:00
committed by Teknium
parent 45356370b0
commit 8f1542422b
3 changed files with 38 additions and 2 deletions

View File

@@ -1736,9 +1736,19 @@ def _fallback_api_mode_hint(fb: dict, fb_provider: str, fb_base_url_hint: Option
rewrites a dual-surface /anthropic base to /v1, losing the Anthropic wire signal. An explicit
``api_mode`` always wins (even "chat_completions") and suppresses later re-detection;
``provider: anthropic`` without a base_url still resolves to anthropic_messages."""
explicit = str(fb.get("api_mode") or "").strip()
from hermes_cli.runtime_provider import _get_named_custom_provider, _parse_api_mode
# Entries accept the same ``api_mode`` / ``transport`` spellings as ``providers.<name>``.
explicit = _parse_api_mode(fb.get("api_mode") or fb.get("transport"))
if explicit:
return True, explicit
# A named ``providers.<name>`` block declares its wire once (``api_mode``/``transport``); a
# fallback entry naming that provider inherits it instead of being re-detected from the host
# (#33062, #81932: an Anthropic-Messages or Responses-only relay on a plain host was downgraded
# to chat_completions while resolve_provider_client had already built the declared client).
if fb_provider and fb_provider not in {"custom", "moa"}:
declared = (_get_named_custom_provider(fb_provider) or {}).get("api_mode")
if declared:
return True, declared
if fb_provider == "anthropic" or (fb_base_url_hint and _is_anthropic_wire_url(fb_base_url_hint)):
return False, "anthropic_messages"
return False, "chat_completions"

View File

@@ -178,6 +178,30 @@ class TestOriginalUrlDetection:
assert agent.api_mode == "chat_completions"
class TestNamedProviderDeclaredWire:
"""A fallback entry naming a ``providers.<name>`` block inherits the block's declared
``api_mode``/``transport`` instead of host re-detection (#33062, #81932)."""
def test_named_block_anthropic_messages_inherited_on_plain_host(self):
fbs = [{"provider": "custom:ai-proxy", "model": "claude-4.7-opus"}]
agent = _make_agent(fallback_model=fbs)
with patch(
"hermes_cli.runtime_provider._get_named_custom_provider",
return_value={"name": "ai-proxy", "base_url": "https://ai-proxy.example.com",
"api_key": "k", "api_mode": "anthropic_messages"},
):
mock_rpc = _activate(agent, "https://ai-proxy.example.com", "claude-4.7-opus")
assert agent.api_mode == "anthropic_messages"
assert mock_rpc.call_args.kwargs["api_mode"] == "anthropic_messages"
def test_entry_transport_alias_is_honored(self):
fbs = [{"provider": "custom", "model": "custom/responses", "base_url": "https://gateway.example.com/v1",
"api_key": "k", "transport": "responses"}]
agent = _make_agent(fallback_model=fbs)
_activate(agent, "https://gateway.example.com/v1", "custom/responses")
assert agent.api_mode == "codex_responses"
class TestPlainFallbackUnchanged:
def test_plain_openrouter_fallback_stays_chat_completions(self):
fbs = [{

View File

@@ -1676,9 +1676,11 @@ fallback_providers:
- provider: anthropic
model: claude-sonnet-4
# base_url: http://localhost:8000/v1 # optional, for custom endpoints
# api_mode: chat_completions # optional override
# api_mode: chat_completions # optional override (`transport:` is an accepted alias)
```
An entry that names a `providers.<name>` block (`provider: my-relay` or `provider: custom:my-relay`) inherits that block's `transport` / `api_mode` when the entry sets none, so a Responses-only or Anthropic-Messages relay keeps its declared wire on fallback. Set `api_mode` on the entry to override it.
The legacy single-pair `fallback_model:` dict is still accepted for back-compat:
```yaml