From 6a159be7ca936f075be2875516498abaed4810c4 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Tue, 19 May 2026 11:30:25 -0700 Subject: [PATCH] fix(runtime): treat 'ollama'/'vllm'/'llamacpp' aliases like 'custom' for base_url trust (#27132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When config.yaml has provider: ollama (or vllm/llamacpp/llama-cpp) with a non-loopback base_url, auth.py's resolve_provider() correctly normalises the alias to 'custom' at the top level, but two sites in runtime_provider.py were still comparing the *original* string against the literal 'custom': - _config_base_url_trustworthy_for_bare_custom() rejected non-loopback URLs because cfg_provider_norm was 'ollama', not 'custom'. - _resolve_openrouter_runtime() only entered the trust branch when requested_norm == 'custom'. Both sites now consult resolve_provider() and treat any alias that resolves to 'custom' identically. Result: provider: ollama + LAN IP no longer silently falls through to OpenRouter (HTTP 401), matching the behaviour of provider: custom with the same base_url. E2E verified across 6 cases (ollama/vllm/llamacpp/custom + LAN; ollama + loopback; openrouter + cloud) — all route to the configured endpoint; 'frobnicate' + LAN still rejects with AuthError as before. Also adds scripts/release.py AUTHOR_MAP entry for @stepanov1975 (PR #22074 — wizard config picker preservation, cherry-picked into the preceding commit). --- hermes_cli/runtime_provider.py | 40 ++++++++++- scripts/release.py | 1 + .../test_runtime_provider_resolution.py | 71 +++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index 11fd9f564ca..0765c72cecb 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -47,7 +47,8 @@ def _config_base_url_trustworthy_for_bare_custom(cfg_base_url: str, cfg_provider """Decide whether ``model.base_url`` may back bare ``custom`` runtime resolution. GitHub #14676: the model picker can select Custom while ``model.provider`` still reflects a - previous provider. Reject non-loopback URLs unless the YAML provider is already ``custom``, + previous provider. Reject non-loopback URLs unless the YAML provider is already ``custom`` + (or one of the local-server aliases that resolve to ``custom`` — ollama, vllm, llamacpp, …), so a stale OpenRouter/Z.ai base_url cannot hijack local ``custom`` sessions. """ cfg_provider_norm = (cfg_provider or "").strip().lower() @@ -56,6 +57,17 @@ def _config_base_url_trustworthy_for_bare_custom(cfg_base_url: str, cfg_provider return False if cfg_provider_norm == "custom": return True + # GitHub #27132: provider aliases that resolve to "custom" at runtime + # (ollama, vllm, llamacpp, …) should be trusted the same way "custom" + # is, otherwise a legit LAN/WireGuard ollama endpoint silently falls + # through to OpenRouter. + try: + from hermes_cli.auth import resolve_provider as _resolve_provider + + if _resolve_provider(cfg_provider_norm) == "custom": + return True + except Exception: + pass if base_url_host_matches(bu, "openrouter.ai"): return False return _loopback_hostname(base_url_hostname(bu)) @@ -547,7 +559,20 @@ def _resolve_named_custom_runtime( # Bare `provider="custom"` with an explicit base_url (e.g. propagated # from a `model_aliases:` direct-alias resolution) — build a runtime # directly so the alias's base_url actually takes effect. + # + # GitHub #27132: provider aliases that resolve to "custom" at runtime + # (ollama, vllm, llamacpp, …) are treated identically here, so a YAML + # `provider: ollama` with a LAN/WireGuard `base_url` doesn't silently + # fall through to OpenRouter. requested_norm = (requested_provider or "").strip().lower() + if requested_norm and requested_norm != "custom": + try: + from hermes_cli.auth import resolve_provider as _resolve_provider + + if _resolve_provider(requested_norm) == "custom": + requested_norm = "custom" + except Exception: + pass if requested_norm == "custom" and explicit_base_url: base_url = explicit_base_url.strip().rstrip("/") # Check credential pool first — mirrors the named-custom-provider path @@ -638,6 +663,19 @@ def _resolve_openrouter_runtime( break requested_norm = (requested_provider or "").strip().lower() cfg_provider = cfg_provider.strip().lower() + # GitHub #27132: provider aliases that resolve to "custom" (ollama, + # vllm, llamacpp, …) follow the same base_url trust + routing rules + # as a bare `provider: custom`. Normalising here keeps every check + # below — `requested_norm == "custom"`, the trust check, the pool + # gate up the stack — alias-aware without duplicating the alias map. + if requested_norm and requested_norm != "custom": + try: + from hermes_cli.auth import resolve_provider as _resolve_provider + + if _resolve_provider(requested_norm) == "custom": + requested_norm = "custom" + except Exception: + pass env_openrouter_base_url = os.getenv("OPENROUTER_BASE_URL", "").strip() env_custom_base_url = os.getenv("CUSTOM_BASE_URL", "").strip() diff --git a/scripts/release.py b/scripts/release.py index 60779899b93..718b1079a0c 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1146,6 +1146,7 @@ AUTHOR_MAP = { "192385615+LifeJiggy@users.noreply.github.com": "LifeJiggy", # stale salvage commit alias (PR #28315) "beastant1@gmail.com": "nekwo", # PR #26481 (PS5.1 UTF-8 BOM) "43717185+nekwo@users.noreply.github.com": "nekwo", + "9785479+stepanov1975@users.noreply.github.com": "stepanov1975", # PR #22074 (setup config picker writes) "67979730+flooryyyy@users.noreply.github.com": "flooryyyy", # PR #26374 (tool_trace error detection) "188585318+dgians@users.noreply.github.com": "dgians", # PR #26034 (.ts/.py/.sh docs types) "zealy@tz.co": "dgians", # PR #26034 (bot-committed by zealy-tzco under dgians' PR) diff --git a/tests/hermes_cli/test_runtime_provider_resolution.py b/tests/hermes_cli/test_runtime_provider_resolution.py index 22c778dbab2..db2b314f2f5 100644 --- a/tests/hermes_cli/test_runtime_provider_resolution.py +++ b/tests/hermes_cli/test_runtime_provider_resolution.py @@ -2321,3 +2321,74 @@ def test_minimax_oauth_pool_forces_anthropic_messages_despite_stale_config(monke assert resolved["provider"] == "minimax-oauth" assert resolved["api_mode"] == "anthropic_messages" assert resolved["base_url"] == "https://api.minimax.io/anthropic" + + +# ---------------------------------------------------------------------- +# GitHub #27132 — provider aliases (ollama/vllm/llamacpp/llama-cpp) must +# follow the same base_url trust + routing rules as bare `provider: custom`. +# Without this, a YAML `provider: ollama` with a LAN/WireGuard `base_url` +# silently falls through to OpenRouter (HTTP 401). +# ---------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "alias,base_url", + [ + ("ollama", "http://192.168.0.103:11434/v1"), + ("vllm", "http://192.168.0.103:8000/v1"), + ("llamacpp", "http://192.168.0.103:8080/v1"), + ("llama-cpp", "http://192.168.0.103:8080/v1"), + ], +) +def test_custom_aliases_with_lan_base_url_route_to_custom_not_openrouter( + monkeypatch, alias, base_url +): + """provider: ollama|vllm|llamacpp + LAN IP must NOT fall through to OpenRouter.""" + monkeypatch.setattr( + rp, + "_get_model_config", + lambda: {"provider": alias, "base_url": base_url}, + ) + # Pretend OPENROUTER_API_KEY is set so the openrouter fallback would + # otherwise succeed — we want to prove the alias short-circuits before + # reaching it. + monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-fake-test") + # No custom credential pool — exercise the bare-alias path. + monkeypatch.setattr(rp, "load_pool", lambda provider: None) + + resolved = rp.resolve_runtime_provider() + + assert resolved["provider"] == "custom", ( + f"alias {alias!r} with LAN base_url should resolve to provider=custom, " + f"got {resolved['provider']!r}" + ) + assert resolved["base_url"] == base_url.rstrip("/"), ( + f"base_url should be the configured LAN endpoint, got {resolved['base_url']!r}" + ) + + +def test_custom_alias_with_loopback_base_url_routes_to_custom(monkeypatch): + """provider: ollama + loopback should also route to custom (regression guard).""" + monkeypatch.setattr( + rp, + "_get_model_config", + lambda: {"provider": "ollama", "base_url": "http://localhost:11434/v1"}, + ) + monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-fake-test") + monkeypatch.setattr(rp, "load_pool", lambda provider: None) + + resolved = rp.resolve_runtime_provider() + + assert resolved["provider"] == "custom" + assert resolved["base_url"] == "http://localhost:11434/v1" + + +def test_trustworthy_check_accepts_custom_aliases(): + """_config_base_url_trustworthy_for_bare_custom() must accept aliases for custom.""" + fn = rp._config_base_url_trustworthy_for_bare_custom + for alias in ("ollama", "vllm", "llamacpp", "llama-cpp", "llama.cpp"): + assert fn("http://192.168.0.103:11434/v1", alias) is True, ( + f"alias {alias!r} should be trusted with non-loopback base_url" + ) + # Unrelated provider name should still be rejected with non-loopback URL. + assert fn("http://192.168.0.103:11434/v1", "openrouter") is False