fix: /model command — bare provider names, custom endpoint display

Two issues with /model preventing proper provider switching:

1. Bare provider names not detected: typing '/model nous' treated 'nous'
   as a model name instead of triggering a provider switch. Fixed by adding
   step 0 in detect_provider_for_model() that checks if the input matches
   a known provider name/alias (excluding 'custom'/'openrouter' which need
   explicit model names) and returns that provider's default model.

2. Custom endpoint details hidden: /model (no args) showed '[custom]' with
   just a usage hint but no endpoint URL or model name. Now displays the
   configured base_url for custom providers in both CLI and gateway.

Note: config base_url and OPENAI_BASE_URL are intentionally NOT cleared on
provider switch — dedicated provider paths (nous, anthropic, codex) have
their own credential resolution that ignores these, and clearing them would
destroy the user's custom endpoint config, preventing switching back.

Co-authored-by: Test <test@test.com>
This commit is contained in:
Teknium 2026-03-19 12:06:48 -07:00 committed by GitHub
parent 04b6ecadc4
commit 4c0c7f4c6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 34 additions and 2 deletions

10
cli.py
View file

@ -973,6 +973,8 @@ def save_config_value(key_path: str, value: any) -> bool:
return False return False
# ============================================================================ # ============================================================================
# HermesCLI Class # HermesCLI Class
# ============================================================================ # ============================================================================
@ -2890,6 +2892,14 @@ class HermesCLI:
for mid, desc in curated: for mid, desc in curated:
current_marker = " ← current" if (is_active and mid == self.model) else "" current_marker = " ← current" if (is_active and mid == self.model) else ""
print(f" {mid}{current_marker}") print(f" {mid}{current_marker}")
elif p["id"] == "custom":
from hermes_cli.models import _get_custom_base_url
custom_url = _get_custom_base_url() or os.getenv("OPENAI_BASE_URL", "")
if custom_url:
print(f" endpoint: {custom_url}")
if is_active:
print(f" model: {self.model} ← current")
print(f" (use /model custom:<model-name>)")
else: else:
print(f" (use /model {p['id']}:<model-name>)") print(f" (use /model {p['id']}:<model-name>)")
print() print()

View file

@ -2380,8 +2380,14 @@ class GatewayRunner:
lines = [ lines = [
f"🤖 **Current model:** `{current}`", f"🤖 **Current model:** `{current}`",
f"**Provider:** {provider_label}", f"**Provider:** {provider_label}",
"",
] ]
# Show custom endpoint URL when using a custom provider
if current_provider == "custom":
from hermes_cli.models import _get_custom_base_url
custom_url = _get_custom_base_url() or os.getenv("OPENAI_BASE_URL", "")
if custom_url:
lines.append(f"**Endpoint:** `{custom_url}`")
lines.append("")
curated = curated_models_for_provider(current_provider) curated = curated_models_for_provider(current_provider)
if curated: if curated:
lines.append(f"**Available models ({provider_label}):**") lines.append(f"**Available models ({provider_label}):**")
@ -2391,7 +2397,7 @@ class GatewayRunner:
lines.append(f"• `{mid}`{label}{marker}") lines.append(f"• `{mid}`{label}{marker}")
lines.append("") lines.append("")
lines.append("To change: `/model model-name`") lines.append("To change: `/model model-name`")
lines.append("Switch provider: `/model provider:model-name`") lines.append("Switch provider: `/model provider-name` or `/model provider:model-name`")
return "\n".join(lines) return "\n".join(lines)
# Parse provider:model syntax # Parse provider:model syntax

View file

@ -389,6 +389,7 @@ def detect_provider_for_model(
Returns ``None`` when no confident match is found. Returns ``None`` when no confident match is found.
Priority: Priority:
0. Bare provider name switch to that provider's default model
1. Direct provider with credentials (highest) 1. Direct provider with credentials (highest)
2. Direct provider without credentials remap to OpenRouter slug 2. Direct provider without credentials remap to OpenRouter slug
3. OpenRouter catalog match 3. OpenRouter catalog match
@ -399,6 +400,21 @@ def detect_provider_for_model(
name_lower = name.lower() name_lower = name.lower()
# --- Step 0: bare provider name typed as model ---
# If someone types `/model nous` or `/model anthropic`, treat it as a
# provider switch and pick the first model from that provider's catalog.
# Skip "custom" and "openrouter" — custom has no model catalog, and
# openrouter requires an explicit model name to be useful.
resolved_provider = _PROVIDER_ALIASES.get(name_lower, name_lower)
if resolved_provider not in {"custom", "openrouter"}:
default_models = _PROVIDER_MODELS.get(resolved_provider, [])
if (
resolved_provider in _PROVIDER_LABELS
and default_models
and resolved_provider != normalize_provider(current_provider)
):
return (resolved_provider, default_models[0])
# Aggregators list other providers' models — never auto-switch TO them # Aggregators list other providers' models — never auto-switch TO them
_AGGREGATORS = {"nous", "openrouter"} _AGGREGATORS = {"nous", "openrouter"}