NousResearch/hermes-agent

Stale base_url retained when switching model provider

Cluster 47828 · agent. Data as of

Nothing open on GitHub. Fix merged in #60985 on 9 Jul 2026. Check it covers your case.

The short answer

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

These items appear to concern one theme: when a model or provider is switched in place (e.g. via /mode), the previous provider's base_url may stay attached to the new model, with one report describing a 400 response. Several members are titled as fixes that clear or refuse the stale base_url, but their actual status is not established here.

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.

  • #60985 fix(agent): switch_model refuses stale base_url on provider change (#47828)

    merged to main

    files: agent/agent_runtime_helpers.py, tests/run_agent/test_switch_model_stale_base_url.py

    Show the diff
    • agent/agent_runtime_helpers.py
      @@ -1805,13 +1805,30 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo
               # ── Swap core runtime fields ──
               agent.model = new_model
               agent.provider = new_provider
      -        # Use new base_url when provided; only fall back to current when the
      -        # new provider genuinely has no endpoint (e.g. native SDK providers).
      -        # Without this guard the old provider's URL (e.g. Ollama's localhost
      -        # address) would persist silently after switching to a cloud provider
      -        # that returns an empty base_url string.
      +        # Use the new base_url when provided. When it's empty AND the
      +        # provider is actually changing, do NOT fall back to the current
      +        # (old provider's) URL — that silently pairs the new provider label
      +        # with the previous provider's endpoint (e.g. new_provider=minimax
      +        # paired with the leftover api.githubcopilot.com URL), and every
      +        # request after the switch 400s at the wrong host. This mismatched
      +        # pair also gets snapshotted into _primary_runtime below, so it
      +        # keeps re-applying on every subsequent turn until a full restart.
      +        # Fail loud instead: the caller (model_switch.switch_model())
      +        # already resolves base_url for every real provider, so an empty
      +        # value here means resolution failed upstream, not that the
      +        # provider genuinely has none. Re-selecting the SAME provider with
      +        # an empty base_url (e.g. a credential-only refresh) is still fine
      +        # to keep the current URL. See #47828.
      +        old_norm_provider = (old_provider or "").strip().lower()
      +        new_norm_provider = (new_provider or "").strip().lower()
               if base_url:
                   agent.base_url = base_url
      +        elif old_norm_provider != new_norm_provider:
      +            raise ValueError(
      +                f"switch_model: no base_url resolved for provider "
      +                f"'{new_provider}' (switching from '{old_provider}'); "
      +                "refusing to keep the previous provider's endpoint"
      +            )
               agent.api_mode = api_mode
               # Invalidate transport cache — new api_mode may need a different transport
               if hasattr(agent, "_transport_cache"):
    • tests/run_agent/test_switch_model_stale_base_url.py
      @@ -0,0 +1,87 @@
      +"""Regression tests for #47828: switch_model must not pair a new provider
      +label with the previous provider's base_url when the resolver returns no
      +new base_url for a genuine provider change.
      +"""
      +
      +from unittest.mock import MagicMock, patch
      +
      +import pytest
      +
      +from run_agent import AIAgent
      +from agent.context_compressor import ContextCompressor
      +
      +
      +def _make_agent_with_compressor(provider="copilot", base_url="https://api.githubcopilot.com") -> AIAgent:
      +    """Build a minimal AIAgent with a context_compressor, skipping __init__."""
      +    agent = AIAgent.__new__(AIAgent)
      +
      +    agent.model = "claude-opus-4.8"
      +    agent.provider = provider
      +    agent.base_url = base_url
      +    agent.api_key = "[redacted:secret-assignment]"
      +    agent.api_mode = "chat_completions"
      +    agent.client = MagicMock()
      +    agent.quiet_mode = True
      +    agent._config_context_length = None
      +
      +    compressor = ContextCompressor(
      +        model=agent.model,
      +        threshold_percent=0.50,
      +        base_url=base_url,
      +        api_key="[redacted:secret-assignment]",
      +        provider=provider,
      +        quiet_mode=True,
      +        config_context_length=None,
      +    )
      +    agent.context_compressor = compressor
      +    agent._primary_runtime = {}
      +
      +    return agent
      +
      +
      +@patch("agent.model_metadata.get_model_context_length", return_value=131_072)
      +def test_switch_model_rejects_stale_base_url_on_provider_change(mock_ctx_len):
      +    """A provider change with no resolved base_url must fail loud instead of
      +    silently keeping the previous provider's endpoint (#47828)."""
      +    agent = _make_agent_with_compressor(provider="copilot", base_url="https://api.githubcopilot.com")
      +
      +    with pytest.raises(ValueError, match="no base_url resolved"):
      +        agent.switch_model("MiniMax-M3", "custom:minimax", api_key="[redacted:secret-assignment]", base_url="")
      +
      +    # Rollback must leave the agent fully on the old (provider, base_url) pair —
      +    # not a mismatched new-model/old-endpoint hybrid.
      +    assert agent.provider == "copilot"
      +    assert agent.base_url == "https://api.githubcopilot.com"
      +    assert agent.model == "claude-opus-4.8"
      +
      +
      +@patch("agent.model_metadata.get_model_context_length", return_value=131_072)
      +def test_switch_model_allows_empty_base_url_for_same_provider(mock_ctx_len):
      +    """Re-selecting the SAME provider (e.g. a credential-only refresh) with no
      +    new base_url must keep the current URL — this is not a provider change."""
      +    agent = _make_agent_with_compressor(provider="openrouter", base_url="https://openrouter.ai/api/v1")
      +
      +    agent.switch_model("new-model", "openrouter", api_key="[redacted:secret-assignment]", base_url="")
      +
      +    assert agent.provider == "openrouter"
      +    assert agent.base_url == "https://openrouter.ai/api/v1"
      +    assert agent.model == "new-model"
      +
      +
      +@patch("agent.model_metadata.get_model_context_length", return_value=131_072)
      +def test_switch_model_applies_new_base_url_on_provider_change(mock_ctx_len):
      +    """The normal, resolved-correctly path must still work: new provider +
      +    new base_url is applied as-is."""
      +    agent = _make_agent_with_compressor(provider="copilot", base_url="https://api.githubcopilot.com")
      +
      +    agent.switch_model(
      +        "MiniMax-M3", "custom:minimax", api_key="[redacted:secret-assignment]", base_url="https://api.minimax.io/v1"
      +    )
      +
      +    assert agent.provider == "custom:minimax"
      +    assert agent.base_url == "https://api.minimax.io/v1"
      +    assert agent.model == "MiniMax-M3"
      +    # _primary_runtime must snapshot the coherent pair so it survives every
      +    # subsequent restore_primary_runtime() call across turns.
      +    assert agent._primary_runtime["provider"] == "custom:minimax"
      +    assert agent._primary_runtime["base_url"] == "https://api.minimax.io/v1"

2 PRs closed without merging

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

  • #47934 fix(model): clear stale base URL on provider switch · done elsewhere → a23d507, #47828, #60931
  • #60931 fix(agent): switch_model no longer pairs new provider with stale base_url · merged via another change → a23d507, #60985, #47828
Closed issues · 1
  • Closed#47828[Bug]: /mode in-place model switch keeps the OLD provider's endpoint — new model fired at previous provider's base_url → 400
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.50: 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
#60985
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
9 Jul 2026

For your agent

The same facts, typed.