From 48ae8029aae7ffd9f963e549bb0d03b2837e2be0 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 7 Jun 2026 22:05:14 -0700 Subject: [PATCH] fix(delegate): resolve custom-endpoint subagent pools by endpoint identity (#41730) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subagents delegated to a custom endpoint were misrouted when the parent ran on a different custom endpoint. Both runtimes collapse to provider="custom", so _resolve_child_credential_pool() treated them as interchangeable and handed the child the parent's pool. Leasing from it then overwrote the child's delegated base_url with the parent's endpoint via _swap_credential() — the child sent the delegated model name to the wrong endpoint. Custom runtimes now resolve by endpoint identity (the custom: pool key derived from base_url). The parent pool is reused only when both parent and child resolve to the same custom endpoint; unregistered raw endpoints return None so the child keeps its fixed delegated credential. Non-custom provider paths are unchanged. Fixes #7833. --- agent/credential_pool.py | 2 +- tests/tools/test_delegate.py | 67 ++++++++++++++++++++++++++++++++++++ tools/delegate_tool.py | 58 +++++++++++++++++++++++++++++-- 3 files changed, 124 insertions(+), 3 deletions(-) diff --git a/agent/credential_pool.py b/agent/credential_pool.py index e5b473ec525..53cc31daf6d 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -374,7 +374,7 @@ def _iter_custom_providers(config: Optional[dict] = None): yield _normalize_custom_pool_name(name), entry -def get_custom_provider_pool_key(base_url: str, provider_name: Optional[str] = None) -> Optional[str]: +def get_custom_provider_pool_key(base_url: Optional[str], provider_name: Optional[str] = None) -> Optional[str]: """Look up the custom_providers list in config.yaml and return 'custom:' for a matching base_url. When provider_name is given, prefer matching by name first (solving the case where diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 89ad050ea40..4b08dc491d3 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -1518,6 +1518,73 @@ class TestChildCredentialPoolResolution(unittest.TestCase): self.assertIsNone(result) + # --- Custom-endpoint identity resolution (issue #7833) --- + + def test_custom_different_endpoint_does_not_inherit_parent_pool(self): + """A child on custom endpoint B must not inherit the parent's custom + endpoint A pool just because both normalize to provider='custom'.""" + parent = _make_mock_parent() + parent.provider = "custom" + parent.base_url = "https://endpoint-a.example.com/v1" + parent._credential_pool = MagicMock(name="parent_custom_a_pool") + + child_pool = MagicMock(name="endpoint_b_pool") + child_pool.has_credentials.return_value = True + + def fake_key(base_url, provider_name=None): + return { + "https://endpoint-a.example.com/v1": "custom:endpoint-a", + "https://endpoint-b.example.com/v1": "custom:endpoint-b", + }.get(base_url) + + with patch("agent.credential_pool.get_custom_provider_pool_key", side_effect=fake_key), \ + patch("agent.credential_pool.load_pool", return_value=child_pool) as load_mock: + result = _resolve_child_credential_pool( + "custom", parent, "https://endpoint-b.example.com/v1" + ) + + # Loaded the child's OWN endpoint pool, not the parent's. + load_mock.assert_called_once_with("custom:endpoint-b") + self.assertIs(result, child_pool) + self.assertIsNot(result, parent._credential_pool) + + def test_custom_same_endpoint_shares_parent_pool(self): + """A child on the SAME custom endpoint as the parent reuses the parent's + pool so rotation/cooldown state stays synchronized.""" + parent = _make_mock_parent() + parent.provider = "custom" + parent.base_url = "https://endpoint-a.example.com/v1" + parent._credential_pool = MagicMock(name="parent_custom_a_pool") + + with patch( + "agent.credential_pool.get_custom_provider_pool_key", + return_value="custom:endpoint-a", + ): + result = _resolve_child_credential_pool( + "custom", parent, "https://endpoint-a.example.com/v1" + ) + + self.assertIs(result, parent._credential_pool) + + def test_custom_unregistered_endpoint_returns_none(self): + """A raw delegation.base_url with no matching custom_providers entry + must NOT inherit the parent's pool — return None so the child keeps its + fixed delegated credential.""" + parent = _make_mock_parent() + parent.provider = "custom" + parent.base_url = "https://endpoint-a.example.com/v1" + parent._credential_pool = MagicMock(name="parent_custom_a_pool") + + with patch( + "agent.credential_pool.get_custom_provider_pool_key", + return_value=None, + ): + result = _resolve_child_credential_pool( + "custom", parent, "https://raw-unregistered.example.com/v1" + ) + + self.assertIsNone(result) + def test_build_child_agent_assigns_parent_pool_when_shared(self): parent = _make_mock_parent() mock_pool = MagicMock() diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index db982776d21..6e195dfe59f 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -1184,7 +1184,9 @@ def _build_child_agent( # Share a credential pool with the child when possible so subagents can # rotate credentials on rate limits instead of getting pinned to one key. - child_pool = _resolve_child_credential_pool(effective_provider, parent_agent) + child_pool = _resolve_child_credential_pool( + effective_provider, parent_agent, effective_base_url + ) if child_pool is not None: child._credential_pool = child_pool @@ -2368,7 +2370,11 @@ def delegate_task( ) -def _resolve_child_credential_pool(effective_provider: Optional[str], parent_agent): +def _resolve_child_credential_pool( + effective_provider: Optional[str], + parent_agent, + effective_base_url: Optional[str] = None, +): """Resolve a credential pool for the child agent. Rules: @@ -2377,12 +2383,60 @@ def _resolve_child_credential_pool(effective_provider: Optional[str], parent_age 2. Different provider -> try to load that provider's own pool. 3. No pool available -> return None and let the child keep the inherited fixed credential behavior. + + Custom endpoints are a special case: every direct ``delegation.base_url`` + runtime collapses to ``provider="custom"``, so bare provider equality would + treat two *different* custom endpoints as interchangeable and let the child + inherit the parent's pool. Leasing from that pool then overwrites the + child's delegated ``base_url`` with the parent's endpoint (issue #7833). + 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. """ if not effective_provider: return getattr(parent_agent, "_credential_pool", None) parent_provider = getattr(parent_agent, "provider", None) or "" parent_pool = getattr(parent_agent, "_credential_pool", None) + + # Custom endpoints: distinguish by endpoint identity, not the bare "custom" + # provider string. Two custom runtimes are only interchangeable when they + # resolve to the same custom: pool key. + if effective_provider == "custom": + try: + from agent.credential_pool import get_custom_provider_pool_key, load_pool + + child_key = get_custom_provider_pool_key(effective_base_url) + if child_key is None: + # Unregistered endpoint (raw delegation.base_url with no + # matching custom_providers entry) -> no shared pool exists. + # Keep the child's fixed delegated credential rather than + # risk inheriting the parent's custom endpoint. + return None + + # Reuse the parent's pool only when it is the same custom endpoint. + parent_key = get_custom_provider_pool_key( + getattr(parent_agent, "base_url", 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 + + pool = load_pool(child_key) + if pool is not None and pool.has_credentials(): + return pool + except Exception as exc: + logger.debug( + "Could not resolve custom credential pool for child endpoint '%s': %s", + effective_base_url, + exc, + ) + return None + if parent_pool is not None and effective_provider == parent_provider: return parent_pool