mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: provider switching via /model + enhanced model display
Add provider:model syntax to /model command for runtime provider switching: /model zai:glm-5 → switch to Z.AI provider with glm-5 /model nous:hermes-3 → switch to Nous Portal with hermes-3 /model openrouter:anthropic/claude-sonnet-4.5 → explicit OpenRouter When switching providers, credentials are resolved via resolve_runtime_provider and validated before committing. Both model and provider are saved to config. Provider aliases work (glm: → zai, kimi: → kimi-coding, etc.). Enhanced /model (no args) display now shows: - Current model and provider - Curated model list for the current provider with ← marker - Usage examples including provider:model syntax 39 tests covering parse_model_input, curated_models_for_provider, provider switching (success + credential failure), and display output.
This commit is contained in:
parent
4a09ae2985
commit
66d3e6a0c2
4 changed files with 213 additions and 92 deletions
|
|
@ -1,6 +1,6 @@
|
|||
"""Regression tests for the `/model` slash command in the interactive CLI."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from cli import HermesCLI
|
||||
|
||||
|
|
@ -21,8 +21,7 @@ class TestModelCommand:
|
|||
def test_valid_model_from_api_saved_to_config(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.auth.resolve_provider", return_value="openrouter"), \
|
||||
patch("hermes_cli.models.fetch_api_models",
|
||||
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")
|
||||
|
|
@ -30,60 +29,51 @@ class TestModelCommand:
|
|||
output = capsys.readouterr().out
|
||||
assert "saved to config" in output
|
||||
assert cli_obj.model == "anthropic/claude-sonnet-4.5"
|
||||
assert cli_obj.agent is None
|
||||
save_mock.assert_called_once_with("model.default", "anthropic/claude-sonnet-4.5")
|
||||
|
||||
def test_invalid_model_from_api_is_rejected(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.auth.resolve_provider", return_value="openrouter"), \
|
||||
patch("hermes_cli.models.fetch_api_models",
|
||||
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 a valid model" in output
|
||||
assert cli_obj.model == "anthropic/claude-opus-4.6" # unchanged
|
||||
assert cli_obj.agent is not None # not reset
|
||||
assert cli_obj.model == "anthropic/claude-opus-4.6"
|
||||
save_mock.assert_not_called()
|
||||
|
||||
def test_model_when_api_unreachable_falls_back_session_only(self, capsys):
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.auth.resolve_provider", return_value="openrouter"), \
|
||||
patch("hermes_cli.models.fetch_api_models", return_value=None), \
|
||||
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 "session only" in output
|
||||
assert cli_obj.model == "anthropic/claude-sonnet-next"
|
||||
assert cli_obj.agent is None
|
||||
save_mock.assert_not_called()
|
||||
|
||||
def test_no_slash_model_probes_api_and_rejects(self, capsys):
|
||||
"""Model without '/' is still probed via API — not rejected on format alone."""
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.auth.resolve_provider", return_value="openrouter"), \
|
||||
patch("hermes_cli.models.fetch_api_models",
|
||||
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
|
||||
assert "not a valid model" in output
|
||||
assert cli_obj.model == "anthropic/claude-opus-4.6" # unchanged
|
||||
fetch_mock.assert_called_once() # API was probed
|
||||
assert cli_obj.model == "anthropic/claude-opus-4.6"
|
||||
fetch_mock.assert_called_once()
|
||||
save_mock.assert_not_called()
|
||||
|
||||
def test_validation_crash_falls_back_to_save(self, capsys):
|
||||
"""If validate_requested_model throws, /model should still work (old behavior)."""
|
||||
cli_obj = self._make_cli()
|
||||
|
||||
with patch("hermes_cli.auth.resolve_provider", return_value="openrouter"), \
|
||||
patch("hermes_cli.models.validate_requested_model",
|
||||
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")
|
||||
|
|
@ -99,4 +89,42 @@ class TestModelCommand:
|
|||
|
||||
output = capsys.readouterr().out
|
||||
assert "anthropic/claude-opus-4.6" in output
|
||||
assert "Usage" in output
|
||||
assert "OpenRouter" in output
|
||||
assert "Available models" in output
|
||||
assert "provider:model-name" 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"
|
||||
# Both model and provider should be saved
|
||||
assert save_mock.call_count == 2
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue