From 421a02a3c61991be61afea4614b56f9aeb2a0a6a Mon Sep 17 00:00:00 2001 From: TurgutKural <58116817+TurgutKural@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:22:11 +0300 Subject: [PATCH 1/4] fix(delegation): resolve nous provider auth before direct endpoint --- tests/tools/test_delegate.py | 34 ++++++++++++++++++++++++++++++++++ tools/delegate_tool.py | 3 ++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 0d7030ddb0c32..f6c74120b229f 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -801,6 +801,40 @@ 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_wins_over_base_url_for_runtime_auth(self, mock_resolve): + """provider=nous must use runtime auth even when base_url is configured. + + Reverse-engineer had delegation.provider=nous plus the Nous inference + base_url and a stale delegation.api_key. Treating that as a direct + custom endpoint bypassed the valid Nous credential resolver and made + subagents 401 while the default profile could call hy3 normally. + """ + 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", + "api_key": "stale-explicit-key", + } + + 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_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 diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 8699edb984707..7f6ebe0f6c544 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -4450,8 +4450,9 @@ 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 + _requires_runtime_provider_auth = _provider_lower in {"nous", "nous-research"} - 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 From 710dc172d361350aa45bb3b9fb8a98c586874c37 Mon Sep 17 00:00:00 2001 From: Turgut Kural <58116817+TurgutKural@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:19:50 +0300 Subject: [PATCH 2/4] chore: add TurgutKural contributor email mapping (directory-based) --- contributors/emails/turgut.kural@outlook.com | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 contributors/emails/turgut.kural@outlook.com diff --git a/contributors/emails/turgut.kural@outlook.com b/contributors/emails/turgut.kural@outlook.com new file mode 100644 index 0000000000000..d4592809d0021 --- /dev/null +++ b/contributors/emails/turgut.kural@outlook.com @@ -0,0 +1,2 @@ +TurgutKural +# PR #61499 (delegation: resolve Nous auth before direct endpoint) From 2bc90da3947c7b4e2b0678b103527d97ebcd4554 Mon Sep 17 00:00:00 2001 From: Turgut Kural <58116817+TurgutKural@users.noreply.github.com> Date: Wed, 22 Jul 2026 10:08:42 +0300 Subject: [PATCH 3/4] fix(delegation): use registered Nous aliases + explicit api_key precedence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address GottZ review (4 items): 1. Alias set: replaced hard-coded {nous, nous-research} with registered aliases {nous, nous-portal, nousresearch} from the provider plugin. 2. Precedence: explicit delegation.api_key now wins over runtime auth — an explicit key means 'use this direct endpoint with this key'. Runtime auth only fires for Nous-family providers WITHOUT explicit key. 3. Docs: rewrote docstring with numbered precedence list matching the actual code behavior. 4. Coverage: added tests for primary path (Nous + base_url, no api_key → runtime auth), explicit-key-wins path, and registered alias coverage. --- tests/tools/test_delegate.py | 66 +++++++++++++++++++++++++++++++----- tools/delegate_tool.py | 43 ++++++++++++++--------- 2 files changed, 85 insertions(+), 24 deletions(-) diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index f6c74120b229f..27dcfcde9d28d 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -802,13 +802,14 @@ def test_direct_endpoint_uses_configured_base_url_and_api_key(self): self.assertEqual(creds["api_mode"], "chat_completions") @patch("hermes_cli.runtime_provider.resolve_runtime_provider") - def test_nous_provider_wins_over_base_url_for_runtime_auth(self, mock_resolve): - """provider=nous must use runtime auth even when base_url is configured. - - Reverse-engineer had delegation.provider=nous plus the Nous inference - base_url and a stale delegation.api_key. Treating that as a direct - custom endpoint bypassed the valid Nous credential resolver and made - subagents 401 while the default profile could call hy3 normally. + 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 = { @@ -822,7 +823,7 @@ def test_nous_provider_wins_over_base_url_for_runtime_auth(self, mock_resolve): "model": "tencent/hy3:free", "provider": "nous", "base_url": "https://inference-api.nousresearch.com/v1", - "api_key": "stale-explicit-key", + # No api_key — runtime resolution must provide the credential. } creds = _resolve_delegation_credentials(cfg, parent) @@ -835,6 +836,55 @@ def test_nous_provider_wins_over_base_url_for_runtime_auth(self, mock_resolve): 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 diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 7f6ebe0f6c544..a6696b5a5b296 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -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. """ @@ -4450,7 +4453,15 @@ 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 - _requires_runtime_provider_auth = _provider_lower in {"nous", "nous-research"} + # 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 + ) 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 From 85d0471f4a0fceb6210a6e87eeb5ad5d4e78e9a7 Mon Sep 17 00:00:00 2001 From: Turgut Kural <58116817+TurgutKural@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:00:23 +0300 Subject: [PATCH 4/4] chore(contributors): consolidate TurgutKural mapping for #61499 + #58495 --- contributors/emails/turgut.kural@outlook.com | 1 + 1 file changed, 1 insertion(+) diff --git a/contributors/emails/turgut.kural@outlook.com b/contributors/emails/turgut.kural@outlook.com index d4592809d0021..4c2510aaed9bf 100644 --- a/contributors/emails/turgut.kural@outlook.com +++ b/contributors/emails/turgut.kural@outlook.com @@ -1,2 +1,3 @@ TurgutKural # PR #61499 (delegation: resolve Nous auth before direct endpoint) +# PR #58495 (compression: skip session split on plugin no-op)