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
3 changes: 3 additions & 0 deletions contributors/emails/turgut.kural@outlook.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
TurgutKural
# PR #61499 (delegation: resolve Nous auth before direct endpoint)
# PR #58495 (compression: skip session split on plugin no-op)
84 changes: 84 additions & 0 deletions tests/tools/test_delegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,90 @@ def test_direct_endpoint_uses_configured_base_url_and_api_key(self):
self.assertEqual(creds["api_key"], "local-key")
self.assertEqual(creds["api_mode"], "chat_completions")

@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_nous_provider_runtime_auth_without_explicit_key(self, mock_resolve):
"""provider=nous + base_url but NO delegation.api_key → runtime auth.

The primary fix path: Nous + base_url configured (e.g. the inference
endpoint) but no explicit delegation.api_key. Without this fix the
base_url branch treats it as a direct custom endpoint and the child
inherits the parent key — which 401s when the parent key is stale or
the Nous JWT rotation has refreshed.
"""
parent = _make_mock_parent(depth=0)
mock_resolve.return_value = {
"model": "tencent/hy3:free",
"provider": "nous",
"base_url": "https://inference-api.nousresearch.com/v1",
"api_key": "fresh-runtime-key",
"api_mode": "chat_completions",
}
cfg = {
"model": "tencent/hy3:free",
"provider": "nous",
"base_url": "https://inference-api.nousresearch.com/v1",
# No api_key — runtime resolution must provide the credential.
}

creds = _resolve_delegation_credentials(cfg, parent)

mock_resolve.assert_called_once_with(
requested="nous", target_model="tencent/hy3:free"
)
self.assertEqual(creds["provider"], "nous")
self.assertEqual(creds["base_url"], "https://inference-api.nousresearch.com/v1")
self.assertEqual(creds["api_key"], "fresh-runtime-key")
self.assertEqual(creds["api_mode"], "chat_completions")

def test_nous_explicit_api_key_uses_direct_endpoint(self):
"""Explicit delegation.api_key with provider=nous → direct endpoint.

When the user explicitly sets delegation.api_key, that means "use this
direct endpoint with this key" — runtime auth must NOT override it.
This preserves the documented precedence: explicit config wins.
"""
parent = _make_mock_parent(depth=0)
cfg = {
"model": "tencent/hy3:free",
"provider": "nous",
"base_url": "https://internal-proxy.example.com/v1",
"api_key": "my-explicit-proxy-key",
}

creds = _resolve_delegation_credentials(cfg, parent)

# Direct endpoint path: provider=custom, explicit key preserved.
self.assertEqual(creds["provider"], "custom")
self.assertEqual(creds["base_url"], "https://internal-proxy.example.com/v1")
self.assertEqual(creds["api_key"], "my-explicit-proxy-key")

@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_nous_registered_aliases_use_runtime_auth(self, mock_resolve):
"""Registered Nous aliases (nous-portal, nousresearch) also route
through runtime auth when no explicit api_key is set."""
parent = _make_mock_parent(depth=0)
mock_resolve.return_value = {
"model": "test/model",
"provider": "nous",
"base_url": "https://inference-api.nousresearch.com/v1",
"api_key": "runtime-key",
"api_mode": "chat_completions",
}

for alias in ("nous-portal", "nousresearch"):
mock_resolve.reset_mock()
cfg = {
"model": "test/model",
"provider": alias,
"base_url": "https://inference-api.nousresearch.com/v1",
}
creds = _resolve_delegation_credentials(cfg, parent)
mock_resolve.assert_called_once_with(
requested=alias, target_model="test/model"
)
self.assertEqual(creds["api_key"], "runtime-key",
f"alias={alias} should use runtime auth")

def test_direct_endpoint_auto_detects_anthropic_messages_suffix(self):
# Issue #10213: Azure AI Foundry exposes Anthropic-compatible models at
# a /anthropic URL suffix. Subagents must pick anthropic_messages
Expand Down
44 changes: 28 additions & 16 deletions tools/delegate_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -4417,21 +4417,24 @@ def _resolve_child_credential_pool(
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.

Otherwise, if ``delegation.provider`` is configured, the full credential
bundle (base_url, api_key, api_mode, provider) is resolved via the runtime
provider system — the same path used by CLI/gateway startup. This lets
subagents run on a completely different provider:model pair.

If neither base_url nor provider is configured, returns None values so the
child inherits everything from the parent agent.
Precedence (highest to lowest):

1. **Explicit ``delegation.api_key``** — always wins. When set, the
configured ``base_url`` is used as a direct OpenAI-compatible endpoint
with that key. This applies even for Nous-family providers: an explicit
key means "use this direct endpoint with this key".
2. **Nous-family provider without explicit key** — ``provider`` is one of
the registered Nous aliases (``nous``, ``nous-portal``, ``nousresearch``)
and no ``delegation.api_key`` is set. Routes through
``resolve_runtime_provider()`` for JWT-rotated credentials, even when
``base_url`` is also configured. This prevents 401s from stale parent
keys when the Nous JWT has rotated.
3. **``delegation.base_url`` without explicit key** — direct endpoint;
``api_key`` is returned as ``None`` so ``_build_child_agent`` inherits
the parent agent's key.
4. **``delegation.provider``** — full credential bundle resolved via the
runtime provider system.
5. **Neither** — child inherits everything from the parent agent.

Raises ValueError with a user-friendly message on credential failure.
"""
Expand All @@ -4450,8 +4453,17 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
_NATIVE_SDK_PROVIDERS = {"bedrock", "vertex", "google", "google-genai"}
_provider_lower = (configured_provider or "").strip().lower()
_is_native_sdk_provider = _provider_lower in _NATIVE_SDK_PROVIDERS
# Nous-family providers must use runtime auth (JWT rotation) UNLESS the
# user explicitly configured delegation.api_key — an explicit key means
# "use this direct endpoint with this key" and must not be overridden.
# Registered aliases: nous, nous-portal, nousresearch
# (plugins/model-providers/nous/__init__.py).
_NOUS_ALIASES = {"nous", "nous-portal", "nousresearch"}
_requires_runtime_provider_auth = (
_provider_lower in _NOUS_ALIASES and not configured_api_key
)

Comment thread
TurgutKural marked this conversation as resolved.
if configured_base_url and not _is_native_sdk_provider:
if configured_base_url and not _is_native_sdk_provider and not _requires_runtime_provider_auth:
# 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
Expand Down
Loading