Delegation API key credential chain resolution bug
Cluster 15810. Data as of
Nothing open on GitHub. Fix merged in #19741 on 4 May 2026. Check it covers your case.
The short answer
- Known?
- Yes. 4 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #19741. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #19741. That is separate from the fix event. - Still open
- Nothing open on GitHub.
A bug where delegate_task falls back to OPENAI_API_KEY instead of the correct provider credential chain when delegation.base_url is set without an explicit delegation.api_key. The fixes make delegation inherit the parent API key or resolve from the proper provider credentials.
Open issues0
Open PRs0
Fixes on record
-
#19741 fix(delegate): inherit parent api_key when delegation.base_url set without delegation.api_key
files:
tests/tools/test_delegate.py,tools/delegate_tool.pyShow the diff
-
tests/tools/test_delegate.py@@ -821,7 +821,9 @@ 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", @@ -829,10 +831,11 @@ def test_direct_endpoint_falls_back_to_openai_api_key_env(self): } with patch.dict(os.environ, {"OPENAI_API_KEY": "[redacted:secret-assignment]"}, 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 @@ 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): -
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 = [redacted:secret-assignment] 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 = [redacted:secret-assignment]"api_key") or "").strip() or None if configured_base_url: - api_key = [redacted:secret-assignment] 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 = [redacted:secret-assignment] 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 = [redacted:secret-assignment] # None → inherited from parent in _build_child_agent base_lower = configured_base_url.lower() provider = "custom"
-
2 PRs closed without merging
From the close comments: merged via another change 1 · other 1
Closed issues · 1
- Closed#15810[bug] delegate_task: api_key falls back to OPENAI_API_KEY instead of provider credential chain when delegation.base_url is set
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #19741
- Closed issues
- 1
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 4 May 2026