diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 6fb57bff7e..ef250c56c7 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -1303,6 +1303,25 @@ class TestChildCredentialPoolResolution(unittest.TestCase): # --- Custom-endpoint identity resolution (issue #7833) --- + def test_named_custom_child_pool_follows_requested_provider_not_endpoint_order(self): + """#45763 (salvage #89021): two named custom providers on one gateway URL keep separate pools; the child + leases the pool of the identity it inherited, not the first entry registered for that URL.""" + from hermes_constants import get_hermes_home + + url = "https://gateway.invalid/v1" + get_hermes_home().joinpath("config.yaml").write_text( + f"providers:\n claude-ai:\n api: {url}\n open-ai:\n api: {url}\n", encoding="utf-8", + ) + parent = _make_mock_parent() + parent.provider, parent.base_url, parent.requested_provider = "custom", url, "custom:open-ai" + parent._credential_pool = None + + with patch("tools.delegate_tool_config._loaded_pool", side_effect=lambda key: key) as loaded: + key = _resolve_child_credential_pool("custom", parent, url, effective_requested_provider="custom:open-ai") + loaded.assert_called_once() + self.assertIn("open-ai", key) + self.assertNotIn("claude", key) + @patch( "tools.delegate_tool._load_config", diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index f2c7c67053..b99d6dea1b 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -275,7 +275,9 @@ def _build_child_agent( if parent_sid and getattr(child, "_session_init_model_config", None) is not None: child._session_init_model_config["_delegate_from"] = parent_sid # Shared pool lets children rotate credentials on rate limits. - child_pool = _resolve_child_credential_pool(rt["provider"], parent_agent, rt["base_url"]) + child_pool = _resolve_child_credential_pool( + rt["provider"], parent_agent, rt["base_url"], effective_requested_provider=rt.get("requested_provider"), + ) if child_pool is not None: child._credential_pool = child_pool diff --git a/tools/delegate_tool_config.py b/tools/delegate_tool_config.py index 65c2ad38f1..c4003d603d 100644 --- a/tools/delegate_tool_config.py +++ b/tools/delegate_tool_config.py @@ -234,6 +234,7 @@ def _pool_serves_endpoint(pool: Any, provider: Optional[str], base_url: Optional def _resolve_child_credential_pool( effective_provider: Optional[str], parent_agent, effective_base_url: Optional[str] = None, + effective_requested_provider: Optional[str] = None, ): """Credential pool for the child: parent's pool (same provider), that provider's own pool, or None (child keeps its fixed credential). Custom endpoints all collapse to ``provider="custom"``, so they are matched by endpoint @@ -246,6 +247,10 @@ def _resolve_child_credential_pool( interchangeable and let the child inherit the parent's pool. We therefore resolve custom runtimes by endpoint identity (the ``custom:`` pool key derived from the base_url) and only share the parent's pool when both resolve to the *same* custom endpoint. See #7833. + + Named custom providers may share one gateway URL with different credentials, so the inherited + ``requested_provider`` identity takes precedence over URL-only matching (#45763): the child must not + lease the first pool registered for the shared endpoint. """ parent_pool = getattr(parent_agent, "_credential_pool", None) if not effective_provider: @@ -254,10 +259,12 @@ def _resolve_child_credential_pool( try: if effective_provider == "custom": from agent.credential_pool import get_custom_provider_pool_key - child_key = get_custom_provider_pool_key(effective_base_url) + child_key = get_custom_provider_pool_key(effective_base_url, provider_name=effective_requested_provider) if child_key is None: return None - parent_key = get_custom_provider_pool_key(getattr(parent_agent, "base_url", None)) + parent_key = get_custom_provider_pool_key( + getattr(parent_agent, "base_url", None), provider_name=getattr(parent_agent, "requested_provider", None), + ) if parent_pool is not None and parent_provider == "custom" and parent_key is not None and parent_key == child_key: return parent_pool return _loaded_pool(child_key)