refactor: remove /model slash command from CLI and gateway (#3080)
The /model command is removed from both the interactive CLI and messenger gateway (Telegram/Discord/Slack/WhatsApp). Users can still change models via 'hermes model' CLI subcommand or by editing config.yaml directly. Removed: - CommandDef entry from COMMAND_REGISTRY - CLI process_command() handler and model autocomplete logic - Gateway _handle_model_command() and dispatch - SlashCommandCompleter model_completer_provider parameter - Two-stage Tab completion and ghost text for /model - All /model-specific tests Unaffected: - /provider command (read-only, shows current model + providers) - ACP adapter _cmd_model (separate system for VS Code/Zed/JetBrains) - model_switch.py module (used by ACP) - 'hermes model' CLI subcommand Author: Teknium
This commit is contained in:
@@ -389,72 +389,6 @@ class TestSubcommandCompletion:
|
||||
assert completions == []
|
||||
|
||||
|
||||
# ── Two-stage /model completion ─────────────────────────────────────────
|
||||
|
||||
|
||||
def _model_completer() -> SlashCommandCompleter:
|
||||
"""Build a completer with mock model/provider info."""
|
||||
return SlashCommandCompleter(
|
||||
model_completer_provider=lambda: {
|
||||
"current_provider": "openrouter",
|
||||
"providers": {
|
||||
"anthropic": "Anthropic",
|
||||
"openrouter": "OpenRouter",
|
||||
"nous": "Nous Research",
|
||||
},
|
||||
"models_for": lambda p: {
|
||||
"anthropic": ["claude-sonnet-4-20250514", "claude-opus-4-20250414"],
|
||||
"openrouter": ["anthropic/claude-sonnet-4", "google/gemini-2.5-pro"],
|
||||
"nous": ["hermes-3-llama-3.1-405b"],
|
||||
}.get(p, []),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TestModelCompletion:
|
||||
def test_stage1_shows_providers(self):
|
||||
completions = _completions(_model_completer(), "/model ")
|
||||
texts = {c.text for c in completions}
|
||||
assert "anthropic:" in texts
|
||||
assert "openrouter:" in texts
|
||||
assert "nous:" in texts
|
||||
|
||||
def test_stage1_current_provider_last(self):
|
||||
completions = _completions(_model_completer(), "/model ")
|
||||
texts = [c.text for c in completions]
|
||||
assert texts[-1] == "openrouter:"
|
||||
|
||||
def test_stage1_current_provider_labeled(self):
|
||||
completions = _completions(_model_completer(), "/model ")
|
||||
for c in completions:
|
||||
if c.text == "openrouter:":
|
||||
assert "current" in c.display_meta_text.lower()
|
||||
break
|
||||
else:
|
||||
raise AssertionError("openrouter: not found in completions")
|
||||
|
||||
def test_stage1_prefix_filters(self):
|
||||
completions = _completions(_model_completer(), "/model an")
|
||||
texts = {c.text for c in completions}
|
||||
assert texts == {"anthropic:"}
|
||||
|
||||
def test_stage2_shows_models(self):
|
||||
completions = _completions(_model_completer(), "/model anthropic:")
|
||||
texts = {c.text for c in completions}
|
||||
assert "anthropic:claude-sonnet-4-20250514" in texts
|
||||
assert "anthropic:claude-opus-4-20250414" in texts
|
||||
|
||||
def test_stage2_prefix_filters_models(self):
|
||||
completions = _completions(_model_completer(), "/model anthropic:claude-s")
|
||||
texts = {c.text for c in completions}
|
||||
assert "anthropic:claude-sonnet-4-20250514" in texts
|
||||
assert "anthropic:claude-opus-4-20250414" not in texts
|
||||
|
||||
def test_stage2_no_model_provider_returns_empty(self):
|
||||
completions = _completions(SlashCommandCompleter(), "/model ")
|
||||
assert completions == []
|
||||
|
||||
|
||||
# ── Ghost text (SlashCommandAutoSuggest) ────────────────────────────────
|
||||
|
||||
|
||||
@@ -492,15 +426,3 @@ class TestGhostText:
|
||||
|
||||
def test_no_suggestion_for_non_slash(self):
|
||||
assert _suggestion("hello") is None
|
||||
|
||||
def test_model_stage1_ghost_text(self):
|
||||
"""/model a → 'nthropic:'"""
|
||||
completer = _model_completer()
|
||||
assert _suggestion("/model a", completer=completer) == "nthropic:"
|
||||
|
||||
def test_model_stage2_ghost_text(self):
|
||||
"""/model anthropic:cl → rest of first matching model"""
|
||||
completer = _model_completer()
|
||||
s = _suggestion("/model anthropic:cl", completer=completer)
|
||||
assert s is not None
|
||||
assert s.startswith("aude-")
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
"""Regression tests for the `/model` slash command in the interactive CLI."""
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from cli import HermesCLI
|
||||
|
||||
|
||||
class TestModelCommand:
|
||||
def _make_cli(self):
|
||||
cli_obj = HermesCLI.__new__(HermesCLI)
|
||||
cli_obj.model = "anthropic/claude-opus-4.6"
|
||||
cli_obj.agent = object()
|
||||
cli_obj.provider = "openrouter"
|
||||
cli_obj.requested_provider = "openrouter"
|
||||
cli_obj.base_url = "https://openrouter.ai/api/v1"
|
||||
cli_obj.api_key = "test-key"
|
||||
cli_obj._explicit_api_key = None
|
||||
cli_obj._explicit_base_url = None
|
||||
return cli_obj
|
||||
|
||||
def test_valid_model_from_api_saved_to_config(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.models.fetch_api_models",
|
||||
return_value=["anthropic/claude-sonnet-4.5", "openai/gpt-5.4"]), \
|
||||
patch("cli.save_config_value", return_value=True) as save_mock:
|
||||
cli_obj.process_command("/model anthropic/claude-sonnet-4.5")
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "saved to config" in output
|
||||
assert cli_obj.model == "anthropic/claude-sonnet-4.5"
|
||||
save_mock.assert_called_once_with("model.default", "anthropic/claude-sonnet-4.5")
|
||||
|
||||
def test_unlisted_model_accepted_with_warning(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.models.fetch_api_models",
|
||||
return_value=["anthropic/claude-opus-4.6"]), \
|
||||
patch("cli.save_config_value") as save_mock:
|
||||
cli_obj.process_command("/model anthropic/fake-model")
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "not found" in output or "Model changed" in output
|
||||
assert cli_obj.model == "anthropic/fake-model" # accepted
|
||||
|
||||
def test_api_unreachable_accepts_and_persists(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.models.fetch_api_models", return_value=None), \
|
||||
patch("cli.save_config_value") as save_mock:
|
||||
cli_obj.process_command("/model anthropic/claude-sonnet-next")
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "saved to config" in output
|
||||
assert cli_obj.model == "anthropic/claude-sonnet-next"
|
||||
save_mock.assert_called_once()
|
||||
|
||||
def test_no_slash_model_accepted_with_warning(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.models.fetch_api_models",
|
||||
return_value=["openai/gpt-5.4"]) as fetch_mock, \
|
||||
patch("cli.save_config_value") as save_mock:
|
||||
cli_obj.process_command("/model gpt-5.4")
|
||||
|
||||
output = capsys.readouterr().out
|
||||
# Auto-detection remaps bare model names to proper OpenRouter slugs
|
||||
assert cli_obj.model == "openai/gpt-5.4"
|
||||
|
||||
def test_validation_crash_falls_back_to_save(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.models.validate_requested_model",
|
||||
side_effect=RuntimeError("boom")), \
|
||||
patch("cli.save_config_value", return_value=True) as save_mock:
|
||||
cli_obj.process_command("/model anthropic/claude-sonnet-4.5")
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "saved to config" in output
|
||||
assert cli_obj.model == "anthropic/claude-sonnet-4.5"
|
||||
save_mock.assert_called_once()
|
||||
|
||||
def test_show_model_when_no_argument(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
cli_obj.process_command("/model")
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "anthropic/claude-opus-4.6" in output
|
||||
assert "OpenRouter" in output
|
||||
assert "Authenticated providers" in output or "Switch model" in output
|
||||
assert "provider" in output and "model" in output
|
||||
|
||||
# -- provider switching tests -------------------------------------------
|
||||
|
||||
def test_provider_colon_model_switches_provider(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.runtime_provider.resolve_runtime_provider", return_value={
|
||||
"provider": "zai",
|
||||
"api_key": "zai-key",
|
||||
"base_url": "https://api.z.ai/api/paas/v4",
|
||||
}), \
|
||||
patch("hermes_cli.models.fetch_api_models",
|
||||
return_value=["glm-5", "glm-4.7"]), \
|
||||
patch("cli.save_config_value", return_value=True) as save_mock:
|
||||
cli_obj.process_command("/model zai:glm-5")
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "glm-5" in output
|
||||
assert "provider:" in output.lower() or "Z.AI" in output
|
||||
assert cli_obj.model == "glm-5"
|
||||
assert cli_obj.provider == "zai"
|
||||
assert cli_obj.base_url == "https://api.z.ai/api/paas/v4"
|
||||
# Model, provider, and base_url should be saved
|
||||
assert save_mock.call_count == 3
|
||||
save_calls = [c.args for c in save_mock.call_args_list]
|
||||
assert ("model.default", "glm-5") in save_calls
|
||||
assert ("model.provider", "zai") in save_calls
|
||||
# base_url is also persisted on provider change (Phase 2 fix)
|
||||
assert any(c[0] == "model.base_url" for c in save_calls)
|
||||
|
||||
def test_provider_switch_fails_on_bad_credentials(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.runtime_provider.resolve_runtime_provider",
|
||||
side_effect=Exception("No API key found")):
|
||||
cli_obj.process_command("/model nous:hermes-3")
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Could not resolve credentials" in output
|
||||
assert cli_obj.model == "anthropic/claude-opus-4.6" # unchanged
|
||||
assert cli_obj.provider == "openrouter" # unchanged
|
||||
Reference in New Issue
Block a user