Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2658,6 +2658,41 @@ def switch_model(self, new_model, new_provider, api_key='', base_url='', api_mod
# ── LM Studio: preload before probing context length ──
self._ensure_lmstudio_runtime_loaded()

# ── Re-resolve config_context_length for the new provider ──
# The top-level model.context_length (stored as _config_context_length
# at startup) can shadow the new provider's context_length when
# switching providers mid-session. Clear it and re-resolve from the
# new provider's config entry.
try:
from hermes_cli.config import load_config, get_custom_provider_context_length
_sm_fresh_cfg = load_config()
_sm_cfg_ctx = _sm_fresh_cfg.get("model", {})
_sm_new_cfg_ctx = _sm_cfg_ctx.get("context_length") if isinstance(_sm_cfg_ctx, dict) else None
# Only use the top-level context_length if the new provider IS the
# default provider — otherwise it belongs to a different endpoint.
_sm_default_provider = (_sm_cfg_ctx.get("provider") or "") if isinstance(_sm_cfg_ctx, dict) else ""
if new_provider and _sm_default_provider and new_provider != _sm_default_provider:
_sm_new_cfg_ctx = None
if _sm_new_cfg_ctx is not None:
try:
self._config_context_length = int(_sm_new_cfg_ctx)
except (TypeError, ValueError):
self._config_context_length = None
else:
# Try per-provider custom_providers context_length
_sm_cps = None
try:
from hermes_cli.config import get_compatible_custom_providers
_sm_cps = get_compatible_custom_providers(_sm_fresh_cfg)
except Exception:
pass
_sm_cp_ctx = get_custom_provider_context_length(
new_model, self.base_url, custom_providers=_sm_cps, config=_sm_fresh_cfg
)
self._config_context_length = int(_sm_cp_ctx) if _sm_cp_ctx else None
except Exception:
pass

# ── Update context compressor ──
if hasattr(self, "context_compressor") and self.context_compressor:
from agent.model_metadata import get_model_context_length
Expand Down
79 changes: 79 additions & 0 deletions tests/run_agent/test_switch_model_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,82 @@ def test_switch_model_without_config_context_length():
mock_ctx_len.assert_called_once()
call_kwargs = mock_ctx_len.call_args.kwargs
assert call_kwargs.get("config_context_length") is None


# ── Provider-switching regression tests ──────────────────────────────
# Covers the bug where top-level model.context_length (1M) persists
# across /model switches and shadows the new provider's context_length.


@patch("run_agent.load_config")
@patch("hermes_cli.config.get_custom_provider_context_length", return_value=None)
@patch("hermes_cli.config.get_compatible_custom_providers", return_value=[])
@patch("agent.model_metadata.get_model_context_length", return_value=8_192)
def test_switch_to_local_provider_clears_stale_config_context(
mock_ctx_len, mock_cps, mock_cp_ctx, mock_load_config
):
"""Switching from cloud (1M ctx) to local (8K ctx) should not shadow
the local provider's context_length with the stale 1M value."""
mock_load_config.return_value = {
"model": {"default": "mimo-pro", "provider": "custom", "context_length": 1_000_000}
}

agent = _make_agent_with_compressor(config_context_length=1_000_000)
assert agent._config_context_length == 1_000_000

agent.switch_model("local-model", "custom:local", base_url="http://localhost:8080/v1")

# _config_context_length should be cleared (None) since the new provider
# is not the default provider
assert agent._config_context_length is None
# get_model_context_length should have been called with None
mock_ctx_len.assert_called_once()
call_kwargs = mock_ctx_len.call_args.kwargs
assert call_kwargs.get("config_context_length") is None


@patch("run_agent.load_config")
@patch("hermes_cli.config.get_custom_provider_context_length", return_value=None)
@patch("hermes_cli.config.get_compatible_custom_providers", return_value=[])
@patch("agent.model_metadata.get_model_context_length", return_value=8_192)
def test_switch_to_local_no_explicit_ctx(
mock_ctx_len, mock_cps, mock_cp_ctx, mock_load_config
):
"""When config has no explicit context_length and we switch providers,
_config_context_length should be re-resolved (None if no override)."""
mock_load_config.return_value = {
"model": {"default": "mimo-pro", "provider": "custom"}
}

agent = _make_agent_with_compressor(config_context_length=None)

agent.switch_model("local-model", "custom:local", base_url="http://localhost:8080/v1")

assert agent._config_context_length is None
mock_ctx_len.assert_called_once()
call_kwargs = mock_ctx_len.call_args.kwargs
assert call_kwargs.get("config_context_length") is None


@patch("run_agent.load_config")
@patch("hermes_cli.config.get_custom_provider_context_length", return_value=None)
@patch("hermes_cli.config.get_compatible_custom_providers", return_value=[])
@patch("agent.model_metadata.get_model_context_length", return_value=1_000_000)
def test_same_provider_keeps_config_context(
mock_ctx_len, mock_cps, mock_cp_ctx, mock_load_config
):
"""Switching models within the same provider should preserve the
config context_length."""
mock_load_config.return_value = {
"model": {"default": "mimo-pro", "provider": "custom", "context_length": 1_000_000}
}

agent = _make_agent_with_compressor(config_context_length=1_000_000)

# Same provider (custom) — context_length should be preserved
agent.switch_model("another-model", "custom", base_url="http://localhost:4000/v1")

assert agent._config_context_length == 1_000_000
mock_ctx_len.assert_called_once()
call_kwargs = mock_ctx_len.call_args.kwargs
assert call_kwargs.get("config_context_length") == 1_000_000