mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(model-switch): drop stale provider from fallback chain and env after /model
Reported during the TUI v2 blitz test: switching from openrouter to anthropic via `/model <name> --provider anthropic` appeared to succeed, but the next turn kept hitting openrouter — the provider the user was deliberately moving away from. Two gaps caused this: 1. `Agent.switch_model` reset `_fallback_activated` / `_fallback_index` but left `_fallback_chain` intact. The chain was seeded from `fallback_providers:` at agent init for the *original* primary, so when the new primary returned 401 (invalid/expired Anthropic key), `_try_activate_fallback()` picked the old provider back up without informing the user. Prune entries matching either the old primary (user is moving away) or the new primary (redundant) whenever the primary provider actually changes. 2. `_apply_model_switch` persisted `HERMES_MODEL` but never updated `HERMES_INFERENCE_PROVIDER`. Any ambient re-resolution of the runtime (credential pool refresh, compressor rebuild, aux clients) falls through to that env var in `resolve_requested_provider`, so it kept reporting the original provider even after an in-memory switch. Adds three regression tests: fallback-chain prune on primary change, no-op on same-provider model swap, and env-var sync on explicit switch.
This commit is contained in:
parent
ce98e1ef11
commit
f0b763c74f
4 changed files with 158 additions and 0 deletions
93
tests/run_agent/test_switch_model_fallback_prune.py
Normal file
93
tests/run_agent/test_switch_model_fallback_prune.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
"""Regression test for TUI v2 blitz bug: explicit /model --provider switch
|
||||
silently fell back to the old primary provider on the next turn because the
|
||||
fallback chain — seeded from config at agent __init__ — kept entries for the
|
||||
provider the user just moved away from.
|
||||
|
||||
Reported: "switched from openrouter provider to anthropic api key via hermes
|
||||
model and the tui keeps trying openrouter".
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from run_agent import AIAgent
|
||||
|
||||
|
||||
def _make_agent(chain):
|
||||
agent = AIAgent.__new__(AIAgent)
|
||||
|
||||
agent.provider = "openrouter"
|
||||
agent.model = "x-ai/grok-4"
|
||||
agent.base_url = "https://openrouter.ai/api/v1"
|
||||
agent.api_key = "or-key"
|
||||
agent.api_mode = "chat_completions"
|
||||
agent.client = MagicMock()
|
||||
agent._client_kwargs = {"api_key": "or-key", "base_url": "https://openrouter.ai/api/v1"}
|
||||
agent.context_compressor = None
|
||||
agent._anthropic_api_key = ""
|
||||
agent._anthropic_base_url = None
|
||||
agent._anthropic_client = None
|
||||
agent._is_anthropic_oauth = False
|
||||
agent._cached_system_prompt = "cached"
|
||||
agent._primary_runtime = {}
|
||||
agent._fallback_activated = False
|
||||
agent._fallback_index = 0
|
||||
agent._fallback_chain = list(chain)
|
||||
agent._fallback_model = chain[0] if chain else None
|
||||
|
||||
return agent
|
||||
|
||||
|
||||
def _switch_to_anthropic(agent):
|
||||
with (
|
||||
patch("agent.anthropic_adapter.build_anthropic_client", return_value=MagicMock()),
|
||||
patch("agent.anthropic_adapter.resolve_anthropic_token", return_value="sk-ant-xyz"),
|
||||
patch("agent.anthropic_adapter._is_oauth_token", return_value=False),
|
||||
patch("hermes_cli.timeouts.get_provider_request_timeout", return_value=None),
|
||||
):
|
||||
agent.switch_model(
|
||||
new_model="claude-sonnet-4-5",
|
||||
new_provider="anthropic",
|
||||
api_key="sk-ant-xyz",
|
||||
base_url="https://api.anthropic.com",
|
||||
api_mode="anthropic_messages",
|
||||
)
|
||||
|
||||
|
||||
def test_switch_drops_old_primary_from_fallback_chain():
|
||||
agent = _make_agent([
|
||||
{"provider": "openrouter", "model": "x-ai/grok-4"},
|
||||
{"provider": "nous", "model": "hermes-4"},
|
||||
])
|
||||
|
||||
_switch_to_anthropic(agent)
|
||||
|
||||
providers = [entry["provider"] for entry in agent._fallback_chain]
|
||||
|
||||
assert "openrouter" not in providers, "old primary must be pruned"
|
||||
assert "anthropic" not in providers, "new primary is redundant in the chain"
|
||||
assert providers == ["nous"]
|
||||
assert agent._fallback_model == {"provider": "nous", "model": "hermes-4"}
|
||||
|
||||
|
||||
def test_switch_with_empty_chain_stays_empty():
|
||||
agent = _make_agent([])
|
||||
|
||||
_switch_to_anthropic(agent)
|
||||
|
||||
assert agent._fallback_chain == []
|
||||
assert agent._fallback_model is None
|
||||
|
||||
|
||||
def test_switch_within_same_provider_preserves_chain():
|
||||
chain = [{"provider": "openrouter", "model": "x-ai/grok-4"}]
|
||||
agent = _make_agent(chain)
|
||||
|
||||
with patch("hermes_cli.timeouts.get_provider_request_timeout", return_value=None):
|
||||
agent.switch_model(
|
||||
new_model="openai/gpt-5",
|
||||
new_provider="openrouter",
|
||||
api_key="or-key",
|
||||
base_url="https://openrouter.ai/api/v1",
|
||||
)
|
||||
|
||||
assert agent._fallback_chain == chain
|
||||
Loading…
Add table
Add a link
Reference in a new issue