{"repo":"NousResearch/hermes-agent","public_id":"15810","display_label":"Delegation API key credential chain resolution bug","summary":"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.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":19741,"merged_at":"2026-05-04T12:18:45+00:00"},"resolution":{"state":"resolved","by":"pr-19741"},"freshness":{"state":"verified","last_verified_at":"2026-05-04T12:18:45+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":15810,"pr_number":19741,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-04T12:18:45+00:00"}],"members":[{"kind":"issue","number":15810,"title":"[bug] delegate_task: api_key falls back to OPENAI_API_KEY instead of provider credential chain when delegation.base_url is set","url":null,"state":"closed"},{"kind":"pr","number":15815,"title":"fix(tools): resolve delegation API key from provider credential chain when base_url is set","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#15810","number":15810}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":15853,"title":"fix(delegate): inherit parent api_key when delegation.base_url set without delegation.api_key (#15810)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19741","number":19741}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":19741,"title":"fix(delegate): inherit parent api_key when delegation.base_url set without delegation.api_key","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["tests/tools/test_delegate.py","tools/delegate_tool.py"],"fix_hunks":[{"file":"tests/tools/test_delegate.py","hunks":["@@ -821,7 +821,9 @@ def test_direct_endpoint_uses_configured_base_url_and_api_key(self):\n         self.assertEqual(creds[\"api_key\"], \"local-key\")\n         self.assertEqual(creds[\"api_mode\"], \"chat_completions\")\n \n-    def test_direct_endpoint_falls_back_to_openai_api_key_env(self):\n+    def test_direct_endpoint_returns_none_api_key_when_not_configured(self):\n+        # When base_url is set without api_key, api_key should be None so\n+        # _build_child_agent inherits the parent's key (effective_api_key = override or parent).\n         parent = _make_mock_parent(depth=0)\n         cfg = {\n             \"model\": \"qwen2.5-coder\",","@@ -829,10 +831,11 @@ def test_direct_endpoint_falls_back_to_openai_api_key_env(self):\n         }\n         with patch.dict(os.environ, {\"OPENAI_API_KEY\": \"[redacted:secret-assignment]\"}, clear=False):\n             creds = _resolve_delegation_credentials(cfg, parent)\n-        self.assertEqual(creds[\"api_key\"], \"env-openai-key\")\n+        self.assertIsNone(creds[\"api_key\"])\n         self.assertEqual(creds[\"provider\"], \"custom\")\n \n-    def test_direct_endpoint_does_not_fall_back_to_openrouter_api_key_env(self):\n+    def test_direct_endpoint_no_raise_when_only_provider_env_key_present(self):\n+        # Even if OPENAI_API_KEY is absent, no ValueError — _build_child_agent uses parent key.\n         parent = _make_mock_parent(depth=0)\n         cfg = {\n             \"model\": \"qwen2.5-coder\",","@@ -846,9 +849,9 @@ def test_direct_endpoint_does_not_fall_back_to_openrouter_api_key_env(self):\n             },\n             clear=False,\n         ):\n-            with self.assertRaises(ValueError) as ctx:\n-                _resolve_delegation_credentials(cfg, parent)\n-        self.assertIn(\"OPENAI_API_KEY\", str(ctx.exception))\n+            creds = _resolve_delegation_credentials(cfg, parent)\n+        self.assertIsNone(creds[\"api_key\"])\n+        self.assertEqual(creds[\"provider\"], \"custom\")\n \n     @patch(\"hermes_cli.runtime_provider.resolve_runtime_provider\")\n     def test_nous_provider_resolves_nous_credentials(self, mock_resolve):"]},{"file":"tools/delegate_tool.py","hunks":["@@ -2237,11 +2237,17 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:\n     \"\"\"Resolve credentials for subagent delegation.\n \n     If ``delegation.base_url`` is configured, subagents use that direct\n-    OpenAI-compatible endpoint. Otherwise, if ``delegation.provider`` is\n-    configured, the full credential bundle (base_url, api_key, api_mode,\n-    provider) is resolved via the runtime provider system — the same path used\n-    by CLI/gateway startup. This lets subagents run on a completely different\n-    provider:model pair.\n+    OpenAI-compatible endpoint. ``delegation.api_key`` overrides the key; when\n+    omitted, ``api_key`` is returned as ``None`` so ``_build_child_agent``\n+    inherits the parent agent's key (``effective_api_key = [redacted:secret-assignment] or\n+    parent_api_key``). This lets providers that store their key outside\n+    ``OPENAI_API_KEY`` (e.g. ``MINIMAX_API_KEY``, ``DASHSCOPE_API_KEY``) work\n+    without a duplicate config entry.\n+\n+    Otherwise, if ``delegation.provider`` is configured, the full credential\n+    bundle (base_url, api_key, api_mode, provider) is resolved via the runtime\n+    provider system — the same path used by CLI/gateway startup. This lets\n+    subagents run on a completely different provider:model pair.\n \n     If neither base_url nor provider is configured, returns None values so the\n     child inherits everything from the parent agent.","@@ -2254,12 +2260,13 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:\n     configured_api_key = [redacted:secret-assignment]\"api_key\") or \"\").strip() or None\n \n     if configured_base_url:\n-        api_key = [redacted:secret-assignment] or os.getenv(\"OPENAI_API_KEY\", \"\").strip()\n-        if not api_key:\n-            raise ValueError(\n-                \"Delegation base_url is configured but no API key was found. \"\n-                \"Set delegation.api_key or OPENAI_API_KEY.\"\n-            )\n+        # When delegation.api_key is not set, return None so _build_child_agent\n+        # falls back to the parent agent's API key via the credential inheritance\n+        # path (effective_api_key = [redacted:secret-assignment] or parent_api_key). This\n+        # lets providers that store their key in a non-OPENAI_API_KEY env var\n+        # (e.g. MINIMAX_API_KEY, DASHSCOPE_API_KEY) work without requiring\n+        # callers to duplicate the key under delegation.api_key.\n+        api_key = [redacted:secret-assignment]  # None → inherited from parent in _build_child_agent\n \n         base_lower = configured_base_url.lower()\n         provider = \"custom\"\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}