Skip to content
Merged
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
15 changes: 9 additions & 6 deletions tests/tools/test_delegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,18 +821,21 @@ def test_direct_endpoint_uses_configured_base_url_and_api_key(self):
self.assertEqual(creds["api_key"], "local-key")
self.assertEqual(creds["api_mode"], "chat_completions")

def test_direct_endpoint_falls_back_to_openai_api_key_env(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
# _build_child_agent inherits the parent's key (effective_api_key = override or parent).
parent = _make_mock_parent(depth=0)
cfg = {
"model": "qwen2.5-coder",
"base_url": "http://localhost:1234/v1",
}
with patch.dict(os.environ, {"OPENAI_API_KEY": "env-openai-key"}, clear=False):
creds = _resolve_delegation_credentials(cfg, parent)
self.assertEqual(creds["api_key"], "env-openai-key")
self.assertIsNone(creds["api_key"])
self.assertEqual(creds["provider"], "custom")

def test_direct_endpoint_does_not_fall_back_to_openrouter_api_key_env(self):
def test_direct_endpoint_no_raise_when_only_provider_env_key_present(self):
# Even if OPENAI_API_KEY is absent, no ValueError — _build_child_agent uses parent key.
parent = _make_mock_parent(depth=0)
cfg = {
"model": "qwen2.5-coder",
Expand All @@ -846,9 +849,9 @@ def test_direct_endpoint_does_not_fall_back_to_openrouter_api_key_env(self):
},
clear=False,
):
with self.assertRaises(ValueError) as ctx:
_resolve_delegation_credentials(cfg, parent)
self.assertIn("OPENAI_API_KEY", str(ctx.exception))
creds = _resolve_delegation_credentials(cfg, parent)
self.assertIsNone(creds["api_key"])
self.assertEqual(creds["provider"], "custom")

@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_nous_provider_resolves_nous_credentials(self, mock_resolve):
Expand Down
29 changes: 18 additions & 11 deletions tools/delegate_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2237,11 +2237,17 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
"""Resolve credentials for subagent delegation.

If ``delegation.base_url`` is configured, subagents use that direct
OpenAI-compatible endpoint. Otherwise, if ``delegation.provider`` is
configured, the full credential bundle (base_url, api_key, api_mode,
provider) is resolved via the runtime provider system — the same path used
by CLI/gateway startup. This lets subagents run on a completely different
provider:model pair.
OpenAI-compatible endpoint. ``delegation.api_key`` overrides the key; when
omitted, ``api_key`` is returned as ``None`` so ``_build_child_agent``
inherits the parent agent's key (``effective_api_key = override_api_key or
parent_api_key``). This lets providers that store their key outside
``OPENAI_API_KEY`` (e.g. ``MINIMAX_API_KEY``, ``DASHSCOPE_API_KEY``) work
without a duplicate config entry.

Otherwise, if ``delegation.provider`` is configured, the full credential
bundle (base_url, api_key, api_mode, provider) is resolved via the runtime
provider system — the same path used by CLI/gateway startup. This lets
subagents run on a completely different provider:model pair.

If neither base_url nor provider is configured, returns None values so the
child inherits everything from the parent agent.
Expand All @@ -2254,12 +2260,13 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
configured_api_key = str(cfg.get("api_key") or "").strip() or None

if configured_base_url:
api_key = configured_api_key or os.getenv("OPENAI_API_KEY", "").strip()
if not api_key:
raise ValueError(
"Delegation base_url is configured but no API key was found. "
"Set delegation.api_key or OPENAI_API_KEY."
)
# When delegation.api_key is not set, return None so _build_child_agent
# falls back to the parent agent's API key via the credential inheritance
# path (effective_api_key = override_api_key or parent_api_key). This
# lets providers that store their key in a non-OPENAI_API_KEY env var
# (e.g. MINIMAX_API_KEY, DASHSCOPE_API_KEY) work without requiring
# callers to duplicate the key under delegation.api_key.
api_key = configured_api_key # None → inherited from parent in _build_child_agent

base_lower = configured_base_url.lower()
provider = "custom"
Expand Down
Loading