fix(runtime): fall back to the provider's declared transport, not chat_completions

The P1 from the enterprise data-residency report: with
model.base_url=https://us.api.openai.com/v1, every tool-calling turn 400'd
('Function tools with reasoning_effort are not supported ... use
/v1/responses') because the runtime resolvers hardcoded
api_mode=chat_completions and consulted URL detection only. openai-api
declares codex_responses in its overlay; the declaration was never
consulted, so any OpenAI host that wasn't literally api.openai.com landed
on the wrong wire protocol.

New _fallback_api_mode(provider, base_url, model): URL detection first
(host-mandated wire shapes keep priority), then
providers.determine_api_mode() (the provider's declared transport), then
chat_completions only for genuinely unknown providers. All three runtime
fallback sites route through it: the pool-entry path, the explicit-runtime
path, and the API-key-provider path, so the lanes cannot drift apart.

Blast radius beyond openai-api: minimax, minimax-cn, and copilot-acp were
the other overlays whose declared non-chat transport fell through to
chat_completions on the same paths (same latent bug class). openrouter is
unaffected (declares openai_chat). _detect_api_mode_for_url also now uses
the shared official-host predicate, so regional hosts detect as
codex_responses on the direct-URL lane too.
This commit is contained in:
Victor Kyriazakos 2026-07-30 19:03:32 +00:00 committed by Teknium
parent 564e9b90af
commit 33a2f29a63
2 changed files with 144 additions and 18 deletions

View File

@ -44,6 +44,7 @@ from hermes_cli.config import (
)
from hermes_cli.providers import custom_provider_aliases, custom_provider_slug
from hermes_constants import OPENROUTER_BASE_URL
from hermes_cli.providers import is_official_openai_host
from utils import base_url_host_matches, base_url_hostname, env_int
@ -124,7 +125,11 @@ def _detect_api_mode_for_url(base_url: str) -> Optional[str]:
hostname = base_url_hostname(base_url)
if hostname == "api.x.ai":
return "codex_responses"
if hostname == "api.openai.com":
# Official OpenAI host family: canonical api.openai.com plus the
# data-residency regional hosts (us./eu.api.openai.com). Same API
# surface, same Responses-API mandate. Shared predicate — see
# providers.is_official_openai_host for the spoof-rejection contract.
if is_official_openai_host(base_url):
return "codex_responses"
# Direct native Anthropic host: realign with providers.determine_api_mode,
# which already maps this host to anthropic_messages. The exact-hostname
@ -140,6 +145,31 @@ def _detect_api_mode_for_url(base_url: str) -> Optional[str]:
return None
def _fallback_api_mode(provider: str, base_url: str, model: str = "") -> str:
"""Resolve api_mode when no explicit/persisted mode applies.
Precedence: URL detection (host-mandated wire shapes) first, then the
transport the provider overlay itself declares via
``providers.determine_api_mode`` which already handles host mandates,
dual-wire providers, and the registry transport map and only then the
``chat_completions`` default for genuinely unknown providers/endpoints.
Before this helper the runtime paths consulted URL detection ONLY and
silently landed reasoning providers on ``chat_completions`` whenever the
hostname wasn't literally recognized. That is how ``openai-api`` pointed
at OpenAI's data-residency hosts (``us.api.openai.com``) 400'd on every
tool-calling turn: the provider declares ``codex_responses`` but the
declaration was never consulted. Same latent class covered the other
non-chat overlays (MiniMax family, copilot-acp).
"""
detected = _detect_api_mode_for_url(base_url)
if detected:
return detected
from hermes_cli.providers import determine_api_mode
return determine_api_mode(provider, base_url, model) or "chat_completions"
def _resolve_plain_custom_api_mode(model_cfg: Dict[str, Any], base_url: str) -> str:
"""Resolve api_mode for legacy/plain ``provider: custom`` endpoints.
@ -519,12 +549,10 @@ def _resolve_runtime_from_pool_entry(
elif configured_mode and _provider_supports_explicit_api_mode(provider, configured_provider):
api_mode = configured_mode
else:
# Auto-detect Anthropic-compatible endpoints (/anthropic suffix,
# Kimi /coding, api.openai.com → codex_responses, api.x.ai →
# codex_responses).
detected = _detect_api_mode_for_url(base_url)
if detected:
api_mode = detected
# URL detection first (Anthropic /anthropic suffix, Kimi /coding,
# official OpenAI hosts → codex_responses, api.x.ai →
# codex_responses), then the provider's own declared transport.
api_mode = _fallback_api_mode(provider, base_url, effective_model)
# OpenCode base URLs end with /v1 for OpenAI-compatible models, but the
# Anthropic SDK prepends its own /v1/messages to the base_url. Normalize
@ -1588,11 +1616,11 @@ def _resolve_explicit_runtime(
if configured_mode:
api_mode = configured_mode
else:
# Auto-detect from URL (Anthropic /anthropic suffix,
# api.openai.com → Responses, Kimi /coding, etc.).
detected = _detect_api_mode_for_url(base_url)
if detected:
api_mode = detected
# URL detection first, then the provider's declared transport
# (fixes regional OpenAI hosts and other non-chat overlays).
api_mode = _fallback_api_mode(
provider, base_url, target_model or model_cfg.get("default", "")
)
return {
"provider": provider,
@ -2183,12 +2211,12 @@ def resolve_runtime_provider(
elif configured_mode and _provider_supports_explicit_api_mode(provider, configured_provider):
api_mode = configured_mode
else:
# Auto-detect Anthropic-compatible endpoints by URL convention
# (e.g. https://api.minimax.io/anthropic, https://dashscope.../anthropic)
# plus api.openai.com → codex_responses and api.x.ai → codex_responses.
detected = _detect_api_mode_for_url(base_url)
if detected:
api_mode = detected
# URL detection first (e.g. https://api.minimax.io/anthropic,
# official OpenAI hosts → codex_responses, api.x.ai →
# codex_responses), then the provider's declared transport.
api_mode = _fallback_api_mode(
provider, base_url, target_model or model_cfg.get("default", "")
)
# Normalize the /v1 suffix for OpenCode by API mode (see comment above).
if provider in {"opencode-zen", "opencode-go"}:
from hermes_cli.models import normalize_opencode_base_url

View File

@ -0,0 +1,98 @@
"""Runtime transport precedence: declared provider transport is the fallback.
The Coatue data-residency report (2026-07): pointing ``openai-api`` at
``us.api.openai.com`` silently fell back to ``chat_completions`` every
tool-calling turn 400'd — because the runtime resolvers defaulted to
``chat_completions`` and consulted URL detection only, never the transport
the provider overlay itself declares.
Contract pinned here: when URL detection has no opinion, the runtime falls
back to ``providers.determine_api_mode(provider, base_url, model)`` (the
provider's declared transport), and only lands on ``chat_completions`` for
genuinely unknown providers/endpoints. Covers the explicit-runtime path and
the API-key-provider path; the pool-entry path shares the same helper.
"""
from __future__ import annotations
from unittest.mock import patch as mock_patch
import pytest
from hermes_cli.runtime_provider import _fallback_api_mode
class TestFallbackApiMode:
@pytest.mark.parametrize(
"base_url",
[
"https://api.openai.com/v1",
"https://us.api.openai.com/v1",
"https://eu.api.openai.com/v1",
],
)
def test_openai_api_official_hosts_resolve_codex_responses(self, base_url):
assert _fallback_api_mode("openai-api", base_url) == "codex_responses"
def test_openai_api_unknown_custom_proxy_still_uses_declared_transport(self):
# Explicitly selected openai-api against a custom proxy keeps the
# provider's declared transport (mirrors determine_api_mode semantics;
# host identity is a separate question from provider selection).
assert (
_fallback_api_mode("openai-api", "https://proxy.corp.test/v1")
== "codex_responses"
)
def test_lookalike_host_is_not_treated_as_official(self):
# The spoof host must not be detected AS OpenAI by the URL lane —
# the provider-declared transport may still apply, but host-derived
# detection must return None for it.
from hermes_cli.runtime_provider import _detect_api_mode_for_url
assert _detect_api_mode_for_url("https://api.openai.com.attacker.test/v1") is None
def test_openrouter_stays_chat_completions(self):
assert _fallback_api_mode("openrouter", "https://openrouter.ai/api/v1") == "chat_completions"
def test_minimax_declared_anthropic_transport_honored(self):
# Same latent bug class: minimax declares an Anthropic-compatible
# transport but previously fell back to chat_completions when the
# URL carried no /anthropic hint.
from hermes_cli.providers import determine_api_mode
expected = determine_api_mode("minimax", "https://api.minimax.io")
assert _fallback_api_mode("minimax", "https://api.minimax.io") == expected
assert expected != "chat_completions" or expected == determine_api_mode("minimax", "")
def test_unknown_provider_defaults_chat_completions(self):
assert _fallback_api_mode("some-unknown", "https://example.test/v1") == "chat_completions"
def test_url_detection_wins_over_provider_declaration(self):
# /anthropic suffix on any provider routes anthropic_messages —
# URL detection stays the higher-priority signal.
assert (
_fallback_api_mode("openai-api", "https://gateway.test/anthropic")
== "anthropic_messages"
)
class TestExplicitRuntimeIntegration:
"""The explicit-runtime path resolves regional OpenAI to codex_responses."""
def test_explicit_openai_api_regional_host(self):
from hermes_cli.runtime_provider import _resolve_explicit_runtime
with mock_patch(
"hermes_cli.runtime_provider._get_model_config",
return_value={"provider": "openai-api", "default": "gpt-5.6-terra"},
):
result = _resolve_explicit_runtime(
provider="openai-api",
requested_provider="openai-api",
explicit_api_key="sk-test",
explicit_base_url="https://us.api.openai.com/v1",
model_cfg={"provider": "openai-api", "default": "gpt-5.6-terra"},
)
assert result is not None
assert result["api_mode"] == "codex_responses"
assert result["base_url"] == "https://us.api.openai.com/v1"