fix(delegation): preserve configured_provider name when runtime returns 'custom'

Named custom providers (e.g. crof.ai) resolve to provider='custom' at the
runtime level, causing subagents to lose their intended provider identity.
On retry/fallback, resolve_provider_client('custom', model=...) searches all
providers advertising that model and picks non-deterministically, routing to
Z.AI or Bailian instead of the configured target.

The fix preserves configured_provider when runtime['provider'] == 'custom',
restoring the original provider name so routing stays correct through retries.
Adds a named constant _RUNTIME_PROVIDER_CUSTOM instead of a magic string.

Adds three regression tests:
- test_named_custom_provider_preserves_provider_name: the #26954 case
- test_standard_provider_not_overwritten_by_configured_name: openrouter/nous
  must still return their own identity, not the configured name
- test_custom_provider_with_empty_configured_provider_falls_back_to_runtime:
  empty provider triggers the early-return None path as before
This commit is contained in:
Mind-Dragon 2026-05-16 16:28:40 +02:00 committed by Teknium
parent 08a66b2ae3
commit 84667cbc21
2 changed files with 71 additions and 1 deletions

View file

@ -1014,6 +1014,71 @@ class TestDelegationCredentialResolution(unittest.TestCase):
self.assertIsNone(creds["model"])
self.assertIsNone(creds["provider"])
@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_named_custom_provider_preserves_provider_name(self, mock_resolve):
"""Named custom provider (e.g. crof.ai) resolves to 'custom' at runtime level
but the subagent must retain the original provider identity so that
resolve_provider_client routes to the correct endpoint on retry/fallback.
Regression test for #26954.
"""
mock_resolve.return_value = {
"provider": "custom", # runtime marks it as "custom" type
"model": "deepseek-v4-pro-CEER",
"base_url": "https://api.crof.ai/v1",
"api_key": "crof-key-abc",
"api_mode": "chat_completions",
}
parent = _make_mock_parent(depth=0)
cfg = {"model": "deepseek-v4-pro-CEER", "provider": "crof.ai"}
creds = _resolve_delegation_credentials(cfg, parent)
# The key assertion: subagent must keep "crof.ai", NOT "custom"
self.assertEqual(creds["provider"], "crof.ai")
self.assertEqual(creds["model"], "deepseek-v4-pro-CEER")
self.assertEqual(creds["base_url"], "https://api.crof.ai/v1")
self.assertEqual(creds["api_key"], "crof-key-abc")
# Verify resolve_runtime_provider was called with the configured name
mock_resolve.assert_called_once_with(
requested="crof.ai", target_model="deepseek-v4-pro-CEER"
)
@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_standard_provider_not_overwritten_by_configured_name(self, mock_resolve):
"""Standard (non-custom) providers must still return runtime identity,
not the configured name, to preserve existing behaviour for openrouter,
nous, etc.
"""
mock_resolve.return_value = {
"provider": "openrouter",
"model": "anthropic/claude-sonnet-4",
"base_url": "https://openrouter.ai/api/v1",
"api_key": "or-key-xyz",
"api_mode": "chat_completions",
}
parent = _make_mock_parent(depth=0)
cfg = {"model": "anthropic/claude-sonnet-4", "provider": "openrouter"}
creds = _resolve_delegation_credentials(cfg, parent)
# Standard provider returns its own name, not "custom"
self.assertEqual(creds["provider"], "openrouter")
@patch("hermes_cli.runtime_provider.resolve_runtime_provider")
def test_custom_provider_with_empty_configured_provider_falls_back_to_runtime(self, mock_resolve):
"""When configured_provider is empty/None, the early return kicks in and
we return provider=None regardless of what runtime resolved. The runtime
path is only reached when configured_provider is a non-empty string.
"""
mock_resolve.return_value = {
"provider": "custom",
"model": "some-model",
"base_url": "https://fallback.example.com/v1",
"api_key": "key-fallback",
"api_mode": "chat_completions",
}
parent = _make_mock_parent(depth=0)
cfg = {"model": "some-model", "provider": ""}
creds = _resolve_delegation_credentials(cfg, parent)
# Empty provider → early return with None (child inherits parent)
self.assertIsNone(creds["provider"])
class TestDelegationProviderIntegration(unittest.TestCase):
"""Integration tests: delegation config → _run_single_child → AIAgent construction."""

View file

@ -31,6 +31,11 @@ from concurrent.futures import (
from typing import Any, Dict, List, Optional
from toolsets import TOOLSETS
# Sentinel value used by the runtime provider system for providers that are
# not natively known (named custom providers, third-party aggregators, etc.).
# Must match hermes_cli.runtime_provider.RUNTIME_PROVIDER_TYPE_CUSTOM.
_RUNTIME_PROVIDER_CUSTOM = "custom"
from tools import file_state
from tools.terminal_tool import set_approval_callback as _set_subagent_approval_cb
from utils import base_url_hostname, is_truthy_value
@ -2442,7 +2447,7 @@ def _resolve_delegation_credentials(cfg: dict, parent_agent) -> dict:
return {
"model": configured_model or runtime.get("model") or None,
"provider": runtime.get("provider"),
"provider": configured_provider if runtime.get("provider") == _RUNTIME_PROVIDER_CUSTOM else runtime.get("provider"),
"base_url": runtime.get("base_url"),
"api_key": api_key,
"api_mode": runtime.get("api_mode"),