Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions tests/tools/test_delegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,41 @@ def test_direct_endpoint_returns_none_api_key_when_not_configured(self):
self.assertIsNone(creds["api_key"])
self.assertEqual(creds["provider"], "custom")

def test_direct_endpoint_uses_configured_api_key_env(self):
# Direct endpoints can be non-OpenAI providers. When api_key_env is set,
# delegation must use that key instead of inheriting the parent's key.
parent = _make_mock_parent(depth=0)
cfg = {
"model": "glm-5.2",
"provider": "custom",
"base_url": "https://api.z.ai/api/coding/paas/v4",
"api_key_env": "GLM_API_KEY",
}
with patch.dict(os.environ, {"GLM_API_KEY": "glm-key-from-env"}, clear=False):
creds = _resolve_delegation_credentials(cfg, parent)
self.assertEqual(creds["api_key"], "glm-key-from-env")
self.assertEqual(creds["provider"], "custom")
self.assertEqual(creds["base_url"], "https://api.z.ai/api/coding/paas/v4")

def test_direct_endpoint_api_key_env_missing_or_empty_raises(self):
# If the user explicitly configured api_key_env, falling back to the
# parent's unrelated key would hide the real setup error and usually
# fail later as a confusing 401 from the custom endpoint.
parent = _make_mock_parent(depth=0)
cfg = {
"model": "glm-5.2",
"provider": "custom",
"base_url": "https://api.z.ai/api/coding/paas/v4",
"api_key_env": "GLM_API_KEY",
}
for env in ({}, {"GLM_API_KEY": ""}, {"GLM_API_KEY": " "}):
with self.subTest(env=env):
with patch.dict(os.environ, env, clear=True):
with self.assertRaises(ValueError) as ctx:
_resolve_delegation_credentials(cfg, parent)
self.assertIn("delegation.api_key_env", str(ctx.exception))
self.assertIn("GLM_API_KEY", str(ctx.exception))

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)
Expand Down
37 changes: 23 additions & 14 deletions tools/delegate_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2652,12 +2652,12 @@ 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. ``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 = override_api_key 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.
OpenAI-compatible endpoint. ``delegation.api_key`` overrides the key;
``delegation.api_key_env`` resolves a provider-specific environment key and
fails fast when that key is missing. When neither key field is configured,
``api_key`` is returned as ``None`` so ``_build_child_agent`` inherits the
parent agent's key (``effective_api_key = override_api_key or
parent_api_key``).

Otherwise, if ``delegation.provider`` is configured, the full credential
bundle (base_url, api_key, api_mode, provider) is resolved via the runtime
Expand All @@ -2673,16 +2673,25 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
configured_provider = str(cfg.get("provider") or "").strip() or None
configured_base_url = str(cfg.get("base_url") or "").strip() or None
configured_api_key = str(cfg.get("api_key") or "").strip() or None
configured_api_key_env = str(cfg.get("api_key_env") or "").strip() or None
configured_api_mode = str(cfg.get("api_mode") or "").strip().lower() or None

if configured_base_url:
# 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 = override_api_key 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 = configured_api_key # None → inherited from parent in _build_child_agent
# Direct endpoints may need a provider-specific env key (for example
# GLM_API_KEY for Z.AI coding-plan). Prefer an explicit api_key, then
# delegation.api_key_env, and only inherit the parent key when neither
# is configured. Without this, a custom delegation endpoint can receive
# the parent's unrelated key and fail with 401.
resolved_key = configured_api_key
if not resolved_key and configured_api_key_env:
env_value = (os.getenv(configured_api_key_env) or "").strip()
if not env_value:
raise ValueError(
"delegation.api_key_env is set to "
f"{configured_api_key_env!r}, but that environment "
"variable is missing or empty"
)
resolved_key = env_value

# Use the shared URL-based api_mode detector (same path the main agent's
# runtime resolver uses) so Anthropic-compatible direct endpoints with a
Expand Down Expand Up @@ -2717,7 +2726,7 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
"model": configured_model,
"provider": provider,
"base_url": configured_base_url,
"api_key": api_key,
"api_key": resolved_key,
"api_mode": api_mode,
}

Expand Down
Loading