mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
fix(delegation): honor api_mode + auto-detect anthropic_messages URLs (#26824)
Subagent delegation hardcoded api_mode='chat_completions' for any delegation.base_url that didn't match three specific hostnames (chatgpt.com, api.anthropic.com, api.kimi.com/coding), and never read delegation.api_mode from config. Azure AI Foundry's https://foundry.services.ai.azure.com/anthropic endpoint fell through and got chat_completions, causing 404s on every delegate_task call. The main agent already handles this correctly via the shared _detect_api_mode_for_url() helper (anything ending in /anthropic → anthropic_messages); delegation reimplemented its own narrower check. Reuse the shared detector and honor an explicit delegation.api_mode when set so users can also force the transport on non-standard endpoints the URL heuristic can't classify. Fixes #10213. Co-authored-by: HiddenPuppy <HiddenPuppy@users.noreply.github.com>
This commit is contained in:
parent
74d0b392e7
commit
c445f48b78
3 changed files with 76 additions and 1 deletions
|
|
@ -1146,6 +1146,10 @@ DEFAULT_CONFIG = {
|
||||||
"provider": "", # e.g. "openrouter" (empty = inherit parent provider + credentials)
|
"provider": "", # e.g. "openrouter" (empty = inherit parent provider + credentials)
|
||||||
"base_url": "", # direct OpenAI-compatible endpoint for subagents
|
"base_url": "", # direct OpenAI-compatible endpoint for subagents
|
||||||
"api_key": "", # API key for delegation.base_url (falls back to OPENAI_API_KEY)
|
"api_key": "", # API key for delegation.base_url (falls back to OPENAI_API_KEY)
|
||||||
|
"api_mode": "", # wire protocol for delegation.base_url: "chat_completions",
|
||||||
|
# "codex_responses", or "anthropic_messages". Empty = auto-detect
|
||||||
|
# from URL (e.g. /anthropic suffix → anthropic_messages). Set this
|
||||||
|
# explicitly for non-standard endpoints the heuristic can't detect.
|
||||||
# When delegate_task narrows child toolsets explicitly, preserve any
|
# When delegate_task narrows child toolsets explicitly, preserve any
|
||||||
# MCP toolsets the parent already has enabled. On by default so
|
# MCP toolsets the parent already has enabled. On by default so
|
||||||
# narrowing (e.g. toolsets=["web","browser"]) expresses "I want these
|
# narrowing (e.g. toolsets=["web","browser"]) expresses "I want these
|
||||||
|
|
|
||||||
|
|
@ -890,6 +890,63 @@ class TestDelegationCredentialResolution(unittest.TestCase):
|
||||||
self.assertEqual(creds["api_key"], "local-key")
|
self.assertEqual(creds["api_key"], "local-key")
|
||||||
self.assertEqual(creds["api_mode"], "chat_completions")
|
self.assertEqual(creds["api_mode"], "chat_completions")
|
||||||
|
|
||||||
|
def test_direct_endpoint_auto_detects_anthropic_messages_suffix(self):
|
||||||
|
# Issue #10213: Azure AI Foundry exposes Anthropic-compatible models at
|
||||||
|
# a /anthropic URL suffix. Subagents must pick anthropic_messages
|
||||||
|
# automatically, matching the main agent's runtime resolver.
|
||||||
|
parent = _make_mock_parent(depth=0)
|
||||||
|
cfg = {
|
||||||
|
"model": "claude-opus-4-6",
|
||||||
|
"provider": "custom",
|
||||||
|
"base_url": "https://myfoundry.services.ai.azure.com/anthropic",
|
||||||
|
"api_key": "foundry-key",
|
||||||
|
}
|
||||||
|
creds = _resolve_delegation_credentials(cfg, parent)
|
||||||
|
self.assertEqual(creds["provider"], "custom")
|
||||||
|
self.assertEqual(creds["base_url"], "https://myfoundry.services.ai.azure.com/anthropic")
|
||||||
|
self.assertEqual(creds["api_key"], "foundry-key")
|
||||||
|
self.assertEqual(creds["api_mode"], "anthropic_messages")
|
||||||
|
|
||||||
|
def test_direct_endpoint_honors_explicit_api_mode(self):
|
||||||
|
# When delegation.api_mode is set explicitly, it overrides URL-based
|
||||||
|
# detection so users can force a transport on non-standard endpoints.
|
||||||
|
parent = _make_mock_parent(depth=0)
|
||||||
|
cfg = {
|
||||||
|
"model": "claude-opus-4-6",
|
||||||
|
"provider": "custom",
|
||||||
|
"base_url": "https://proxy.example.com/v1",
|
||||||
|
"api_key": "proxy-key",
|
||||||
|
"api_mode": "anthropic_messages",
|
||||||
|
}
|
||||||
|
creds = _resolve_delegation_credentials(cfg, parent)
|
||||||
|
self.assertEqual(creds["api_mode"], "anthropic_messages")
|
||||||
|
|
||||||
|
def test_direct_endpoint_explicit_api_mode_overrides_url_detection(self):
|
||||||
|
# Explicit api_mode in config always wins over auto-detection.
|
||||||
|
parent = _make_mock_parent(depth=0)
|
||||||
|
cfg = {
|
||||||
|
"model": "claude-opus-4-6",
|
||||||
|
"provider": "custom",
|
||||||
|
"base_url": "https://myfoundry.services.ai.azure.com/anthropic",
|
||||||
|
"api_key": "foundry-key",
|
||||||
|
"api_mode": "chat_completions",
|
||||||
|
}
|
||||||
|
creds = _resolve_delegation_credentials(cfg, parent)
|
||||||
|
self.assertEqual(creds["api_mode"], "chat_completions")
|
||||||
|
|
||||||
|
def test_direct_endpoint_invalid_api_mode_falls_back_to_detection(self):
|
||||||
|
# An invalid api_mode string must not break detection; fall back to URL heuristic.
|
||||||
|
parent = _make_mock_parent(depth=0)
|
||||||
|
cfg = {
|
||||||
|
"model": "claude-opus-4-6",
|
||||||
|
"provider": "custom",
|
||||||
|
"base_url": "https://myfoundry.services.ai.azure.com/anthropic",
|
||||||
|
"api_key": "foundry-key",
|
||||||
|
"api_mode": "garbage",
|
||||||
|
}
|
||||||
|
creds = _resolve_delegation_credentials(cfg, parent)
|
||||||
|
self.assertEqual(creds["api_mode"], "anthropic_messages")
|
||||||
|
|
||||||
def test_direct_endpoint_returns_none_api_key_when_not_configured(self):
|
def test_direct_endpoint_returns_none_api_key_when_not_configured(self):
|
||||||
# When base_url is set without api_key, api_key should be None so
|
# When base_url is set without api_key, api_key should be None so
|
||||||
# _build_child_agent inherits the parent's key (effective_api_key = override or parent).
|
# _build_child_agent inherits the parent's key (effective_api_key = override or parent).
|
||||||
|
|
|
||||||
|
|
@ -2362,6 +2362,7 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
||||||
configured_provider = str(cfg.get("provider") or "").strip() or None
|
configured_provider = str(cfg.get("provider") or "").strip() or None
|
||||||
configured_base_url = str(cfg.get("base_url") or "").strip() or None
|
configured_base_url = str(cfg.get("base_url") or "").strip() or None
|
||||||
configured_api_key = str(cfg.get("api_key") or "").strip() or None
|
configured_api_key = str(cfg.get("api_key") or "").strip() or None
|
||||||
|
configured_api_mode = str(cfg.get("api_mode") or "").strip().lower() or None
|
||||||
|
|
||||||
if configured_base_url:
|
if configured_base_url:
|
||||||
# When delegation.api_key is not set, return None so _build_child_agent
|
# When delegation.api_key is not set, return None so _build_child_agent
|
||||||
|
|
@ -2372,9 +2373,17 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
||||||
# callers to duplicate the key under delegation.api_key.
|
# callers to duplicate the key under delegation.api_key.
|
||||||
api_key = configured_api_key # None → inherited from parent in _build_child_agent
|
api_key = configured_api_key # None → inherited from parent in _build_child_agent
|
||||||
|
|
||||||
|
# Use the shared URL-based api_mode detector (same path the main agent's
|
||||||
|
# runtime resolver uses) so Anthropic-compatible direct endpoints with a
|
||||||
|
# /anthropic suffix — Azure AI Foundry, MiniMax, Zhipu GLM, LiteLLM
|
||||||
|
# proxies — pick the right transport automatically. Without this,
|
||||||
|
# subagents would default to chat_completions and hit 404s on endpoints
|
||||||
|
# that only speak the Anthropic Messages protocol. Fixes #10213.
|
||||||
|
from hermes_cli.runtime_provider import _detect_api_mode_for_url
|
||||||
|
|
||||||
base_lower = configured_base_url.lower()
|
base_lower = configured_base_url.lower()
|
||||||
provider = "custom"
|
provider = "custom"
|
||||||
api_mode = "chat_completions"
|
api_mode = _detect_api_mode_for_url(configured_base_url) or "chat_completions"
|
||||||
if (
|
if (
|
||||||
base_url_hostname(configured_base_url) == "chatgpt.com"
|
base_url_hostname(configured_base_url) == "chatgpt.com"
|
||||||
and "/backend-api/codex" in base_lower
|
and "/backend-api/codex" in base_lower
|
||||||
|
|
@ -2388,6 +2397,11 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
|
||||||
provider = "custom"
|
provider = "custom"
|
||||||
api_mode = "anthropic_messages"
|
api_mode = "anthropic_messages"
|
||||||
|
|
||||||
|
# Explicit delegation.api_mode in config always wins. Lets users force
|
||||||
|
# a transport for non-standard endpoints the URL heuristic can't detect.
|
||||||
|
if configured_api_mode in {"chat_completions", "codex_responses", "anthropic_messages"}:
|
||||||
|
api_mode = configured_api_mode
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"model": configured_model,
|
"model": configured_model,
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue