Custom provider base_url routing and config persistence

NousResearch/hermes-agent · cluster 7494 · agent

Data as of

Nothing open on GitHub. Fix merged in #28882. Check it covers your case.

The short answer

0open issues
0open PRs
5items grouped
Known?
Yes. 5 related items grouped here, 2 issues already closed.
Fix event
A fix was merged to main in PR #28882. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this partially resolved, deciding PR #28882. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and fixes concern custom provider aliases (ollama, vllm, llamacpp) incorrectly falling through to cloud/OpenRouter when a LAN or remote base_url is configured, alongside related CLI wizard config preservation problems.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #28882 fix(cli): preserve wizard config + honour custom-provider aliases (#22074, #27132)

    merged to main

    files: hermes_cli/runtime_provider.py, hermes_cli/setup.py, scripts/release.py, tests/hermes_cli/test_runtime_provider_resolution.py, tests/hermes_cli/test_setup_model_provider.py

    Show the diff
    • 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()
    • hermes_cli/setup.py
      @@ -820,13 +820,12 @@ def setup_model_provider(config: dict, *, quick: bool = False):
           # Re-sync the wizard's config dict from what cmd_model saved to disk.
           # This is critical: cmd_model writes to disk via its own load/save cycle,
           # and the wizard's final save_config(config) must not overwrite those
      -    # changes with stale values (#4172).
      +    # changes with stale values (#4172). Refresh the dict in place so callers
      +    # that keep the same object see every section the shared model picker may
      +    # have changed (model, custom_providers, auxiliary, provider metadata, etc.).
           _refreshed = load_config()
      -    config["model"] = _refreshed.get("model", config.get("model"))
      -    if "custom_providers" in _refreshed:
      -        config["custom_providers"] = _refreshed["custom_providers"]
      -    else:
      -        config.pop("custom_providers", None)
      +    config.clear()
      +    config.update(_refreshed)
       
           # Derive the selected provider for downstream steps (vision setup).
           selected_provider = None
    • scripts/release.py
      @@ -1145,6 +1145,7 @@
           "[redacted:email]": "LifeJiggy",  # stale salvage commit alias (PR #28315)
           "[redacted:email]": "nekwo",  # PR #26481 (PS5.1 UTF-8 BOM)
           "[redacted:email]": "nekwo",
      +    "[redacted:email]": "stepanov1975",  # PR #22074 (setup config picker writes)
           "[redacted:email]": "flooryyyy",  # PR #26374 (tool_trace error detection)
           "[redacted:email]": "dgians",  # PR #26034 (.ts/.py/.sh docs types)
           "[redacted:email]": "dgians",  # PR #26034 (bot-committed by zealy-tzco under dgians' PR)
    • tests/hermes_cli/test_runtime_provider_resolution.py
      @@ -2321,3 +2321,74 @@ def select(self):
           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).
      +# ----------------------------------------------------------------------
      +
      +
      [redacted:email](
      +    "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
    • tests/hermes_cli/test_setup_model_provider.py
      @@ -63,6 +63,38 @@ def _write_model_config(provider, base_url="", model_name="test-model"):
           save_config(cfg)
       
       
      +def _write_aux_config(task="compression", provider="gemini", model_name="gemini-2.5-flash"):
      +    """Simulate the aux picker writing a task override to disk."""
      +    cfg = load_config()
      +    aux = cfg.setdefault("auxiliary", {})
      +    entry = aux.setdefault(task, {})
      +    entry["provider"] = provider
      +    entry["model"] = model_name
      +    save_config(cfg)
      +
      +
      +def test_setup_model_provider_preserves_auxiliary_choices_written_by_picker(tmp_path, monkeypatch):
      +    """Aux choices made inside hermes setup must survive the wizard's final save."""
      +    monkeypatch.setenv("HERMES_HOME", str(tmp_path))
      +    _clear_provider_env(monkeypatch)
      +
      +    config = load_config()
      +    assert config["auxiliary"]["compression"]["provider"] == "auto"
      +
      +    def fake_select():
      +        _write_aux_config("compression", "gemini", "gemini-2.5-flash")
      +
      +    monkeypatch.setattr("hermes_cli.main.select_provider_and_model", fake_select)
      +
      +    setup_model_provider(config, quick=True)
      +    save_config(config)  # mirrors run_setup_wizard(section="model") final save
      +
      +    reloaded = load_config()
      +    compression = reloaded["auxiliary"]["compression"]
      +    assert compression["provider"] == "gemini"
      +    assert compression["model"] == "gemini-2.5-flash"
      +
      +
       def test_setup_keep_current_custom_from_config_does_not_fall_through(tmp_path, monkeypatch):
           """Keep-current custom should not fall through to the generic model menu."""
           monkeypatch.setenv("HERMES_HOME", str(tmp_path))

2 PRs closed without merging

From the close comments: done elsewhere 1 · merged via another change 1

  • #22074 fix(cli): preserve setup config picker writes · merged via another change → #28882, #27132
  • #27133 fix: trust custom provider aliases (ollama/vllm/llamacpp) for LAN/remote base_url · done elsewhere
Closed issues · 2
  • Closed#7494[Bug]: Remote Ollama endpoint not working — only connects via localhost
  • Closed#27132Provider aliases (ollama/vllm/llamacpp) silently fall through to OpenRouter when base_url is a LAN or remote IP
How this cluster was grouped
  • Link evidence (partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
  • Members outside that structural core may belong to a different underlying problem.
  • Pairwise link density 0.34: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#28882
Closed issues
2
Attempts
2 PRs closed without merging
Fix link checked
19 May 2026

For your agent

The same facts, typed.