NousResearch/hermes-agent

Provider config, auth, and API error fixes

Cluster 3846 · gateway. Data as of

Nothing open on GitHub. Fix merged in #54441 on 28 Jun 2026. Check it covers your case.

The short answer

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

These issues and PRs cover several adjacent but distinct themes: provider routing bugs (auto+base_url, OpenCode Go/Zen showing wrong models), authentication and credential problems (Nous 401 refresh, Telegram auth loss, OAuth gate 401s), and general API errors. No single unified theme, but most relate to provider configuration or auth failures.

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.

  • #54441 fix(provider): auto+base_url bypasses cloud API when custom endpoint configured (#3846)

    merged to main

    files: hermes_cli/runtime_provider.py, tests/hermes_cli/test_runtime_provider_resolution.py

    Show the diff
    • hermes_cli/runtime_provider.py
      @@ -1454,6 +1454,43 @@ def resolve_runtime_provider(
               custom_runtime["requested_provider"] = requested_provider
               return custom_runtime
       
      +    # If provider is "auto" (or unset) but config.yaml has an explicit base_url
      +    # pointing at a custom/local endpoint (e.g. Ollama at localhost:11434),
      +    # route through the OpenAI-compatible resolver instead of letting
      +    # resolve_provider() pick up an ANTHROPIC_API_KEY or OPENAI_API_KEY from
      +    # the environment and send the request to a cloud API. Fixes #3846.
      +    if not explicit_base_url and not explicit_api_key:
      +        model_cfg = _get_model_config()
      +        cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
      +        cfg_base_url = str(model_cfg.get("base_url") or "").strip()
      +        if cfg_base_url and cfg_provider in ("auto", ""):
      +            # Check that base_url isn't one of the well-known cloud API roots
      +            # (OpenRouter, Anthropic, OpenAI). If it's something else (Ollama,
      +            # LM Studio, vLLM, …) we honour it directly. The full detection
      +            # logic lives in _resolve_openrouter_runtime; we just skip the
      +            # resolve_provider() call so env-var credentials don't shadow it.
      +            # Match on HOST, not substring, so a look-alike base_url
      +            # (e.g. http://api.anthropic.com.attacker.test/v1, or one whose
      +            # path merely contains "openai.com") cannot evade the bypass and
      +            # leak a cloud credential. Mirrors the host-gating used for
      +            # API-key selection in _resolve_openrouter_runtime.
      +            _known_cloud_hosts = (
      +                "openrouter.ai",
      +                "anthropic.com",
      +                "openai.com",
      +            )
      +            if not any(
      +                base_url_host_matches(cfg_base_url, host)
      +                for host in _known_cloud_hosts
      +            ):
      +                runtime = _resolve_openrouter_runtime(
      +                    requested_provider=requested_provider,
      +                    explicit_api_key=[redacted:secret-assignment]
      +                    explicit_base_url=explicit_base_url,
      +                )
      +                runtime["requested_provider"] = requested_provider
      +                return runtime
      +
           provider = resolve_provider(
               requested_provider,
               explicit_api_key=[redacted:secret-assignment]
    • tests/hermes_cli/test_runtime_provider_resolution.py
      @@ -2893,3 +2893,102 @@ def test_resolve_runtime_provider_bedrock_nonclaude_target_model_uses_converse(m
           assert resolved["provider"] == "bedrock"
           assert resolved["api_mode"] == "bedrock_converse"
           assert resolved.get("bedrock_anthropic") is not True
      +
      +
      +def test_auto_provider_with_local_base_url_bypasses_anthropic_key(monkeypatch):
      +    """provider:auto + base_url:localhost should NOT route to Anthropic even if
      +    ANTHROPIC_API_KEY is set in the environment. Regression test for #3846.
      +
      +    Users running Ollama locally had requests silently sent to Anthropic's API
      +    because resolve_provider("auto") picked up ANTHROPIC_API_KEY before the
      +    config.yaml base_url was checked.
      +    """
      +    # ANTHROPIC_API_KEY is present in environment — should be ignored
      +    monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-fake-key")
      +    monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
      +    monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
      +    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
      +    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
      +
      +    monkeypatch.setattr(
      +        rp,
      +        "_get_model_config",
      +        lambda: {
      +            "default": "ollama/minimax-m2.7:cloud",
      +            "provider": "auto",
      +            "base_url": "http://localhost:11434",
      +        },
      +    )
      +
      +    resolved = rp.resolve_runtime_provider()
      +
      +    # Must NOT go to Anthropic's API
      +    assert "anthropic.com" not in resolved.get("base_url", ""), (
      +        f"Expected custom endpoint, got Anthropic: {resolved}"
      +    )
      +    assert resolved["base_url"] == "http://localhost:11434"
      +    # provider should be custom/openrouter (OpenAI-compat), not anthropic
      +    assert resolved["provider"] != "anthropic", (
      +        f"Should have routed to custom endpoint, not anthropic: {resolved}"
      +    )
      +
      +
      +def test_auto_provider_with_known_cloud_base_url_still_uses_anthropic(monkeypatch):
      +    """provider:auto + base_url pointing to Anthropic should still use Anthropic.
      +
      +    The local-endpoint bypass only applies to non-cloud endpoints; when the
      +    configured base_url IS a cloud API root, resolve_provider() must run
      +    normally and pick up ANTHROPIC_API_KEY.
      +    """
      +    monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-fake-key")
      +    monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
      +    monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
      +    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
      +    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
      +
      +    monkeypatch.setattr(
      +        rp,
      +        "_get_model_config",
      +        lambda: {
      +            "provider": "auto",
      +            "base_url": "https://api.anthropic.com",
      +        },
      +    )
      +
      +    resolved = rp.resolve_runtime_provider()
      +
      +    # Cloud base_url → bypass does NOT fire → resolve_provider picks anthropic.
      +    assert resolved["provider"] == "anthropic", (
      +        f"Cloud base_url must not be diverted to the custom resolver: {resolved}"
      +    )
      +
      +
      +def test_auto_provider_lookalike_cloud_host_does_not_bypass_to_cloud(monkeypatch):
      +    """A look-alike host (api.anthropic.com.attacker.test) must be treated as a
      +    custom endpoint, NOT mistaken for the real Anthropic cloud.
      +
      +    Guards the host-match (vs naive substring) check in the local-endpoint
      +    bypass: substring matching on "api.anthropic.com" would wrongly classify
      +    this attacker-controlled host as cloud and hand it the ANTHROPIC_API_KEY.
      +    """
      +    monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-fake-key")
      +    monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
      +    monkeypatch.delenv("OPENROUTER_BASE_URL", raising=False)
      +    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
      +    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
      +
      +    lookalike = "http://api.anthropic.com.attacker.test/v1"
      +    monkeypatch.setattr(
      +        rp,
      +        "_get_model_config",
      +        lambda: {"provider": "auto", "base_url": lookalike},
      +    )
      +
      +    resolved = rp.resolve_runtime_provider()
      +
      +    # Host doesn't actually match anthropic.com → bypass fires → custom route,
      +    # and the real Anthropic credential is NOT sent there.
      +    assert resolved["provider"] != "anthropic", (
      +        f"Look-alike host must not be classified as Anthropic cloud: {resolved}"
      +    )
      +    assert resolved["base_url"] == lookalike

1 PR closed without merging

From the close comments: merged via another change 1

  • #3997 fix(provider): auto+base_url bypasses cloud API when custom endpoint configured · merged via another change → b0b7ff0, #54441, #3846
Closed issues · 1
  • Closed#3846[Bug]: telegram bot responds with random error i dont know how to fix!
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: 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
gateway
Merged PRs
#54441
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
28 Jun 2026

For your agent

The same facts, typed.