feat(model): /model command overhaul — Phases 2, 3, 5

* feat(model): persist base_url on /model switch, auto-detect for bare /model custom

Phase 2+3 of the /model command overhaul:

Phase 2 — Persist base_url on model switch:
- CLI: save model.base_url when switching to a non-OpenRouter endpoint;
  clear it when switching away from custom to prevent stale URLs
  leaking into the new provider's resolution
- Gateway: same logic using direct YAML write

Phase 3 — Better feedback and edge cases:
- Bare '/model custom' now auto-detects the model from the endpoint
  using _auto_detect_local_model() and saves all three config values
  (model, provider, base_url) atomically
- Shows endpoint URL in success messages when switching to/from
  custom providers (both CLI and gateway)
- Clear error messages when no custom endpoint is configured
- Updated test assertions for the additional save_config_value call

Fixes #2562 (Phase 2+3)

* feat(model): support custom:name:model triple syntax for named custom providers

Phase 5 of the /model command overhaul.

Extends parse_model_input() to handle the triple syntax:
  /model custom:local-server:qwen → provider='custom:local-server', model='qwen'
  /model custom:my-model          → provider='custom', model='my-model' (unchanged)

The 'custom:local-server' provider string is already supported by
_get_named_custom_provider() in runtime_provider.py, which matches
it against the custom_providers list in config.yaml. This just wires
the parsing so users can do it from the /model slash command.

Added 4 tests covering single, triple, whitespace, and empty model cases.
This commit is contained in:
Teknium
2026-03-24 06:58:04 -07:00
committed by GitHub
parent 2f1c4fb01f
commit b641ee88f4
5 changed files with 166 additions and 15 deletions

60
cli.py
View File

@@ -3571,6 +3571,43 @@ class HermesCLI:
raw_input = parts[1].strip()
# Handle bare "/model custom" — switch to custom provider
# and auto-detect the model from the endpoint.
if raw_input.strip().lower() == "custom":
from hermes_cli.runtime_provider import (
resolve_runtime_provider,
_auto_detect_local_model,
)
try:
runtime = resolve_runtime_provider(requested="custom")
cust_base = runtime.get("base_url", "")
cust_key = runtime.get("api_key", "")
if not cust_base or "openrouter.ai" in cust_base:
print("(>_<) No custom endpoint configured.")
print(" Set model.base_url in config.yaml, or set OPENAI_BASE_URL in .env,")
print(" or run: hermes setup → Custom OpenAI-compatible endpoint")
return True
detected_model = _auto_detect_local_model(cust_base)
if detected_model:
self.model = detected_model
self.requested_provider = "custom"
self.provider = "custom"
self.api_key = cust_key
self.base_url = cust_base
self.agent = None
save_config_value("model.default", detected_model)
save_config_value("model.provider", "custom")
save_config_value("model.base_url", cust_base)
print(f"(^_^)b Model changed to: {detected_model} [provider: Custom]")
print(f" Endpoint: {cust_base}")
print(f" Status: connected (model auto-detected)")
else:
print(f"(>_<) Custom endpoint at {cust_base} is reachable but no single model was auto-detected.")
print(f" Specify the model explicitly: /model custom:<model-name>")
except Exception as e:
print(f"(>_<) Could not resolve custom endpoint: {e}")
return True
# 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)
@@ -3642,6 +3679,14 @@ class HermesCLI:
saved_model = save_config_value("model.default", new_model)
if provider_changed:
save_config_value("model.provider", target_provider)
# Persist base_url for custom endpoints so it
# survives restart; clear it when switching away
# from custom to prevent stale URLs leaking into
# the new provider's resolution (#2562 Phase 2).
if base_url_for_probe and "openrouter.ai" not in (base_url_for_probe or ""):
save_config_value("model.base_url", base_url_for_probe)
else:
save_config_value("model.base_url", None)
if saved_model:
print(f"(^_^)b Model changed to: {new_model}{provider_note} (saved to config)")
else:
@@ -3653,12 +3698,17 @@ class HermesCLI:
print(f" Reason: {message}")
print(" Note: Model will revert on restart. Use a verified model to save to config.")
# Helpful hint when staying on a custom endpoint
if is_custom and not provider_changed:
endpoint = self.base_url or "custom endpoint"
# Show endpoint info for custom providers
_target_is_custom = target_provider == "custom" or (
base_url_for_probe and "openrouter.ai" not in (base_url_for_probe or "")
and ("localhost" in (base_url_for_probe or "") or "127.0.0.1" in (base_url_for_probe or ""))
)
if _target_is_custom or (is_custom and not provider_changed):
endpoint = base_url_for_probe or self.base_url or "custom endpoint"
print(f" Endpoint: {endpoint}")
print(f" Tip: To switch providers, use /model provider:model")
print(f" e.g. /model openai-codex:gpt-5.2-codex")
if not provider_changed:
print(f" Tip: To switch providers, use /model provider:model")
print(f" e.g. /model openai-codex:gpt-5.2-codex")
else:
self._show_model_and_providers()
elif canonical == "provider":