Skip to content
Open
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
99 changes: 99 additions & 0 deletions tests/tools/test_delegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,105 @@ def test_provider_resolves_but_no_api_key_raises(self, mock_resolve):
_resolve_delegation_credentials(cfg, parent)
self.assertIn("no API key", str(ctx.exception))

@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_provider_path_honors_explicit_api_mode(self, mock_resolve):
"""delegation.api_mode is documented alongside delegation.provider; the provider
branch must honor it just like the base_url branch does ("always wins")."""
mock_resolve.return_value = {
"provider": "minimax",
"base_url": "https://api.minimax.io/v1",
"api_key": "k",
"api_mode": "chat_completions",
}
parent = _make_mock_parent(depth=0)
cfg = {"model": "m", "provider": "minimax", "api_mode": "anthropic_messages"}
creds = _resolve_delegation_credentials(cfg, parent)
self.assertEqual(creds["api_mode"], "anthropic_messages")

@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_provider_path_invalid_api_mode_keeps_runtime_value(self, mock_resolve):
"""An unsupported delegation.api_mode keeps the provider-resolved transport and
warns — never a silent drop, never an incoherent transport."""
mock_resolve.return_value = {
"provider": "openrouter",
"base_url": "https://openrouter.ai/api/v1",
"api_key": "k",
"api_mode": "chat_completions",
}
parent = _make_mock_parent(depth=0)
cfg = {"model": "m", "provider": "openrouter", "api_mode": "garbage"}
with self.assertLogs("tools.delegate_tool", level="WARNING") as logs:
creds = _resolve_delegation_credentials(cfg, parent)
self.assertEqual(creds["api_mode"], "chat_completions")
self.assertTrue(any("not a supported override" in m for m in logs.output))

@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_provider_path_native_sdk_ignores_api_mode(self, mock_resolve):
"""A native-SDK provider's wire protocol is fixed by its bundle; an explicit
delegation.api_mode cannot break it (mirrors the base_url-branch exclusion)."""
mock_resolve.return_value = {
"provider": "bedrock",
"base_url": None,
"api_key": "k",
"api_mode": "bedrock_converse",
}
parent = _make_mock_parent(depth=0)
cfg = {"model": "m", "provider": "bedrock", "api_mode": "chat_completions"}
with self.assertLogs("tools.delegate_tool", level="WARNING") as logs:
creds = _resolve_delegation_credentials(cfg, parent)
self.assertEqual(creds["api_mode"], "bedrock_converse")
self.assertTrue(any("native-SDK" in m for m in logs.output))

@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_provider_path_bedrock_alias_still_excluded(self, mock_resolve):
"""A registered alias (aws -> bedrock) resolves to a canonical native-SDK
provider; the api_mode exclusion keys on the RESOLVED provider, so an override
cannot become valid merely because an alias spelling was configured."""
mock_resolve.return_value = {
"provider": "bedrock", # canonical, resolved from the "aws" alias
"base_url": None,
"api_key": "k",
"api_mode": "bedrock_converse",
}
parent = _make_mock_parent(depth=0)
cfg = {"model": "m", "provider": "aws", "api_mode": "chat_completions"}
with self.assertLogs("tools.delegate_tool", level="WARNING") as logs:
creds = _resolve_delegation_credentials(cfg, parent)
self.assertEqual(creds["api_mode"], "bedrock_converse")
self.assertTrue(any("native-SDK" in m for m in logs.output))

@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_provider_path_vertex_alias_still_excluded(self, mock_resolve):
"""Same for vertex aliases (google-vertex -> vertex): resolved-canonical
exclusion, override ignored, loud warning."""
mock_resolve.return_value = {
"provider": "vertex", # canonical, resolved from the "google-vertex" alias
"base_url": "https://aiplatform.googleapis.com",
"api_key": "k",
"api_mode": "chat_completions",
}
parent = _make_mock_parent(depth=0)
cfg = {"model": "m", "provider": "google-vertex", "api_mode": "anthropic_messages"}
with self.assertLogs("tools.delegate_tool", level="WARNING") as logs:
creds = _resolve_delegation_credentials(cfg, parent)
self.assertEqual(creds["api_mode"], "chat_completions")
self.assertTrue(any("native-SDK" in m for m in logs.output))

@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_provider_path_no_api_mode_unchanged(self, mock_resolve):
"""Without delegation.api_mode the provider-resolved transport flows through
untouched — zero behavior change for existing configs."""
mock_resolve.return_value = {
"provider": "openrouter",
"base_url": "https://openrouter.ai/api/v1",
"api_key": "k",
"api_mode": "codex_responses",
}
parent = _make_mock_parent(depth=0)
cfg = {"model": "m", "provider": "openrouter"}
creds = _resolve_delegation_credentials(cfg, parent)
self.assertEqual(creds["api_mode"], "codex_responses")

@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_named_custom_provider_preserves_provider_name(self, mock_resolve):
"""Named custom provider (e.g. crof.ai) resolves to 'custom' at runtime level
Expand Down
39 changes: 38 additions & 1 deletion tools/delegate_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -3591,12 +3591,49 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
f"Set the appropriate environment variable or run 'hermes auth'."
)

# Explicit delegation.api_mode in config wins here too, mirroring the base_url
# branch above ("always wins"). Without this, the documented provider-path
# example (delegation.provider + delegation.api_mode) was a silent no-op and
# the child always got the runtime-resolved transport. Native-SDK providers
# are excluded (their wire protocol is fixed by the bundle), and an
# unsupported value keeps the resolved mode — loudly, never silently.
#
# The exclusion is keyed on the RESOLVED canonical provider, not the configured
# spelling: registered aliases (bedrock: aws/aws-bedrock/amazon-bedrock/amazon;
# vertex: google-vertex/vertex-ai/gcp-vertex) resolve to a canonical native-SDK
# provider, and an override must not become valid merely because an alias was
# used. The configured-spelling check stays as a fallback for the rare path
# where resolution reports "custom" but the spelling itself is canonical.
resolved_provider_name = str(runtime.get("provider") or "").strip().lower()
_is_native_resolved = (
resolved_provider_name in _NATIVE_SDK_PROVIDERS or _is_native_sdk_provider
)
resolved_api_mode = runtime.get("api_mode")
if configured_api_mode:
if _is_native_resolved:
logger.warning(
"delegation.api_mode '%s' is ignored for native-SDK provider '%s' "
"(its wire protocol is fixed)",
configured_api_mode,
resolved_provider_name or configured_provider,
)
elif configured_api_mode in {"chat_completions", "codex_responses", "anthropic_messages"}:
resolved_api_mode = configured_api_mode
else:
logger.warning(
"delegation.api_mode '%s' is not a supported override "
"(chat_completions, codex_responses, anthropic_messages); "
"keeping provider-resolved '%s'",
configured_api_mode,
resolved_api_mode,
)

return {
"model": configured_model or runtime.get("model") or None,
"provider": configured_provider if runtime.get("provider") == _RUNTIME_PROVIDER_CUSTOM else runtime.get("provider"),
"base_url": runtime.get("base_url"),
"api_key": api_key,
"api_mode": runtime.get("api_mode"),
"api_mode": resolved_api_mode,
"request_overrides": dict(runtime.get("request_overrides") or {}),
"max_output_tokens": runtime.get("max_output_tokens"),
"command": runtime.get("command"),
Expand Down
2 changes: 1 addition & 1 deletion website/docs/user-guide/features/delegation.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ delegation:
# orchestrator_enabled: true # Disable to force all children to leaf role.
model: "google/gemini-3-flash-preview" # Optional provider/model override
provider: "openrouter" # Optional built-in provider
api_mode: anthropic_messages # optional; auto-detected from base_url for anthropic_messages endpoints
api_mode: anthropic_messages # Optional wire-protocol override ("chat_completions", "codex_responses", or "anthropic_messages"); overrides the provider-resolved transport. Ignored for native-SDK providers (bedrock/vertex/google).

# Or use a direct custom endpoint instead of provider:
delegation:
Expand Down
Loading