Skip to content
Open
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
1 change: 1 addition & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"evansrory@gmail.com": "zimigit2020",
"237263164+ft-ioxcs@users.noreply.github.com": "ft-ioxcs",
"tharushkadinujaya05@gmail.com": "0xneobyte",
"Veritas-7@users.noreply.github.com": "Veritas-7",
"138671361+Veritas-7@users.noreply.github.com": "Veritas-7",
"keiron@onehanded.com": "kmccammon",
"268233388+CiarasClaws@users.noreply.github.com": "CiarasClaws",
Expand Down
42 changes: 42 additions & 0 deletions tests/tools/test_delegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,48 @@ 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_strips_configured_api_key_env(self):
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\n"}, clear=False):
creds = _resolve_delegation_credentials(cfg, parent)
self.assertEqual(creds["api_key"], "glm-key-from-env")

def test_direct_endpoint_missing_configured_api_key_env_raises(self):
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": ""}, clear=False):
with self.assertRaises(ValueError) as ctx:
_resolve_delegation_credentials(cfg, parent)
self.assertIn("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: 24 additions & 13 deletions tools/delegate_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2652,12 +2652,14 @@ 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`` can name a provider-specific environment
variable. When neither 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``). This keeps
parent-key inheritance for local OpenAI-compatible endpoints while avoiding
accidental inheritance when the user explicitly selected a provider-specific
environment variable.

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 +2675,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.
api_key = configured_api_key
if not api_key and configured_api_key_env:
api_key = (os.getenv(configured_api_key_env) or "").strip() or None
if not api_key:
raise ValueError(
f"delegation.api_key_env is set to '{configured_api_key_env}', "
f"but that environment variable is missing or empty. "
f"Set {configured_api_key_env}, set delegation.api_key, "
f"or remove delegation.api_key_env to inherit the parent key."
)

# 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
Loading