fix(providers): align custom route scoping

This commit is contained in:
cucurigoo 2026-07-22 00:00:14 +00:00 committed by kshitij
parent 97499d702e
commit cb785e6b49
5 changed files with 362 additions and 13 deletions

View file

@ -174,6 +174,19 @@ def _provider_default_routes(provider: str) -> set[str]:
return routes
def _normalize_custom_provider_name(value: Any) -> str:
"""Mirror runtime normalization for a requested custom-provider identity."""
return str(value or "").strip().lower().replace(" ", "-")
def _custom_provider_runtime_ids(value: Any) -> set[str]:
"""Return raw/menu identities that runtime accepts for a configured name."""
normalized = _normalize_custom_provider_name(value)
if not normalized:
return set()
return {normalized, f"custom:{normalized}"}
def _build_codex_gpt5_autoraise_notice(
autoraise: Dict[str, Any], context_length: Optional[int] = None
) -> str:
@ -1901,17 +1914,98 @@ def init_agent(
_configured_base_url = _normalize_route_base_url(
_model_cfg.get("base_url")
)
if not _configured_base_url and _configured_provider.lower().startswith("custom:"):
_configured_custom_name = _configured_provider.split(":", 1)[1].lower()
for _provider_entry in _custom_providers:
if not isinstance(_provider_entry, dict):
continue
if str(_provider_entry.get("name") or "").strip().lower() != _configured_custom_name:
continue
_configured_base_url = _normalize_route_base_url(
_provider_entry.get("base_url")
_configured_provider_norm = _normalize_custom_provider_name(
_configured_provider
)
_custom_provider_candidate = bool(_configured_provider_norm)
_runtime_first_provider_ids = {
"auto",
"moa",
"vertex",
"google-vertex",
"vertex-ai",
"gcp-vertex",
"vertexai",
}
if _configured_provider_norm in _runtime_first_provider_ids:
_custom_provider_candidate = False
elif (
_custom_provider_candidate
and _configured_provider_norm != "custom"
and not _configured_provider_norm.startswith("custom:")
):
try:
from hermes_cli.auth import resolve_provider as resolve_auth_provider
_resolved_auth_provider = resolve_auth_provider(
_configured_provider_norm
)
break
_custom_provider_candidate = (
str(_resolved_auth_provider or "").strip().lower()
!= _configured_provider_norm
)
except Exception:
pass
if not _configured_base_url and _custom_provider_candidate:
_configured_custom_provider = _normalize_custom_provider_name(
_configured_provider
)
_user_providers = _agent_cfg.get("providers")
_disabled_custom_provider_ids: set[str] = set()
if isinstance(_user_providers, dict):
from hermes_cli.config import is_provider_enabled
for _provider_key, _provider_entry in _user_providers.items():
if not isinstance(_provider_entry, dict):
continue
_entry_name = str(
_provider_entry.get("name") or ""
).strip()
_entry_provider_ids = _custom_provider_runtime_ids(
_provider_key
) | _custom_provider_runtime_ids(_entry_name)
if not is_provider_enabled(_provider_entry):
_disabled_custom_provider_ids.update(
provider_id
for provider_id in _entry_provider_ids
if provider_id
)
continue
if _configured_custom_provider not in _entry_provider_ids:
continue
_configured_base_url = _normalize_route_base_url(
_provider_entry.get("api")
or _provider_entry.get("url")
or _provider_entry.get("base_url")
)
if _configured_base_url:
break
if not _configured_base_url:
for _provider_entry in _custom_providers:
if not isinstance(_provider_entry, dict):
continue
_entry_name = str(
_provider_entry.get("name") or ""
).strip()
_entry_provider_key = str(
_provider_entry.get("provider_key") or ""
).strip().lower()
_entry_provider_ids = _custom_provider_runtime_ids(
_entry_name
) | _custom_provider_runtime_ids(_entry_provider_key)
if (
_entry_provider_key
and _custom_provider_runtime_ids(_entry_provider_key)
& _disabled_custom_provider_ids
):
continue
if _configured_custom_provider not in _entry_provider_ids:
continue
_configured_base_url = _normalize_route_base_url(
_provider_entry.get("base_url")
)
if _configured_base_url:
break
_active_route_url = str(agent.base_url or "")
_requested_route_url = str(base_url or "")
if "?" in _requested_route_url.split("#", 1)[0]:

View file

@ -5228,6 +5228,8 @@ def providers_dict_to_custom_providers(providers_dict: Any) -> List[Dict[str, An
custom_providers: List[Dict[str, Any]] = []
for key, entry in providers_dict.items():
if isinstance(entry, dict) and not is_provider_enabled(entry):
continue
normalized = _normalize_custom_provider_entry(entry, provider_key=str(key))
if normalized is not None:
custom_providers.append(normalized)

View file

@ -1404,6 +1404,22 @@ class TestCustomProviderCompatibility:
assert compatible[0]["provider_key"] == "openai-direct"
assert compatible[0]["api_mode"] == "codex_responses"
def test_disabled_provider_is_excluded_from_compatibility_projection(self):
"""Compatibility fallback must not resurrect a disabled modern entry."""
compatible = get_compatible_custom_providers(
{
"providers": {
"route-key": {
"name": "Route Key",
"api": "https://disabled.example/v1",
"enabled": False,
}
}
}
)
assert compatible == []
def test_compatible_custom_providers_prefers_base_url_then_url_then_api(self, tmp_path):
"""URL field precedence is base_url > url > api (PR #9332)."""
config_path = tmp_path / "config.yaml"

View file

@ -1154,6 +1154,25 @@ def test_named_custom_provider_does_not_shadow_builtin_provider(monkeypatch):
assert resolved["requested_provider"] == "nous"
def test_disabled_named_custom_provider_is_not_compatibility_fallback(monkeypatch):
"""Disabled modern entries stay unavailable through the legacy projection."""
monkeypatch.setattr(
rp,
"load_config",
lambda: {
"providers": {
"route-key": {
"name": "Route Key",
"api": "https://disabled.example/v1",
"enabled": False,
}
}
},
)
assert rp._get_named_custom_provider("custom:route-key") is None
def test_nous_pool_entry_refreshes_expired_agent_key(monkeypatch):
stale_token = _fake_invoke_jwt(ttl_seconds=-60)
fresh_token = _fake_invoke_jwt(ttl_seconds=3600)

View file

@ -610,10 +610,16 @@ def test_direct_start_named_custom_route_resolves_configured_base_url():
},
"custom_providers": [
{
"name": "large-route",
"base_url": "https://large.example/v1",
"name": "Large Route",
"base_url": "https://legacy-large.example/v1",
}
],
"providers": {
"large-route": {
"name": "Large Route",
"api": "https://large.example/v1",
}
},
}
agent = _make_direct_start_agent(
@ -630,7 +636,219 @@ def test_direct_start_named_custom_route_resolves_configured_base_url():
cfg,
model="shared-model",
provider="custom",
base_url="https://large.example/v1",
base_url="HTTPS://LARGE.EXAMPLE:443/v1/",
)
assert matching_agent.context_compressor.config_context_length == 1_048_576
legacy_agent = _make_direct_start_agent(
cfg,
model="shared-model",
provider="custom",
base_url="https://legacy-large.example/v1",
)
assert legacy_agent.context_compressor.config_context_length is None
def test_direct_start_named_custom_provider_key_uses_canonical_slug():
"""Raw, canonical, and prefixed provider keys/names share runtime identity."""
cfg = {
"model": {
"default": "shared-model",
"provider": "custom:Route Key",
"context_length": 1_048_576,
},
"providers": {
"Route Key": {
"name": "Friendly Label",
"api": "https://key.example/v1",
},
"custom:Prefixed Key": {
"name": "custom:Prefixed Label",
"api": "https://prefixed.example/v1",
},
},
}
for configured_provider in (
"custom:Route Key",
"custom:Friendly Label",
"Route Key",
"route-key",
"Friendly Label",
"friendly-label",
):
cfg["model"]["provider"] = configured_provider
agent = _make_direct_start_agent(
cfg,
model="shared-model",
provider="custom",
base_url="https://key.example/v1",
)
assert agent.context_compressor.config_context_length == 1_048_576
for configured_provider in (
"custom:Prefixed Key",
"custom:Prefixed Label",
"custom:custom:Prefixed Key",
"custom:custom:Prefixed Label",
):
cfg["model"]["provider"] = configured_provider
agent = _make_direct_start_agent(
cfg,
model="shared-model",
provider="custom",
base_url="https://prefixed.example/v1",
)
assert agent.context_compressor.config_context_length == 1_048_576
for configured_provider in (
"custom: Prefixed Key",
"custom:\tPrefixed Key",
):
cfg["model"]["provider"] = configured_provider
agent = _make_direct_start_agent(
cfg,
model="shared-model",
provider="custom",
base_url="https://prefixed.example/v1",
)
assert agent.context_compressor.config_context_length is None
def test_direct_start_named_custom_raw_legacy_display_name_matches():
"""Legacy display names accepted by runtime also identify the scoped route."""
cfg = {
"model": {
"default": "shared-model",
"provider": "Legacy Route",
"context_length": 1_048_576,
},
"custom_providers": [
{
"name": "Legacy Route",
"base_url": "https://legacy.example/v1",
}
],
}
agent = _make_direct_start_agent(
cfg,
model="shared-model",
provider="custom",
base_url="https://legacy.example/v1",
)
assert agent.context_compressor.config_context_length == 1_048_576
def test_direct_start_literal_bare_custom_entry_matches_runtime():
"""A providers.custom entry makes bare custom a complete route identity."""
cfg = {
"model": {
"default": "shared-model",
"provider": "custom",
"context_length": 1_048_576,
},
"providers": {
"custom": {
"api": "https://literal.example/v1",
}
},
}
agent = _make_direct_start_agent(
cfg,
model="shared-model",
provider="custom",
base_url="https://literal.example/v1",
)
assert agent.context_compressor.config_context_length == 1_048_576
def test_direct_start_disabled_modern_custom_falls_back_only_to_legacy():
"""Disabled modern entries cannot retain pins, but legacy fallback can."""
cfg = {
"model": {
"default": "shared-model",
"provider": "custom:route-key",
"context_length": 1_048_576,
},
"providers": {
"route-key": {
"name": "Route Key",
"api": "https://disabled.example/v1",
"enabled": False,
}
},
}
disabled_agent = _make_direct_start_agent(
cfg,
model="shared-model",
provider="custom",
base_url="https://disabled.example/v1",
)
assert disabled_agent.context_compressor.config_context_length is None
cfg["custom_providers"] = [
{
"name": "Route Key",
"base_url": "https://legacy.example/v1",
}
]
legacy_agent = _make_direct_start_agent(
cfg,
model="shared-model",
provider="custom",
base_url="https://legacy.example/v1",
)
assert legacy_agent.context_compressor.config_context_length == 1_048_576
def test_direct_start_runtime_first_provider_names_require_explicit_custom_prefix():
"""Auto, MoA, and Vertex routes cannot be shadowed by raw custom names."""
for provider_name in (
"auto",
"moa",
"vertex",
"google-vertex",
"vertex-ai",
"gcp-vertex",
"vertexai",
):
base_url = f"https://{provider_name}.shadow.example/v1"
cfg = {
"model": {
"default": "shared-model",
"provider": provider_name,
"context_length": 1_048_576,
},
"providers": {
provider_name: {
"api": base_url,
}
},
}
raw_agent = _make_direct_start_agent(
cfg,
model="shared-model",
provider="custom",
base_url=base_url,
)
assert raw_agent.context_compressor.config_context_length is None
cfg["model"]["provider"] = f"custom:{provider_name}"
custom_agent = _make_direct_start_agent(
cfg,
model="shared-model",
provider="custom",
base_url=base_url,
)
assert custom_agent.context_compressor.config_context_length == 1_048_576