From 0b5fd40a01f6d48549a2d9130e0cb1443be1900c Mon Sep 17 00:00:00 2001 From: briandevans <252620095+briandevans@users.noreply.github.com> Date: Sat, 25 Apr 2026 19:22:10 -0700 Subject: [PATCH] =?UTF-8?q?fix(delegate):=20correct=20=5Fspawn=5Fchild=20?= =?UTF-8?q?=E2=86=92=20=5Fbuild=5Fchild=5Fagent=20in=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/tools/test_delegate.py | 15 +++++++++------ tools/delegate_tool.py | 29 ++++++++++++++++++----------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 089c46da09..dfe35ea19c 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -821,7 +821,9 @@ class TestDelegationCredentialResolution(unittest.TestCase): 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", @@ -829,10 +831,11 @@ class TestDelegationCredentialResolution(unittest.TestCase): } 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", @@ -846,9 +849,9 @@ class TestDelegationCredentialResolution(unittest.TestCase): }, 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): diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index c288e7b28a..5968697e94 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -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. @@ -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"