fix(delegate): key a named custom child's credential pool by its inherited identity

Two named custom providers may share one gateway URL with different
credentials. `_resolve_child_credential_pool` matched custom pools by URL
only, so a child inherited whichever pool was registered first for that
endpoint. Pass the child's `requested_provider` (and the parent's) into
`get_custom_provider_pool_key(provider_name=...)`, which already prefers a
name match over URL order.

Second half of the class fixed by #117839; grafted from PR #89021 onto the
split `delegate_tool_config` layout (#45763).
This commit is contained in:
up
2026-09-21 00:01:12 -07:00
committed by Teknium
parent 7216a86079
commit b905042d37
3 changed files with 31 additions and 3 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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:<name>`` 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)