Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agent/model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@
# are preserved so the full model name reaches cache lookups and server queries.
_PROVIDER_PREFIXES: frozenset[str] = frozenset({
"openrouter", "nous", "openai-codex", "copilot", "copilot-acp",
"gemini", "zai", "kimi-coding", "minimax", "minimax-cn", "anthropic", "deepseek",
"gemini", "zai", "mimo", "kimi-coding", "minimax", "minimax-cn", "anthropic", "deepseek",
"opencode-zen", "opencode-go", "ai-gateway", "kilocode", "alibaba",
"custom", "local",
# Common aliases
"google", "google-gemini", "google-ai-studio",
"glm", "z-ai", "z.ai", "zhipu", "github", "github-copilot",
"github-models", "kimi", "moonshot", "claude", "deep-seek",
"github-models", "google", "google-gemini", "google-ai-studio", "kimi", "moonshot", "claude", "deep-seek",
"opencode", "zen", "go", "vercel", "kilo", "dashscope", "aliyun", "qwen",
})

Expand Down
82 changes: 58 additions & 24 deletions agent/models_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ def has_api_url(self) -> bool:
"openrouter": "openrouter",
"anthropic": "anthropic",
"zai": "zai",
"zai-coding": "zai",
"mimo": "xiaomi-token-plan-sgp",
"kimi-coding": "kimi-for-coding",
"minimax": "minimax",
"minimax-cn": "minimax-cn",
Expand All @@ -174,6 +176,14 @@ def has_api_url(self) -> bool:
# Reverse mapping: models.dev → Hermes (built lazily)
_MODELS_DEV_TO_PROVIDER: Optional[Dict[str, str]] = None

# Provider-level fallback catalogs when the primary mapping is more specific
# than the full family catalog (for example Mimo token-plan Singapore vs the
# broader Xiaomi catalog that still contains flash-model metadata).
PROVIDER_MODELS_DEV_FALLBACKS: Dict[str, Tuple[str, ...]] = {
"mimo": ("xiaomi", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn"),
"zai-coding": ("zai",),
}


def _get_reverse_mapping() -> Dict[str, str]:
"""Return models.dev ID → Hermes provider ID mapping."""
Expand Down Expand Up @@ -345,6 +355,30 @@ def _get_provider_models(provider: str) -> Optional[Dict[str, Any]]:
return models


def _iter_provider_model_sets(provider: str) -> List[Tuple[str, Dict[str, Any]]]:
"""Return primary + fallback model catalogs for a Hermes provider."""
data = fetch_models_dev()
provider_ids: list[str] = []

primary = PROVIDER_TO_MODELS_DEV.get(provider)
if primary:
provider_ids.append(primary)

for fallback in PROVIDER_MODELS_DEV_FALLBACKS.get(provider, ()):
if fallback not in provider_ids:
provider_ids.append(fallback)

results: list[Tuple[str, Dict[str, Any]]] = []
for provider_id in provider_ids:
provider_data = data.get(provider_id)
if not isinstance(provider_data, dict):
continue
models = provider_data.get("models", {})
if isinstance(models, dict):
results.append((provider_id, models))
return results


def _find_model_entry(models: Dict[str, Any], model: str) -> Optional[Dict[str, Any]]:
"""Find a model entry by exact match, then case-insensitive fallback."""
# Exact match
Expand Down Expand Up @@ -375,11 +409,11 @@ def get_model_capabilities(provider: str, model: str) -> Optional[ModelCapabilit
- limit.output (int) → max_output_tokens
- family (str) → model_family
"""
models = _get_provider_models(provider)
if models is None:
return None

entry = _find_model_entry(models, model)
entry = None
for _provider_id, models in _iter_provider_model_sets(provider):
entry = _find_model_entry(models, model)
if entry is not None:
break
if entry is None:
return None

Expand Down Expand Up @@ -682,27 +716,27 @@ def get_model_info(
Accepts Hermes or models.dev provider ID. Tries exact match then
case-insensitive fallback. Returns None if not found.
"""
mdev_id = PROVIDER_TO_MODELS_DEV.get(provider_id, provider_id)

data = fetch_models_dev()
pdata = data.get(mdev_id)
if not isinstance(pdata, dict):
return None

models = pdata.get("models", {})
if not isinstance(models, dict):
return None
provider_sets = _iter_provider_model_sets(provider_id)
if not provider_sets:
mdev_id = PROVIDER_TO_MODELS_DEV.get(provider_id, provider_id)
data = fetch_models_dev()
pdata = data.get(mdev_id)
if isinstance(pdata, dict):
models = pdata.get("models", {})
if isinstance(models, dict):
provider_sets = [(mdev_id, models)]

# Exact match
raw = models.get(model_id)
if isinstance(raw, dict):
return _parse_model_info(model_id, raw, mdev_id)
for mdev_id, models in provider_sets:
# Exact match
raw = models.get(model_id)
if isinstance(raw, dict):
return _parse_model_info(model_id, raw, mdev_id)

# Case-insensitive fallback
model_lower = model_id.lower()
for mid, mdata in models.items():
if mid.lower() == model_lower and isinstance(mdata, dict):
return _parse_model_info(mid, mdata, mdev_id)
# Case-insensitive fallback
model_lower = model_id.lower()
for mid, mdata in models.items():
if mid.lower() == model_lower and isinstance(mdata, dict):
return _parse_model_info(mid, mdata, mdev_id)

return None

Expand Down
17 changes: 17 additions & 0 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,22 @@ class ProviderConfig:
api_key_env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"),
base_url_env_var="GLM_BASE_URL",
),
"zai-coding": ProviderConfig(
id="zai-coding",
name="Z.AI Coding Plan",
auth_type="api_key",
inference_base_url="https://api.z.ai/api/coding/paas/v4",
api_key_env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"),
base_url_env_var="GLM_CODING_BASE_URL",
),
"mimo": ProviderConfig(
id="mimo",
name="Mimo",
auth_type="api_key",
inference_base_url="https://token-plan-sgp.xiaomimimo.com/v1",
api_key_env_vars=("MIMO_API_KEY", "XIAOMI_API_KEY"),
base_url_env_var="MIMO_BASE_URL",
),
"kimi-coding": ProviderConfig(
id="kimi-coding",
name="Kimi / Moonshot",
Expand Down Expand Up @@ -809,6 +825,7 @@ def resolve_provider(
_PROVIDER_ALIASES = {
"glm": "zai", "z-ai": "zai", "z.ai": "zai", "zhipu": "zai",
"google": "gemini", "google-gemini": "gemini", "google-ai-studio": "gemini",
"xiaomi": "mimo", "xiaomi-token-plan": "mimo", "mimo-token-plan": "mimo",
"kimi": "kimi-coding", "moonshot": "kimi-coding",
"minimax-china": "minimax-cn", "minimax_cn": "minimax-cn",
"claude": "anthropic", "claude-code": "anthropic",
Expand Down
44 changes: 33 additions & 11 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,30 @@ def ensure_hermes_home():
"category": "provider",
"advanced": True,
},
"GLM_CODING_BASE_URL": {
"description": "Z.AI Coding Plan base URL override",
"prompt": "Z.AI Coding Plan base URL (leave empty for default)",
"url": None,
"password": False,
"category": "provider",
"advanced": True,
},
"MIMO_API_KEY": {
"description": "Mimo API key",
"prompt": "Mimo API key",
"url": "https://token-plan-sgp.xiaomimimo.com/",
"password": True,
"category": "provider",
"advanced": True,
},
"MIMO_BASE_URL": {
"description": "Mimo base URL override",
"prompt": "Mimo base URL (leave empty for default)",
"url": None,
"password": False,
"category": "provider",
"advanced": True,
},
"KIMI_API_KEY": {
"description": "Kimi / Moonshot API key",
"prompt": "Kimi API key",
Expand Down Expand Up @@ -2082,18 +2106,16 @@ def _sanitize_env_lines(lines: list) -> list:
split_positions = []
for key_name in known_keys:
needle = key_name + "="
idx = stripped.find(needle)
while idx >= 0:
split_positions.append(idx)
idx = stripped.find(needle, idx + len(needle))

if len(split_positions) > 1:
split_positions.sort()
# Deduplicate (shouldn't happen, but be safe)
pos = raw.find(needle)
if pos > 0:
split_positions.append(pos)

if split_positions:
split_positions = sorted(set(split_positions))
for i, pos in enumerate(split_positions):
end = split_positions[i + 1] if i + 1 < len(split_positions) else len(stripped)
part = stripped[pos:end].strip()
start = 0
for pos in split_positions + [len(raw)]:
part = raw[start:pos].strip()
start = pos
if part:
sanitized.append(part + "\n")
else:
Expand Down
2 changes: 2 additions & 0 deletions hermes_cli/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,8 @@ def run_doctor(args):
# If supports_models_endpoint is False, we skip the health check and just show "configured"
_apikey_providers = [
("Z.AI / GLM", ("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"), "https://api.z.ai/api/paas/v4/models", "GLM_BASE_URL", True),
("Z.AI Coding Plan", ("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"), "https://api.z.ai/api/coding/paas/v4/models", "GLM_CODING_BASE_URL", True),
("Mimo", ("MIMO_API_KEY", "XIAOMI_API_KEY"), "https://token-plan-sgp.xiaomimimo.com/v1/models", "MIMO_BASE_URL", True),
("Kimi / Moonshot", ("KIMI_API_KEY",), "https://api.moonshot.ai/v1/models", "KIMI_BASE_URL", True),
("DeepSeek", ("DEEPSEEK_API_KEY",), "https://api.deepseek.com/v1/models", "DEEPSEEK_BASE_URL", True),
("Hugging Face", ("HF_TOKEN",), "https://router.huggingface.co/v1/models", "HF_BASE_URL", True),
Expand Down
2 changes: 2 additions & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,8 @@ def select_provider_and_model(args=None):
"anthropic": "Anthropic",
"gemini": "Google AI Studio",
"zai": "Z.AI / GLM",
"zai-coding": "Z.AI Coding Plan",
"mimo": "Mimo",
"kimi-coding": "Kimi / Moonshot",
"minimax": "MiniMax",
"minimax-cn": "MiniMax (China)",
Expand Down
2 changes: 2 additions & 0 deletions hermes_cli/model_normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
_PASSTHROUGH_PROVIDERS: frozenset[str] = frozenset({
"gemini",
"zai",
"zai-coding",
"mimo",
"kimi-coding",
"minimax",
"minimax-cn",
Expand Down
15 changes: 11 additions & 4 deletions hermes_cli/model_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,7 @@ def list_authenticated_providers(
get_provider_info as _mdev_pinfo,
)
from hermes_cli.models import OPENROUTER_MODELS, _PROVIDER_MODELS
from hermes_cli.providers import get_overlay

results: List[dict] = []
seen_slugs: set = set()
Expand All @@ -750,7 +751,12 @@ def list_authenticated_providers(
if not isinstance(pdata, dict):
continue

env_vars = pdata.get("env", [])
env_vars = list(pdata.get("env", []))
overlay = get_overlay(hermes_id)
if overlay and overlay.extra_env_vars:
for ev in overlay.extra_env_vars:
if ev not in env_vars:
env_vars.append(ev)
if not isinstance(env_vars, list):
continue

Expand Down Expand Up @@ -791,9 +797,10 @@ def list_authenticated_providers(
if overlay.auth_type in ("oauth_device_code", "oauth_external", "external_process"):
# These use auth stores, not env vars — check for auth.json entries
try:
from hermes_cli.auth import _read_auth_store
store = _read_auth_store()
if store and pid in store:
from hermes_cli.auth import _load_auth_store
store = _load_auth_store() or {}
providers_state = store.get("providers") if isinstance(store, dict) else {}
if isinstance(providers_state, dict) and pid in providers_state:
has_creds = True
except Exception:
pass
Expand Down
26 changes: 25 additions & 1 deletion hermes_cli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,27 @@
"gemma-4-26b-it",
],
"zai": [
"glm-5.1",
"glm-5",
"glm-5-turbo",
"glm-4.7",
"glm-4.5",
"glm-4.5-flash",
],
"zai-coding": [
"glm-5.1",
"mimo-v2-pro",
"glm-5",
"glm-5-turbo",
"glm-4.7",
"glm-4.5",
"glm-4.5-flash",
],
"mimo": [
"mimo-v2-pro",
"mimo-v2-omni",
"mimo-v2-flash",
],
"kimi-coding": [
"kimi-for-coding",
"kimi-k2.5",
Expand Down Expand Up @@ -469,6 +484,8 @@ def check_nous_free_tier() -> bool:
"copilot": "GitHub Copilot",
"gemini": "Google AI Studio",
"zai": "Z.AI / GLM",
"zai-coding": "Z.AI Coding Plan",
"mimo": "Mimo",
"kimi-coding": "Kimi / Moonshot",
"minimax": "MiniMax",
"minimax-cn": "MiniMax (China)",
Expand Down Expand Up @@ -497,6 +514,13 @@ def check_nous_free_tier() -> bool:
"google": "gemini",
"google-gemini": "gemini",
"google-ai-studio": "gemini",
"xiaomi": "mimo",
"xiaomi-token-plan": "mimo",
"mimo-token-plan": "mimo",
"glm-coding": "zai-coding",
"zai-coding": "zai-coding",
"z-ai-coding": "zai-coding",
"z.ai-coding": "zai-coding",
"kimi": "kimi-coding",
"moonshot": "kimi-coding",
"minimax-china": "minimax-cn",
Expand Down Expand Up @@ -762,7 +786,7 @@ def list_available_providers() -> list[dict[str, str]]:
_PROVIDER_ORDER = [
"openrouter", "nous", "openai-codex", "copilot", "copilot-acp",
"gemini", "huggingface",
"zai", "kimi-coding", "minimax", "minimax-cn", "kilocode", "anthropic", "alibaba",
"zai", "zai-coding", "mimo", "kimi-coding", "minimax", "minimax-cn", "kilocode", "anthropic", "alibaba",
"opencode-zen", "opencode-go",
"ai-gateway", "deepseek", "custom",
]
Expand Down
Loading
Loading