fix(auth): preserve 'custom' provider instead of silently remapping to 'openrouter'

resolve_provider('custom') was silently returning 'openrouter', causing
users who set provider: custom in config.yaml to unknowingly route
through OpenRouter instead of their local/custom endpoint. The display
showed 'via openrouter' even when the user explicitly chose custom.

Changes:
- auth.py: Split the conditional so 'custom' returns 'custom' as-is
- runtime_provider.py: _resolve_named_custom_runtime now returns
  provider='custom' instead of 'openrouter'
- runtime_provider.py: _resolve_openrouter_runtime returns
  provider='custom' when that was explicitly requested
- Add 'no-key-required' placeholder for keyless local servers
- Update existing test + add 5 new tests covering the fix

Fixes #2562
This commit is contained in:
Teknium
2026-03-24 06:41:11 -07:00
committed by GitHub
parent 4313b8aff6
commit 2f1c4fb01f
3 changed files with 92 additions and 4 deletions

View File

@@ -690,8 +690,10 @@ def resolve_provider(
}
normalized = _PROVIDER_ALIASES.get(normalized, normalized)
if normalized in {"openrouter", "custom"}:
if normalized == "openrouter":
return "openrouter"
if normalized == "custom":
return "custom"
if normalized in PROVIDER_REGISTRY:
return normalized
if normalized != "auto":

View File

@@ -198,7 +198,7 @@ def _resolve_named_custom_runtime(
api_key = next((candidate for candidate in api_key_candidates if has_usable_secret(candidate)), "")
return {
"provider": "openrouter",
"provider": "custom",
"api_mode": custom_provider.get("api_mode")
or _detect_api_mode_for_url(base_url)
or "chat_completions",
@@ -279,8 +279,16 @@ def _resolve_openrouter_runtime(
source = "explicit" if (explicit_api_key or explicit_base_url) else "env/config"
# When "custom" was explicitly requested, preserve that as the provider
# name instead of silently relabeling to "openrouter" (#2562).
# Also provide a placeholder API key for local servers that don't require
# authentication — the OpenAI SDK requires a non-empty api_key string.
effective_provider = "custom" if requested_norm == "custom" else "openrouter"
if effective_provider == "custom" and not api_key and not _is_openrouter_url:
api_key = "no-key-required"
return {
"provider": "openrouter",
"provider": effective_provider,
"api_mode": _parse_api_mode(model_cfg.get("api_mode"))
or _detect_api_mode_for_url(base_url)
or "chat_completions",

View File

@@ -267,7 +267,7 @@ def test_named_custom_provider_uses_saved_credentials(monkeypatch):
resolved = rp.resolve_runtime_provider(requested="local")
assert resolved["provider"] == "openrouter"
assert resolved["provider"] == "custom"
assert resolved["api_mode"] == "chat_completions"
assert resolved["base_url"] == "http://1.2.3.4:1234/v1"
assert resolved["api_key"] == "local-provider-key"
@@ -579,3 +579,81 @@ def test_named_custom_provider_anthropic_api_mode(monkeypatch):
assert resolved["api_mode"] == "anthropic_messages"
assert resolved["base_url"] == "https://proxy.example.com/anthropic"
# ------------------------------------------------------------------
# fix #2562 — resolve_provider("custom") must not remap to "openrouter"
# ------------------------------------------------------------------
def test_resolve_provider_custom_returns_custom():
"""resolve_provider('custom') must return 'custom', not 'openrouter'."""
from hermes_cli.auth import resolve_provider
assert resolve_provider("custom") == "custom"
def test_resolve_provider_openrouter_unchanged():
"""resolve_provider('openrouter') must still return 'openrouter'."""
from hermes_cli.auth import resolve_provider
assert resolve_provider("openrouter") == "openrouter"
def test_custom_provider_runtime_preserves_provider_name(monkeypatch):
"""resolve_runtime_provider with provider='custom' must return provider='custom'."""
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
monkeypatch.setattr(
rp,
"load_config",
lambda: {
"model": {
"provider": "custom",
"base_url": "http://localhost:8080/v1",
"api_key": "test-key-123",
}
},
)
resolved = rp.resolve_runtime_provider(requested="custom")
assert resolved["provider"] == "custom", (
f"Expected provider='custom', got provider='{resolved['provider']}'"
)
assert resolved["base_url"] == "http://localhost:8080/v1"
assert resolved["api_key"] == "test-key-123"
def test_custom_provider_no_key_gets_placeholder(monkeypatch):
"""Local server with no API key should get 'no-key-required' placeholder."""
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
monkeypatch.setattr(
rp,
"load_config",
lambda: {
"model": {
"provider": "custom",
"base_url": "http://localhost:8080/v1",
}
},
)
resolved = rp.resolve_runtime_provider(requested="custom")
assert resolved["provider"] == "custom"
assert resolved["api_key"] == "no-key-required"
assert resolved["base_url"] == "http://localhost:8080/v1"
def test_openrouter_provider_not_affected_by_custom_fix(monkeypatch):
"""Fixing custom must not change openrouter behavior."""
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
monkeypatch.setenv("OPENROUTER_API_KEY", "test-or-key")
monkeypatch.setattr(rp, "load_config", lambda: {})
resolved = rp.resolve_runtime_provider(requested="openrouter")
assert resolved["provider"] == "openrouter"