223 lines
6.6 KiB
Python
223 lines
6.6 KiB
Python
"""
|
|
Canonical model catalogs and lightweight validation helpers.
|
|
|
|
Add, remove, or reorder entries here — both `hermes setup` and
|
|
`hermes` provider-selection will pick up the change automatically.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from difflib import get_close_matches
|
|
from typing import Any, Optional
|
|
|
|
# (model_id, display description shown in menus)
|
|
OPENROUTER_MODELS: list[tuple[str, str]] = [
|
|
("anthropic/claude-opus-4.6", "recommended"),
|
|
("anthropic/claude-sonnet-4.5", ""),
|
|
("openai/gpt-5.4-pro", ""),
|
|
("openai/gpt-5.4", ""),
|
|
("openai/gpt-5.3-codex", ""),
|
|
("google/gemini-3-pro-preview", ""),
|
|
("google/gemini-3-flash-preview", ""),
|
|
("qwen/qwen3.5-plus-02-15", ""),
|
|
("qwen/qwen3.5-35b-a3b", ""),
|
|
("stepfun/step-3.5-flash", ""),
|
|
("z-ai/glm-5", ""),
|
|
("moonshotai/kimi-k2.5", ""),
|
|
("minimax/minimax-m2.5", ""),
|
|
]
|
|
|
|
_PROVIDER_MODELS: dict[str, list[str]] = {
|
|
"zai": [
|
|
"glm-5",
|
|
"glm-4.7",
|
|
"glm-4.5",
|
|
"glm-4.5-flash",
|
|
],
|
|
"kimi-coding": [
|
|
"kimi-k2.5",
|
|
"kimi-k2-thinking",
|
|
"kimi-k2-turbo-preview",
|
|
"kimi-k2-0905-preview",
|
|
],
|
|
"minimax": [
|
|
"MiniMax-M2.5",
|
|
"MiniMax-M2.5-highspeed",
|
|
"MiniMax-M2.1",
|
|
],
|
|
"minimax-cn": [
|
|
"MiniMax-M2.5",
|
|
"MiniMax-M2.5-highspeed",
|
|
"MiniMax-M2.1",
|
|
],
|
|
}
|
|
|
|
_PROVIDER_LABELS = {
|
|
"openrouter": "OpenRouter",
|
|
"openai-codex": "OpenAI Codex",
|
|
"nous": "Nous Portal",
|
|
"zai": "Z.AI / GLM",
|
|
"kimi-coding": "Kimi / Moonshot",
|
|
"minimax": "MiniMax",
|
|
"minimax-cn": "MiniMax (China)",
|
|
"custom": "custom endpoint",
|
|
}
|
|
|
|
_PROVIDER_ALIASES = {
|
|
"glm": "zai",
|
|
"z-ai": "zai",
|
|
"z.ai": "zai",
|
|
"zhipu": "zai",
|
|
"kimi": "kimi-coding",
|
|
"moonshot": "kimi-coding",
|
|
"minimax-china": "minimax-cn",
|
|
"minimax_cn": "minimax-cn",
|
|
}
|
|
|
|
|
|
def model_ids() -> list[str]:
|
|
"""Return just the OpenRouter model-id strings."""
|
|
return [mid for mid, _ in OPENROUTER_MODELS]
|
|
|
|
|
|
def menu_labels() -> list[str]:
|
|
"""Return display labels like 'anthropic/claude-opus-4.6 (recommended)'."""
|
|
labels = []
|
|
for mid, desc in OPENROUTER_MODELS:
|
|
labels.append(f"{mid} ({desc})" if desc else mid)
|
|
return labels
|
|
|
|
|
|
def normalize_provider(provider: Optional[str]) -> str:
|
|
"""Normalize provider aliases to Hermes' canonical provider ids."""
|
|
normalized = (provider or "openrouter").strip().lower()
|
|
return _PROVIDER_ALIASES.get(normalized, normalized)
|
|
|
|
|
|
def provider_model_ids(provider: Optional[str]) -> list[str]:
|
|
"""Return the best known model catalog for a provider."""
|
|
normalized = normalize_provider(provider)
|
|
if normalized == "openrouter":
|
|
return model_ids()
|
|
if normalized == "openai-codex":
|
|
from hermes_cli.codex_models import get_codex_model_ids
|
|
|
|
return get_codex_model_ids()
|
|
return list(_PROVIDER_MODELS.get(normalized, []))
|
|
|
|
|
|
def validate_requested_model(
|
|
model_name: str,
|
|
provider: Optional[str],
|
|
*,
|
|
base_url: Optional[str] = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Validate a `/model` value for the active provider.
|
|
|
|
Returns a dict with:
|
|
- accepted: whether the CLI should switch to the requested model now
|
|
- persist: whether it is safe to save to config
|
|
- recognized: whether it matched a known provider catalog
|
|
- message: optional warning / guidance for the user
|
|
"""
|
|
requested = (model_name or "").strip()
|
|
normalized = normalize_provider(provider)
|
|
if normalized == "openrouter" and base_url and "openrouter.ai" not in base_url:
|
|
normalized = "custom"
|
|
|
|
if not requested:
|
|
return {
|
|
"accepted": False,
|
|
"persist": False,
|
|
"recognized": False,
|
|
"message": "Model name cannot be empty.",
|
|
}
|
|
|
|
if any(ch.isspace() for ch in requested):
|
|
return {
|
|
"accepted": False,
|
|
"persist": False,
|
|
"recognized": False,
|
|
"message": "Model names cannot contain spaces.",
|
|
}
|
|
|
|
known_models = provider_model_ids(normalized)
|
|
if requested in known_models:
|
|
return {
|
|
"accepted": True,
|
|
"persist": True,
|
|
"recognized": True,
|
|
"message": None,
|
|
}
|
|
|
|
suggestion = get_close_matches(requested, known_models, n=1, cutoff=0.6)
|
|
suggestion_text = f" Did you mean `{suggestion[0]}`?" if suggestion else ""
|
|
provider_label = _PROVIDER_LABELS.get(normalized, normalized)
|
|
|
|
if normalized == "custom":
|
|
return {
|
|
"accepted": True,
|
|
"persist": True,
|
|
"recognized": False,
|
|
"message": None,
|
|
}
|
|
|
|
if normalized == "openrouter":
|
|
if "/" not in requested or requested.startswith("/") or requested.endswith("/"):
|
|
return {
|
|
"accepted": False,
|
|
"persist": False,
|
|
"recognized": False,
|
|
"message": (
|
|
"OpenRouter model IDs should use the `provider/model` format "
|
|
"(for example `anthropic/claude-opus-4.6`)."
|
|
f"{suggestion_text}"
|
|
),
|
|
}
|
|
return {
|
|
"accepted": True,
|
|
"persist": False,
|
|
"recognized": False,
|
|
"message": (
|
|
f"`{requested}` is not in Hermes' curated {provider_label} model list. "
|
|
"Using it for this session only; config unchanged."
|
|
f"{suggestion_text}"
|
|
),
|
|
}
|
|
|
|
if normalized == "nous":
|
|
return {
|
|
"accepted": True,
|
|
"persist": False,
|
|
"recognized": False,
|
|
"message": (
|
|
f"Could not validate `{requested}` against the live {provider_label} catalog here. "
|
|
"Using it for this session only; config unchanged."
|
|
f"{suggestion_text}"
|
|
),
|
|
}
|
|
|
|
if known_models:
|
|
return {
|
|
"accepted": True,
|
|
"persist": False,
|
|
"recognized": False,
|
|
"message": (
|
|
f"`{requested}` is not in the known {provider_label} model list. "
|
|
"Using it for this session only; config unchanged."
|
|
f"{suggestion_text}"
|
|
),
|
|
}
|
|
|
|
return {
|
|
"accepted": True,
|
|
"persist": False,
|
|
"recognized": False,
|
|
"message": (
|
|
f"Could not validate `{requested}` for provider {provider_label}. "
|
|
"Using it for this session only; config unchanged."
|
|
f"{suggestion_text}"
|
|
),
|
|
}
|