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:
teknium1 2026-03-08 05:45:55 -07:00
parent 4a09ae2985
commit 66d3e6a0c2
4 changed files with 213 additions and 92 deletions

85
cli.py
View file

@ -2061,29 +2061,43 @@ class HermesCLI:
# Use original case so model names like "Anthropic/Claude-Opus-4" are preserved
parts = cmd_original.split(maxsplit=1)
if len(parts) > 1:
new_model = parts[1].strip()
from hermes_cli.auth import resolve_provider
from hermes_cli.models import validate_requested_model
from hermes_cli.models import (
parse_model_input,
validate_requested_model,
_PROVIDER_LABELS,
)
try:
provider_for_validation = resolve_provider(
self.requested_provider,
explicit_api_key=self._explicit_api_key,
explicit_base_url=self._explicit_base_url,
)
except Exception:
provider_for_validation = self.provider or self.requested_provider
raw_input = parts[1].strip()
# Parse provider:model syntax (e.g. "openrouter:anthropic/claude-sonnet-4.5")
current_provider = self.provider or self.requested_provider or "openrouter"
target_provider, new_model = parse_model_input(raw_input, current_provider)
provider_changed = target_provider != current_provider
# If provider is changing, re-resolve credentials for the new provider
api_key_for_probe = self.api_key
base_url_for_probe = self.base_url
if provider_changed:
try:
from hermes_cli.runtime_provider import resolve_runtime_provider
runtime = resolve_runtime_provider(requested=target_provider)
api_key_for_probe = runtime.get("api_key", "")
base_url_for_probe = runtime.get("base_url", "")
except Exception as e:
provider_label = _PROVIDER_LABELS.get(target_provider, target_provider)
print(f"(>_<) Could not resolve credentials for provider '{provider_label}': {e}")
print(f"(^_^) Current model unchanged: {self.model}")
return True
try:
validation = validate_requested_model(
new_model,
provider_for_validation,
api_key=self.api_key,
base_url=self.base_url,
target_provider,
api_key=api_key_for_probe,
base_url=base_url_for_probe,
)
except Exception:
# Validation itself failed — fall back to old behavior (accept + save)
validation = {"accepted": True, "persist": True, "recognized": False, "message": None}
if not validation.get("accepted"):
@ -2093,20 +2107,49 @@ class HermesCLI:
self.model = new_model
self.agent = None # Force re-init
if provider_changed:
self.requested_provider = target_provider
self.provider = target_provider
self.api_key = api_key_for_probe
self.base_url = base_url_for_probe
provider_label = _PROVIDER_LABELS.get(target_provider, target_provider)
provider_note = f" [provider: {provider_label}]" if provider_changed else ""
if validation.get("persist"):
if save_config_value("model.default", new_model):
print(f"(^_^)b Model changed to: {new_model} (saved to config)")
saved_model = save_config_value("model.default", new_model)
if provider_changed:
save_config_value("model.provider", target_provider)
if saved_model:
print(f"(^_^)b Model changed to: {new_model}{provider_note} (saved to config)")
else:
print(f"(^_^) Model changed to: {new_model} (session only)")
print(f"(^_^) Model changed to: {new_model}{provider_note} (session only)")
else:
print(f"(^_^) Model changed to: {new_model} (session only)")
print(f"(^_^) Model changed to: {new_model}{provider_note} (session only)")
message = validation.get("message")
if message:
print(f" Warning: {message}")
else:
print(f"Current model: {self.model}")
print(" Usage: /model <model-name> to change")
from hermes_cli.models import curated_models_for_provider, _PROVIDER_LABELS
provider_label = _PROVIDER_LABELS.get(
self.provider or "openrouter",
self.provider or "openrouter",
)
print(f"\n Current model: {self.model}")
print(f" Current provider: {provider_label}")
print()
curated = curated_models_for_provider(self.provider)
if curated:
print(f" Available models ({provider_label}):")
for mid, desc in curated:
marker = "" if mid == self.model else ""
label = f" {desc}" if desc else ""
print(f" {mid}{label}{marker}")
print()
print(" Usage: /model <model-name>")
print(" /model provider:model-name (to switch provider)")
print(" Example: /model openrouter:anthropic/claude-sonnet-4.5")
elif cmd_lower.startswith("/prompt"):
# Use original case so prompt text isn't lowercased
self._handle_prompt_command(cmd_original)