From 4422637e7a3b0731cb161edc2459119918da84c3 Mon Sep 17 00:00:00 2001 From: stablegenius49 <16443023+stablegenius49@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:52:16 -0700 Subject: [PATCH 1/2] fix: resolve named custom delegation providers --- hermes_cli/runtime_provider.py | 76 ++++++++++++++++++ tests/test_runtime_provider_resolution.py | 93 ++++++++++++++++++++++- 2 files changed, 168 insertions(+), 1 deletion(-) diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index 6cd57f95d..fb487f450 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -18,6 +18,10 @@ from hermes_cli.config import load_config from hermes_constants import OPENROUTER_BASE_URL +def _normalize_custom_provider_name(value: str) -> str: + return value.strip().lower().replace(" ", "-") + + def _get_model_config() -> Dict[str, Any]: config = load_config() model_cfg = config.get("model") @@ -47,6 +51,69 @@ def resolve_requested_provider(requested: Optional[str] = None) -> str: return "auto" +def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, Any]]: + requested_norm = _normalize_custom_provider_name(requested_provider or "") + if not requested_norm or requested_norm == "custom": + return None + + config = load_config() + custom_providers = config.get("custom_providers") + if not isinstance(custom_providers, list): + return None + + for entry in custom_providers: + if not isinstance(entry, dict): + continue + name = entry.get("name") + base_url = entry.get("base_url") + if not isinstance(name, str) or not isinstance(base_url, str): + continue + name_norm = _normalize_custom_provider_name(name) + menu_key = f"custom:{name_norm}" + if requested_norm not in {name_norm, menu_key}: + continue + return { + "name": name.strip(), + "base_url": base_url.strip(), + "api_key": str(entry.get("api_key", "") or "").strip(), + } + + return None + + +def _resolve_named_custom_runtime( + *, + requested_provider: str, + explicit_api_key: Optional[str] = None, + explicit_base_url: Optional[str] = None, +) -> Optional[Dict[str, Any]]: + custom_provider = _get_named_custom_provider(requested_provider) + if not custom_provider: + return None + + base_url = ( + (explicit_base_url or "").strip() + or custom_provider.get("base_url", "") + ).rstrip("/") + if not base_url: + return None + + api_key = ( + (explicit_api_key or "").strip() + or custom_provider.get("api_key", "") + or os.getenv("OPENAI_API_KEY", "").strip() + or os.getenv("OPENROUTER_API_KEY", "").strip() + ) + + return { + "provider": "openrouter", + "api_mode": "chat_completions", + "base_url": base_url, + "api_key": api_key, + "source": f"custom_provider:{custom_provider.get('name', requested_provider)}", + } + + def _resolve_openrouter_runtime( *, requested_provider: str, @@ -122,6 +189,15 @@ def resolve_runtime_provider( """Resolve runtime provider credentials for agent execution.""" requested_provider = resolve_requested_provider(requested) + custom_runtime = _resolve_named_custom_runtime( + requested_provider=requested_provider, + explicit_api_key=explicit_api_key, + explicit_base_url=explicit_base_url, + ) + if custom_runtime: + custom_runtime["requested_provider"] = requested_provider + return custom_runtime + provider = resolve_provider( requested_provider, explicit_api_key=explicit_api_key, diff --git a/tests/test_runtime_provider_resolution.py b/tests/test_runtime_provider_resolution.py index 520205df0..3ff1066cd 100644 --- a/tests/test_runtime_provider_resolution.py +++ b/tests/test_runtime_provider_resolution.py @@ -150,7 +150,7 @@ def test_custom_endpoint_auto_provider_prefers_openai_key(monkeypatch): monkeypatch.setenv("OPENAI_BASE_URL", "https://my-vllm-server.example.com/v1") monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False) monkeypatch.setenv("OPENAI_API_KEY", "sk-vllm-key") - monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-should-not-leak") + monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-...leak") resolved = rp.resolve_runtime_provider(requested="auto") @@ -158,6 +158,97 @@ def test_custom_endpoint_auto_provider_prefers_openai_key(monkeypatch): assert resolved["api_key"] == "sk-vllm-key" +def test_named_custom_provider_uses_saved_credentials(monkeypatch): + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + monkeypatch.setattr( + rp, + "load_config", + lambda: { + "custom_providers": [ + { + "name": "Local", + "base_url": "http://1.2.3.4:1234/v1", + "api_key": "local-provider-key", + } + ] + }, + ) + monkeypatch.setattr( + rp, + "resolve_provider", + lambda *a, **k: (_ for _ in ()).throw( + AssertionError( + "resolve_provider should not be called for named custom providers" + ) + ), + ) + + resolved = rp.resolve_runtime_provider(requested="local") + + assert resolved["provider"] == "openrouter" + assert resolved["api_mode"] == "chat_completions" + assert resolved["base_url"] == "http://1.2.3.4:1234/v1" + assert resolved["api_key"] == "local-provider-key" + assert resolved["requested_provider"] == "local" + assert resolved["source"] == "custom_provider:Local" + + +def test_named_custom_provider_falls_back_to_openai_api_key(monkeypatch): + monkeypatch.setenv("OPENAI_API_KEY", "env-openai-key") + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + monkeypatch.setattr( + rp, + "load_config", + lambda: { + "custom_providers": [ + { + "name": "Local LLM", + "base_url": "http://localhost:1234/v1", + } + ] + }, + ) + monkeypatch.setattr( + rp, + "resolve_provider", + lambda *a, **k: (_ for _ in ()).throw( + AssertionError( + "resolve_provider should not be called for named custom providers" + ) + ), + ) + + resolved = rp.resolve_runtime_provider(requested="custom:local-llm") + + assert resolved["base_url"] == "http://localhost:1234/v1" + assert resolved["api_key"] == "env-openai-key" + assert resolved["requested_provider"] == "custom:local-llm" + + +def test_resolve_runtime_provider_nous_api(monkeypatch): + """Nous Portal API key provider resolves via the api_key path.""" + monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "nous-api") + monkeypatch.setattr( + rp, + "resolve_api_key_provider_credentials", + lambda pid: { + "provider": "nous-api", + "api_key": "nous-test-key", + "base_url": "https://inference-api.nousresearch.com/v1", + "source": "NOUS_API_KEY", + }, + ) + + resolved = rp.resolve_runtime_provider(requested="nous-api") + + assert resolved["provider"] == "nous-api" + assert resolved["api_mode"] == "chat_completions" + assert resolved["base_url"] == "https://inference-api.nousresearch.com/v1" + assert resolved["api_key"] == "nous-test-key" + assert resolved["requested_provider"] == "nous-api" + + def test_explicit_openrouter_skips_openai_base_url(monkeypatch): """When the user explicitly requests openrouter, OPENAI_BASE_URL (which may point to a custom endpoint) must not override the From 88951215d36882c8df0cd98bb6302c0636ef7790 Mon Sep 17 00:00:00 2001 From: teknium1 Date: Sat, 14 Mar 2026 11:24:29 -0700 Subject: [PATCH 2/2] fix: avoid custom provider shadowing built-in providers Follow up on salvaged PR #1012. Prevents raw custom-provider names from intercepting built-in provider ids, and keeps the regression coverage focused on current-main behavior. --- hermes_cli/runtime_provider.py | 14 +++++++++ tests/test_runtime_provider_resolution.py | 36 +++++++++++++++-------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index fb487f450..fead68000 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -5,6 +5,7 @@ from __future__ import annotations import os from typing import Any, Dict, Optional +from hermes_cli import auth as auth_mod from hermes_cli.auth import ( AuthError, PROVIDER_REGISTRY, @@ -56,6 +57,19 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An if not requested_norm or requested_norm == "custom": return None + # Raw names should only map to custom providers when they are not already + # valid built-in providers or aliases. Explicit menu keys like + # ``custom:local`` always target the saved custom provider. + if requested_norm == "auto": + return None + if not requested_norm.startswith("custom:"): + try: + auth_mod.resolve_provider(requested_norm) + except AuthError: + pass + else: + return None + config = load_config() custom_providers = config.get("custom_providers") if not isinstance(custom_providers, list): diff --git a/tests/test_runtime_provider_resolution.py b/tests/test_runtime_provider_resolution.py index 3ff1066cd..a53c716a3 100644 --- a/tests/test_runtime_provider_resolution.py +++ b/tests/test_runtime_provider_resolution.py @@ -226,27 +226,37 @@ def test_named_custom_provider_falls_back_to_openai_api_key(monkeypatch): assert resolved["requested_provider"] == "custom:local-llm" -def test_resolve_runtime_provider_nous_api(monkeypatch): - """Nous Portal API key provider resolves via the api_key path.""" - monkeypatch.setattr(rp, "resolve_provider", lambda *a, **k: "nous-api") +def test_named_custom_provider_does_not_shadow_builtin_provider(monkeypatch): monkeypatch.setattr( rp, - "resolve_api_key_provider_credentials", - lambda pid: { - "provider": "nous-api", - "api_key": "nous-test-key", + "load_config", + lambda: { + "custom_providers": [ + { + "name": "nous", + "base_url": "http://localhost:1234/v1", + "api_key": "shadow-key", + } + ] + }, + ) + monkeypatch.setattr( + rp, + "resolve_nous_runtime_credentials", + lambda **kwargs: { "base_url": "https://inference-api.nousresearch.com/v1", - "source": "NOUS_API_KEY", + "api_key": "nous-runtime-key", + "source": "portal", + "expires_at": None, }, ) - resolved = rp.resolve_runtime_provider(requested="nous-api") + resolved = rp.resolve_runtime_provider(requested="nous") - assert resolved["provider"] == "nous-api" - assert resolved["api_mode"] == "chat_completions" + assert resolved["provider"] == "nous" assert resolved["base_url"] == "https://inference-api.nousresearch.com/v1" - assert resolved["api_key"] == "nous-test-key" - assert resolved["requested_provider"] == "nous-api" + assert resolved["api_key"] == "nous-runtime-key" + assert resolved["requested_provider"] == "nous" def test_explicit_openrouter_skips_openai_base_url(monkeypatch):