diff --git a/.env.example b/.env.example
index 2693931e0..e43f5a9b6 100644
--- a/.env.example
+++ b/.env.example
@@ -13,6 +13,34 @@ OPENROUTER_API_KEY=
# Examples: anthropic/claude-opus-4.6, openai/gpt-4o, google/gemini-3-flash-preview, zhipuai/glm-4-plus
LLM_MODEL=anthropic/claude-opus-4.6
+# =============================================================================
+# LLM PROVIDER (z.ai / GLM)
+# =============================================================================
+# z.ai provides access to ZhipuAI GLM models (GLM-4-Plus, etc.)
+# Get your key at: https://z.ai or https://open.bigmodel.cn
+GLM_API_KEY=
+# GLM_BASE_URL=https://api.z.ai/api/paas/v4 # Override default base URL
+
+# =============================================================================
+# LLM PROVIDER (Kimi / Moonshot)
+# =============================================================================
+# Kimi/Moonshot provides access to Moonshot AI coding models
+# Get your key at: https://platform.moonshot.ai
+KIMI_API_KEY=
+# KIMI_BASE_URL=https://api.moonshot.ai/v1 # Override default base URL
+
+# =============================================================================
+# LLM PROVIDER (MiniMax)
+# =============================================================================
+# MiniMax provides access to MiniMax models (global endpoint)
+# Get your key at: https://www.minimax.io
+MINIMAX_API_KEY=
+# MINIMAX_BASE_URL=https://api.minimax.io/v1 # Override default base URL
+
+# MiniMax China endpoint (for users in mainland China)
+MINIMAX_CN_API_KEY=
+# MINIMAX_CN_BASE_URL=https://api.minimaxi.com/v1 # Override default base URL
+
# =============================================================================
# TOOL API KEYS
# =============================================================================
diff --git a/README.md b/README.md
index fa9a2bd04..b172d13f2 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
**The self-improving AI agent built by [Nous Research](https://nousresearch.com).** It's the only agent with a built-in learning loop — it creates skills from experience, improves them during use, nudges itself to persist knowledge, searches its own past conversations, and builds a deepening model of who you are across sessions. Run it on a $5 VPS, a GPU cluster, or serverless infrastructure that costs nearly nothing when idle. It's not tied to your laptop — talk to it from Telegram while it works on a cloud VM.
-Use any model you want — [Nous Portal](https://portal.nousresearch.com), [OpenRouter](https://openrouter.ai) (200+ models), OpenAI, or your own endpoint. Switch with `hermes model` — no code changes, no lock-in.
+Use any model you want — [Nous Portal](https://portal.nousresearch.com), [OpenRouter](https://openrouter.ai) (200+ models), [z.ai/GLM](https://z.ai), [Kimi/Moonshot](https://platform.moonshot.ai), [MiniMax](https://www.minimax.io), OpenAI, or your own endpoint. Switch with `hermes model` — no code changes, no lock-in.
| A real terminal interface | Full TUI with multiline editing, slash-command autocomplete, conversation history, interrupt-and-redirect, and streaming tool output. |
diff --git a/cli-config.yaml.example b/cli-config.yaml.example
index c9d2c2e50..d8489d95b 100644
--- a/cli-config.yaml.example
+++ b/cli-config.yaml.example
@@ -13,6 +13,10 @@ model:
# "auto" - Use Nous Portal if logged in, otherwise OpenRouter/env vars (default)
# "openrouter" - Always use OpenRouter API key from OPENROUTER_API_KEY
# "nous" - Always use Nous Portal (requires: hermes login)
+ # "zai" - Use z.ai / ZhipuAI GLM models (requires: GLM_API_KEY)
+ # "kimi-coding"- Use Kimi / Moonshot AI models (requires: KIMI_API_KEY)
+ # "minimax" - Use MiniMax global endpoint (requires: MINIMAX_API_KEY)
+ # "minimax-cn" - Use MiniMax China endpoint (requires: MINIMAX_CN_API_KEY)
# Can also be overridden with --provider flag or HERMES_INFERENCE_PROVIDER env var.
provider: "auto"
diff --git a/cli.py b/cli.py
index 6433d4ce8..8898eef20 100755
--- a/cli.py
+++ b/cli.py
@@ -833,7 +833,7 @@ class HermesCLI:
Args:
model: Model to use (default: from env or claude-sonnet)
toolsets: List of toolsets to enable (default: all)
- provider: Inference provider ("auto", "openrouter", "nous", "openai-codex")
+ provider: Inference provider ("auto", "openrouter", "nous", "openai-codex", "zai", "kimi-coding", "minimax", "minimax-cn")
api_key: API key (default: from environment)
base_url: API base URL (default: OpenRouter)
max_turns: Maximum tool-calling iterations (default: 60)
@@ -3229,7 +3229,7 @@ def main(
q: Shorthand for --query
toolsets: Comma-separated list of toolsets to enable (e.g., "web,terminal")
model: Model to use (default: anthropic/claude-opus-4-20250514)
- provider: Inference provider ("auto", "openrouter", "nous")
+ provider: Inference provider ("auto", "openrouter", "nous", "openai-codex", "zai", "kimi-coding", "minimax", "minimax-cn")
api_key: API key for authentication
base_url: Base URL for the API
max_turns: Maximum tool-calling iterations (default: 60)
diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py
index 7a2fba0a9..440fc2b6f 100644
--- a/hermes_cli/auth.py
+++ b/hermes_cli/auth.py
@@ -72,15 +72,19 @@ CODEX_ACCESS_TOKEN_REFRESH_SKEW_SECONDS = 120
@dataclass
class ProviderConfig:
- """Describes a known OAuth provider."""
+ """Describes a known inference provider."""
id: str
name: str
- auth_type: str # "oauth_device_code" or "api_key"
+ auth_type: str # "oauth_device_code", "oauth_external", or "api_key"
portal_base_url: str = ""
inference_base_url: str = ""
client_id: str = ""
scope: str = ""
extra: Dict[str, Any] = field(default_factory=dict)
+ # For API-key providers: env vars to check (in priority order)
+ api_key_env_vars: tuple = ()
+ # Optional env var for base URL override
+ base_url_env_var: str = ""
PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
@@ -99,6 +103,38 @@ PROVIDER_REGISTRY: Dict[str, ProviderConfig] = {
auth_type="oauth_external",
inference_base_url=DEFAULT_CODEX_BASE_URL,
),
+ "zai": ProviderConfig(
+ id="zai",
+ name="Z.AI / GLM",
+ auth_type="api_key",
+ inference_base_url="https://api.z.ai/api/paas/v4",
+ api_key_env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"),
+ base_url_env_var="GLM_BASE_URL",
+ ),
+ "kimi-coding": ProviderConfig(
+ id="kimi-coding",
+ name="Kimi / Moonshot",
+ auth_type="api_key",
+ inference_base_url="https://api.moonshot.ai/v1",
+ api_key_env_vars=("KIMI_API_KEY",),
+ base_url_env_var="KIMI_BASE_URL",
+ ),
+ "minimax": ProviderConfig(
+ id="minimax",
+ name="MiniMax",
+ auth_type="api_key",
+ inference_base_url="https://api.minimax.io/v1",
+ api_key_env_vars=("MINIMAX_API_KEY",),
+ base_url_env_var="MINIMAX_BASE_URL",
+ ),
+ "minimax-cn": ProviderConfig(
+ id="minimax-cn",
+ name="MiniMax (China)",
+ auth_type="api_key",
+ inference_base_url="https://api.minimaxi.com/v1",
+ api_key_env_vars=("MINIMAX_CN_API_KEY",),
+ base_url_env_var="MINIMAX_CN_BASE_URL",
+ ),
}
@@ -355,10 +391,19 @@ def resolve_provider(
1. active_provider in auth.json with valid credentials
2. Explicit CLI api_key/base_url -> "openrouter"
3. OPENAI_API_KEY or OPENROUTER_API_KEY env vars -> "openrouter"
- 4. Fallback: "openrouter"
+ 4. Provider-specific API keys (GLM, Kimi, MiniMax) -> that provider
+ 5. Fallback: "openrouter"
"""
normalized = (requested or "auto").strip().lower()
+ # Normalize provider aliases
+ _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",
+ }
+ normalized = _PROVIDER_ALIASES.get(normalized, normalized)
+
if normalized in {"openrouter", "custom"}:
return "openrouter"
if normalized in PROVIDER_REGISTRY:
@@ -387,6 +432,14 @@ def resolve_provider(
if os.getenv("OPENAI_API_KEY") or os.getenv("OPENROUTER_API_KEY"):
return "openrouter"
+ # Auto-detect API-key providers by checking their env vars
+ for pid, pconfig in PROVIDER_REGISTRY.items():
+ if pconfig.auth_type != "api_key":
+ continue
+ for env_var in pconfig.api_key_env_vars:
+ if os.getenv(env_var, "").strip():
+ return pid
+
return "openrouter"
@@ -1230,6 +1283,37 @@ def get_codex_auth_status() -> Dict[str, Any]:
}
+def get_api_key_provider_status(provider_id: str) -> Dict[str, Any]:
+ """Status snapshot for API-key providers (z.ai, Kimi, MiniMax)."""
+ pconfig = PROVIDER_REGISTRY.get(provider_id)
+ if not pconfig or pconfig.auth_type != "api_key":
+ return {"configured": False}
+
+ api_key = ""
+ key_source = ""
+ for env_var in pconfig.api_key_env_vars:
+ val = os.getenv(env_var, "").strip()
+ if val:
+ api_key = val
+ key_source = env_var
+ break
+
+ base_url = pconfig.inference_base_url
+ if pconfig.base_url_env_var:
+ env_url = os.getenv(pconfig.base_url_env_var, "").strip()
+ if env_url:
+ base_url = env_url
+
+ return {
+ "configured": bool(api_key),
+ "provider": provider_id,
+ "name": pconfig.name,
+ "key_source": key_source,
+ "base_url": base_url,
+ "logged_in": bool(api_key), # compat with OAuth status shape
+ }
+
+
def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:
"""Generic auth status dispatcher."""
target = provider_id or get_active_provider()
@@ -1237,9 +1321,49 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:
return get_nous_auth_status()
if target == "openai-codex":
return get_codex_auth_status()
+ # API-key providers
+ pconfig = PROVIDER_REGISTRY.get(target)
+ if pconfig and pconfig.auth_type == "api_key":
+ return get_api_key_provider_status(target)
return {"logged_in": False}
+def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]:
+ """Resolve API key and base URL for an API-key provider.
+
+ Returns dict with: provider, api_key, base_url, source.
+ """
+ pconfig = PROVIDER_REGISTRY.get(provider_id)
+ if not pconfig or pconfig.auth_type != "api_key":
+ raise AuthError(
+ f"Provider '{provider_id}' is not an API-key provider.",
+ provider=provider_id,
+ code="invalid_provider",
+ )
+
+ api_key = ""
+ key_source = ""
+ for env_var in pconfig.api_key_env_vars:
+ val = os.getenv(env_var, "").strip()
+ if val:
+ api_key = val
+ key_source = env_var
+ break
+
+ base_url = pconfig.inference_base_url
+ if pconfig.base_url_env_var:
+ env_url = os.getenv(pconfig.base_url_env_var, "").strip()
+ if env_url:
+ base_url = env_url.rstrip("/")
+
+ return {
+ "provider": provider_id,
+ "api_key": api_key,
+ "base_url": base_url.rstrip("/"),
+ "source": key_source or "default",
+ }
+
+
# =============================================================================
# External credential detection
# =============================================================================
diff --git a/hermes_cli/config.py b/hermes_cli/config.py
index 12c8e78fb..2fa9d19cf 100644
--- a/hermes_cli/config.py
+++ b/hermes_cli/config.py
@@ -170,6 +170,86 @@ OPTIONAL_ENV_VARS = {
"category": "provider",
"advanced": True,
},
+ "GLM_API_KEY": {
+ "description": "Z.AI / GLM API key (also recognized as ZAI_API_KEY / Z_AI_API_KEY)",
+ "prompt": "Z.AI / GLM API key",
+ "url": "https://z.ai/",
+ "password": True,
+ "category": "provider",
+ "advanced": True,
+ },
+ "ZAI_API_KEY": {
+ "description": "Z.AI API key (alias for GLM_API_KEY)",
+ "prompt": "Z.AI API key",
+ "url": "https://z.ai/",
+ "password": True,
+ "category": "provider",
+ "advanced": True,
+ },
+ "Z_AI_API_KEY": {
+ "description": "Z.AI API key (alias for GLM_API_KEY)",
+ "prompt": "Z.AI API key",
+ "url": "https://z.ai/",
+ "password": True,
+ "category": "provider",
+ "advanced": True,
+ },
+ "GLM_BASE_URL": {
+ "description": "Z.AI / GLM base URL override",
+ "prompt": "Z.AI / GLM 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",
+ "url": "https://platform.moonshot.cn/",
+ "password": True,
+ "category": "provider",
+ "advanced": True,
+ },
+ "KIMI_BASE_URL": {
+ "description": "Kimi / Moonshot base URL override",
+ "prompt": "Kimi base URL (leave empty for default)",
+ "url": None,
+ "password": False,
+ "category": "provider",
+ "advanced": True,
+ },
+ "MINIMAX_API_KEY": {
+ "description": "MiniMax API key (international)",
+ "prompt": "MiniMax API key",
+ "url": "https://www.minimax.io/",
+ "password": True,
+ "category": "provider",
+ "advanced": True,
+ },
+ "MINIMAX_BASE_URL": {
+ "description": "MiniMax base URL override",
+ "prompt": "MiniMax base URL (leave empty for default)",
+ "url": None,
+ "password": False,
+ "category": "provider",
+ "advanced": True,
+ },
+ "MINIMAX_CN_API_KEY": {
+ "description": "MiniMax API key (China endpoint)",
+ "prompt": "MiniMax (China) API key",
+ "url": "https://www.minimaxi.com/",
+ "password": True,
+ "category": "provider",
+ "advanced": True,
+ },
+ "MINIMAX_CN_BASE_URL": {
+ "description": "MiniMax (China) base URL override",
+ "prompt": "MiniMax (China) base URL (leave empty for default)",
+ "url": None,
+ "password": False,
+ "category": "provider",
+ "advanced": True,
+ },
# ── Tool API keys ──
"FIRECRAWL_API_KEY": {
diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py
index 36795c016..a76a6b390 100644
--- a/hermes_cli/doctor.py
+++ b/hermes_cli/doctor.py
@@ -132,7 +132,11 @@ def run_doctor(args):
# Check for common issues
content = env_path.read_text()
- if "OPENROUTER_API_KEY" in content or "ANTHROPIC_API_KEY" in content:
+ if any(k in content for k in (
+ "OPENROUTER_API_KEY", "ANTHROPIC_API_KEY",
+ "GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY",
+ "KIMI_API_KEY", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
+ )):
check_ok("API key configured")
else:
check_warn("No API key found in ~/.hermes/.env")
@@ -468,7 +472,42 @@ def run_doctor(args):
print(f"\r {color('⚠', Colors.YELLOW)} Anthropic API {color(msg, Colors.DIM)} ")
except Exception as e:
print(f"\r {color('⚠', Colors.YELLOW)} Anthropic API {color(f'({e})', Colors.DIM)} ")
-
+
+ # -- API-key providers (Z.AI/GLM, Kimi, MiniMax, MiniMax-CN) --
+ _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"),
+ ("Kimi / Moonshot", ("KIMI_API_KEY",), "https://api.moonshot.ai/v1/models", "KIMI_BASE_URL"),
+ ("MiniMax", ("MINIMAX_API_KEY",), "https://api.minimax.io/v1/models", "MINIMAX_BASE_URL"),
+ ("MiniMax (China)", ("MINIMAX_CN_API_KEY",), "https://api.minimaxi.com/v1/models", "MINIMAX_CN_BASE_URL"),
+ ]
+ for _pname, _env_vars, _default_url, _base_env in _apikey_providers:
+ _key = ""
+ for _ev in _env_vars:
+ _key = os.getenv(_ev, "")
+ if _key:
+ break
+ if _key:
+ _label = _pname.ljust(20)
+ print(f" Checking {_pname} API...", end="", flush=True)
+ try:
+ import httpx
+ _base = os.getenv(_base_env, "")
+ _url = (_base.rstrip("/") + "/models") if _base else _default_url
+ _resp = httpx.get(
+ _url,
+ headers={"Authorization": f"Bearer {_key}"},
+ timeout=10,
+ )
+ if _resp.status_code == 200:
+ print(f"\r {color('✓', Colors.GREEN)} {_label} ")
+ elif _resp.status_code == 401:
+ print(f"\r {color('✗', Colors.RED)} {_label} {color('(invalid API key)', Colors.DIM)} ")
+ issues.append(f"Check {_env_vars[0]} in .env")
+ else:
+ print(f"\r {color('⚠', Colors.YELLOW)} {_label} {color(f'(HTTP {_resp.status_code})', Colors.DIM)} ")
+ except Exception as _e:
+ print(f"\r {color('⚠', Colors.YELLOW)} {_label} {color(f'({_e})', Colors.DIM)} ")
+
# =========================================================================
# Check: Submodules
# =========================================================================
diff --git a/hermes_cli/main.py b/hermes_cli/main.py
index f709db1d7..0332170ee 100644
--- a/hermes_cli/main.py
+++ b/hermes_cli/main.py
@@ -64,7 +64,13 @@ def _has_any_provider_configured() -> bool:
# Check env vars (may be set by .env or shell).
# OPENAI_BASE_URL alone counts — local models (vLLM, llama.cpp, etc.)
# often don't require an API key.
- provider_env_vars = ("OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_BASE_URL")
+ from hermes_cli.auth import PROVIDER_REGISTRY
+
+ # Collect all provider env vars
+ provider_env_vars = {"OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_BASE_URL"}
+ for pconfig in PROVIDER_REGISTRY.values():
+ if pconfig.auth_type == "api_key":
+ provider_env_vars.update(pconfig.api_key_env_vars)
if any(os.getenv(v) for v in provider_env_vars):
return True
@@ -411,6 +417,10 @@ def cmd_model(args):
"openrouter": "OpenRouter",
"nous": "Nous Portal",
"openai-codex": "OpenAI Codex",
+ "zai": "Z.AI / GLM",
+ "kimi-coding": "Kimi / Moonshot",
+ "minimax": "MiniMax",
+ "minimax-cn": "MiniMax (China)",
"custom": "Custom endpoint",
}
active_label = provider_labels.get(active, active)
@@ -425,11 +435,16 @@ def cmd_model(args):
("openrouter", "OpenRouter (100+ models, pay-per-use)"),
("nous", "Nous Portal (Nous Research subscription)"),
("openai-codex", "OpenAI Codex"),
+ ("zai", "Z.AI / GLM (Zhipu AI direct API)"),
+ ("kimi-coding", "Kimi / Moonshot (Moonshot AI direct API)"),
+ ("minimax", "MiniMax (global direct API)"),
+ ("minimax-cn", "MiniMax China (domestic direct API)"),
("custom", "Custom endpoint (self-hosted / VLLM / etc.)"),
]
# Reorder so the active provider is at the top
- active_key = active if active in ("openrouter", "nous", "openai-codex") else "custom"
+ known_keys = {k for k, _ in providers}
+ active_key = active if active in known_keys else "custom"
ordered = []
for key, label in providers:
if key == active_key:
@@ -454,6 +469,8 @@ def cmd_model(args):
_model_flow_openai_codex(config, current_model)
elif selected_provider == "custom":
_model_flow_custom(config)
+ elif selected_provider in ("zai", "kimi-coding", "minimax", "minimax-cn"):
+ _model_flow_api_key_provider(config, selected_provider, current_model)
def _prompt_provider_choice(choices):
@@ -723,6 +740,117 @@ def _model_flow_custom(config):
print("Endpoint saved. Use `/model` in chat or `hermes model` to set a model.")
+# Curated model lists for direct API-key providers
+_PROVIDER_MODELS = {
+ "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",
+ ],
+}
+
+
+def _model_flow_api_key_provider(config, provider_id, current_model=""):
+ """Generic flow for API-key providers (z.ai, Kimi, MiniMax)."""
+ from hermes_cli.auth import (
+ PROVIDER_REGISTRY, _prompt_model_selection, _save_model_choice,
+ _update_config_for_provider, deactivate_provider,
+ )
+ from hermes_cli.config import get_env_value, save_env_value, load_config, save_config
+
+ pconfig = PROVIDER_REGISTRY[provider_id]
+ key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
+ base_url_env = pconfig.base_url_env_var or ""
+
+ # Check / prompt for API key
+ existing_key = ""
+ for ev in pconfig.api_key_env_vars:
+ existing_key = get_env_value(ev) or os.getenv(ev, "")
+ if existing_key:
+ break
+
+ if not existing_key:
+ print(f"No {pconfig.name} API key configured.")
+ if key_env:
+ try:
+ new_key = input(f"{key_env} (or Enter to cancel): ").strip()
+ except (KeyboardInterrupt, EOFError):
+ print()
+ return
+ if not new_key:
+ print("Cancelled.")
+ return
+ save_env_value(key_env, new_key)
+ print("API key saved.")
+ print()
+ else:
+ print(f" {pconfig.name} API key: {existing_key[:8]}... ✓")
+ print()
+
+ # Optional base URL override
+ current_base = ""
+ if base_url_env:
+ current_base = get_env_value(base_url_env) or os.getenv(base_url_env, "")
+ effective_base = current_base or pconfig.inference_base_url
+
+ try:
+ override = input(f"Base URL [{effective_base}]: ").strip()
+ except (KeyboardInterrupt, EOFError):
+ print()
+ override = ""
+ if override and base_url_env:
+ save_env_value(base_url_env, override)
+ effective_base = override
+
+ # Model selection
+ model_list = _PROVIDER_MODELS.get(provider_id, [])
+ if model_list:
+ selected = _prompt_model_selection(model_list, current_model=current_model)
+ else:
+ try:
+ selected = input("Model name: ").strip()
+ except (KeyboardInterrupt, EOFError):
+ selected = None
+
+ if selected:
+ # Clear custom endpoint if set (avoid confusion)
+ if get_env_value("OPENAI_BASE_URL"):
+ save_env_value("OPENAI_BASE_URL", "")
+ save_env_value("OPENAI_API_KEY", "")
+
+ _save_model_choice(selected)
+
+ # Update config with provider and base URL
+ cfg = load_config()
+ model = cfg.get("model")
+ if isinstance(model, dict):
+ model["provider"] = provider_id
+ model["base_url"] = effective_base
+ save_config(cfg)
+ deactivate_provider()
+
+ print(f"Default model set to: {selected} (via {pconfig.name})")
+ else:
+ print("No change.")
+
+
def cmd_login(args):
"""Authenticate Hermes CLI with a provider."""
from hermes_cli.auth import login_command
@@ -1141,7 +1269,7 @@ For more help on a command:
)
chat_parser.add_argument(
"--provider",
- choices=["auto", "openrouter", "nous", "openai-codex"],
+ choices=["auto", "openrouter", "nous", "openai-codex", "zai", "kimi-coding", "minimax", "minimax-cn"],
default=None,
help="Inference provider (default: auto)"
)
diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py
index 1c00cca68..bf86fa88b 100644
--- a/hermes_cli/runtime_provider.py
+++ b/hermes_cli/runtime_provider.py
@@ -7,10 +7,12 @@ from typing import Any, Dict, Optional
from hermes_cli.auth import (
AuthError,
+ PROVIDER_REGISTRY,
format_auth_error,
resolve_provider,
resolve_nous_runtime_credentials,
resolve_codex_runtime_credentials,
+ resolve_api_key_provider_credentials,
)
from hermes_cli.config import load_config
from hermes_constants import OPENROUTER_BASE_URL
@@ -146,6 +148,19 @@ def resolve_runtime_provider(
"requested_provider": requested_provider,
}
+ # API-key providers (z.ai/GLM, Kimi, MiniMax, MiniMax-CN)
+ pconfig = PROVIDER_REGISTRY.get(provider)
+ if pconfig and pconfig.auth_type == "api_key":
+ creds = resolve_api_key_provider_credentials(provider)
+ return {
+ "provider": provider,
+ "api_mode": "chat_completions",
+ "base_url": creds.get("base_url", "").rstrip("/"),
+ "api_key": creds.get("api_key", ""),
+ "source": creds.get("source", "env"),
+ "requested_provider": requested_provider,
+ }
+
runtime = _resolve_openrouter_runtime(
requested_provider=requested_provider,
explicit_api_key=explicit_api_key,
diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py
index ef3a521d7..0aaecb425 100644
--- a/hermes_cli/setup.py
+++ b/hermes_cli/setup.py
@@ -729,6 +729,10 @@ def run_setup_wizard(args):
"Login with OpenAI Codex",
"OpenRouter API key (100+ models, pay-per-use)",
"Custom OpenAI-compatible endpoint (self-hosted / VLLM / etc.)",
+ "Z.AI / GLM (Zhipu AI models)",
+ "Kimi / Moonshot (Kimi coding models)",
+ "MiniMax (global endpoint)",
+ "MiniMax China (mainland China endpoint)",
]
if keep_label:
provider_choices.append(keep_label)
@@ -864,14 +868,143 @@ def run_setup_wizard(args):
config['model'] = model_name
save_env_value("LLM_MODEL", model_name)
print_success("Custom endpoint configured")
- # else: provider_idx == 4 (Keep current) — only shown when a provider already exists
+
+ elif provider_idx == 4: # Z.AI / GLM
+ selected_provider = "zai"
+ print()
+ print_header("Z.AI / GLM API Key")
+ pconfig = PROVIDER_REGISTRY["zai"]
+ print_info(f"Provider: {pconfig.name}")
+ print_info(f"Base URL: {pconfig.inference_base_url}")
+ print_info("Get your API key at: https://open.bigmodel.cn/")
+ print()
+
+ existing_key = get_env_value("GLM_API_KEY") or get_env_value("ZAI_API_KEY")
+ if existing_key:
+ print_info(f"Current: {existing_key[:8]}... (configured)")
+ if prompt_yes_no("Update API key?", False):
+ api_key = prompt(" GLM API key", password=True)
+ if api_key:
+ save_env_value("GLM_API_KEY", api_key)
+ print_success("GLM API key updated")
+ else:
+ api_key = prompt(" GLM API key", password=True)
+ if api_key:
+ save_env_value("GLM_API_KEY", api_key)
+ print_success("GLM API key saved")
+ else:
+ print_warning("Skipped - agent won't work without an API key")
+
+ # Clear custom endpoint vars if switching
+ if existing_custom:
+ save_env_value("OPENAI_BASE_URL", "")
+ save_env_value("OPENAI_API_KEY", "")
+ _update_config_for_provider("zai", pconfig.inference_base_url)
+
+ elif provider_idx == 5: # Kimi / Moonshot
+ selected_provider = "kimi-coding"
+ print()
+ print_header("Kimi / Moonshot API Key")
+ pconfig = PROVIDER_REGISTRY["kimi-coding"]
+ print_info(f"Provider: {pconfig.name}")
+ print_info(f"Base URL: {pconfig.inference_base_url}")
+ print_info("Get your API key at: https://platform.moonshot.cn/")
+ print()
+
+ existing_key = get_env_value("KIMI_API_KEY")
+ if existing_key:
+ print_info(f"Current: {existing_key[:8]}... (configured)")
+ if prompt_yes_no("Update API key?", False):
+ api_key = prompt(" Kimi API key", password=True)
+ if api_key:
+ save_env_value("KIMI_API_KEY", api_key)
+ print_success("Kimi API key updated")
+ else:
+ api_key = prompt(" Kimi API key", password=True)
+ if api_key:
+ save_env_value("KIMI_API_KEY", api_key)
+ print_success("Kimi API key saved")
+ else:
+ print_warning("Skipped - agent won't work without an API key")
+
+ # Clear custom endpoint vars if switching
+ if existing_custom:
+ save_env_value("OPENAI_BASE_URL", "")
+ save_env_value("OPENAI_API_KEY", "")
+ _update_config_for_provider("kimi-coding", pconfig.inference_base_url)
+
+ elif provider_idx == 6: # MiniMax
+ selected_provider = "minimax"
+ print()
+ print_header("MiniMax API Key")
+ pconfig = PROVIDER_REGISTRY["minimax"]
+ print_info(f"Provider: {pconfig.name}")
+ print_info(f"Base URL: {pconfig.inference_base_url}")
+ print_info("Get your API key at: https://platform.minimaxi.com/")
+ print()
+
+ existing_key = get_env_value("MINIMAX_API_KEY")
+ if existing_key:
+ print_info(f"Current: {existing_key[:8]}... (configured)")
+ if prompt_yes_no("Update API key?", False):
+ api_key = prompt(" MiniMax API key", password=True)
+ if api_key:
+ save_env_value("MINIMAX_API_KEY", api_key)
+ print_success("MiniMax API key updated")
+ else:
+ api_key = prompt(" MiniMax API key", password=True)
+ if api_key:
+ save_env_value("MINIMAX_API_KEY", api_key)
+ print_success("MiniMax API key saved")
+ else:
+ print_warning("Skipped - agent won't work without an API key")
+
+ # Clear custom endpoint vars if switching
+ if existing_custom:
+ save_env_value("OPENAI_BASE_URL", "")
+ save_env_value("OPENAI_API_KEY", "")
+ _update_config_for_provider("minimax", pconfig.inference_base_url)
+
+ elif provider_idx == 7: # MiniMax China
+ selected_provider = "minimax-cn"
+ print()
+ print_header("MiniMax China API Key")
+ pconfig = PROVIDER_REGISTRY["minimax-cn"]
+ print_info(f"Provider: {pconfig.name}")
+ print_info(f"Base URL: {pconfig.inference_base_url}")
+ print_info("Get your API key at: https://platform.minimaxi.com/")
+ print()
+
+ existing_key = get_env_value("MINIMAX_CN_API_KEY")
+ if existing_key:
+ print_info(f"Current: {existing_key[:8]}... (configured)")
+ if prompt_yes_no("Update API key?", False):
+ api_key = prompt(" MiniMax CN API key", password=True)
+ if api_key:
+ save_env_value("MINIMAX_CN_API_KEY", api_key)
+ print_success("MiniMax CN API key updated")
+ else:
+ api_key = prompt(" MiniMax CN API key", password=True)
+ if api_key:
+ save_env_value("MINIMAX_CN_API_KEY", api_key)
+ print_success("MiniMax CN API key saved")
+ else:
+ print_warning("Skipped - agent won't work without an API key")
+
+ # Clear custom endpoint vars if switching
+ if existing_custom:
+ save_env_value("OPENAI_BASE_URL", "")
+ save_env_value("OPENAI_API_KEY", "")
+ _update_config_for_provider("minimax-cn", pconfig.inference_base_url)
+
+ # else: provider_idx == 8 (Keep current) — only shown when a provider already exists
# =========================================================================
# Step 1b: OpenRouter API Key for tools (if not already set)
# =========================================================================
# Tools (vision, web, MoA) use OpenRouter independently of the main provider.
# Prompt for OpenRouter key if not set and a non-OpenRouter provider was chosen.
- if selected_provider in ("nous", "openai-codex", "custom") and not get_env_value("OPENROUTER_API_KEY"):
+ if selected_provider in ("nous", "openai-codex", "custom", "zai", "kimi-coding", "minimax", "minimax-cn") and not get_env_value("OPENROUTER_API_KEY"):
print()
print_header("OpenRouter API Key (for tools)")
print_info("Tools like vision analysis, web search, and MoA use OpenRouter")
@@ -944,6 +1077,60 @@ def run_setup_wizard(args):
config['model'] = custom
save_env_value("LLM_MODEL", custom)
_update_config_for_provider("openai-codex", DEFAULT_CODEX_BASE_URL)
+ elif selected_provider == "zai":
+ zai_models = ["glm-5", "glm-4.7", "glm-4.5", "glm-4.5-flash"]
+ model_choices = list(zai_models)
+ model_choices.append("Custom model")
+ model_choices.append(f"Keep current ({current_model})")
+
+ keep_idx = len(model_choices) - 1
+ model_idx = prompt_choice("Select default model:", model_choices, keep_idx)
+
+ if model_idx < len(zai_models):
+ config['model'] = zai_models[model_idx]
+ save_env_value("LLM_MODEL", zai_models[model_idx])
+ elif model_idx == len(zai_models):
+ custom = prompt("Enter model name")
+ if custom:
+ config['model'] = custom
+ save_env_value("LLM_MODEL", custom)
+ # else: keep current
+ elif selected_provider == "kimi-coding":
+ kimi_models = ["kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"]
+ model_choices = list(kimi_models)
+ model_choices.append("Custom model")
+ model_choices.append(f"Keep current ({current_model})")
+
+ keep_idx = len(model_choices) - 1
+ model_idx = prompt_choice("Select default model:", model_choices, keep_idx)
+
+ if model_idx < len(kimi_models):
+ config['model'] = kimi_models[model_idx]
+ save_env_value("LLM_MODEL", kimi_models[model_idx])
+ elif model_idx == len(kimi_models):
+ custom = prompt("Enter model name")
+ if custom:
+ config['model'] = custom
+ save_env_value("LLM_MODEL", custom)
+ # else: keep current
+ elif selected_provider in ("minimax", "minimax-cn"):
+ minimax_models = ["MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1"]
+ model_choices = list(minimax_models)
+ model_choices.append("Custom model")
+ model_choices.append(f"Keep current ({current_model})")
+
+ keep_idx = len(model_choices) - 1
+ model_idx = prompt_choice("Select default model:", model_choices, keep_idx)
+
+ if model_idx < len(minimax_models):
+ config['model'] = minimax_models[model_idx]
+ save_env_value("LLM_MODEL", minimax_models[model_idx])
+ elif model_idx == len(minimax_models):
+ custom = prompt("Enter model name")
+ if custom:
+ config['model'] = custom
+ save_env_value("LLM_MODEL", custom)
+ # else: keep current
else:
# Static list for OpenRouter / fallback (from canonical list)
from hermes_cli.models import model_ids, menu_labels
diff --git a/hermes_cli/status.py b/hermes_cli/status.py
index 1604f5477..7c94b5852 100644
--- a/hermes_cli/status.py
+++ b/hermes_cli/status.py
@@ -79,6 +79,10 @@ def show_status(args):
"OpenRouter": "OPENROUTER_API_KEY",
"Anthropic": "ANTHROPIC_API_KEY",
"OpenAI": "OPENAI_API_KEY",
+ "Z.AI/GLM": "GLM_API_KEY",
+ "Kimi": "KIMI_API_KEY",
+ "MiniMax": "MINIMAX_API_KEY",
+ "MiniMax-CN": "MINIMAX_CN_API_KEY",
"Firecrawl": "FIRECRAWL_API_KEY",
"Browserbase": "BROWSERBASE_API_KEY",
"FAL": "FAL_KEY",
@@ -137,6 +141,28 @@ def show_status(args):
if codex_status.get("error") and not codex_logged_in:
print(f" Error: {codex_status.get('error')}")
+ # =========================================================================
+ # API-Key Providers
+ # =========================================================================
+ print()
+ print(color("◆ API-Key Providers", Colors.CYAN, Colors.BOLD))
+
+ apikey_providers = {
+ "Z.AI / GLM": ("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"),
+ "Kimi / Moonshot": ("KIMI_API_KEY",),
+ "MiniMax": ("MINIMAX_API_KEY",),
+ "MiniMax (China)": ("MINIMAX_CN_API_KEY",),
+ }
+ for pname, env_vars in apikey_providers.items():
+ key_val = ""
+ for ev in env_vars:
+ key_val = get_env_value(ev) or ""
+ if key_val:
+ break
+ configured = bool(key_val)
+ label = "configured" if configured else "not configured (run: hermes model)"
+ print(f" {pname:<16} {check_mark(configured)} {label}")
+
# =========================================================================
# Terminal Configuration
# =========================================================================
diff --git a/tests/test_api_key_providers.py b/tests/test_api_key_providers.py
new file mode 100644
index 000000000..a6be4d99f
--- /dev/null
+++ b/tests/test_api_key_providers.py
@@ -0,0 +1,342 @@
+"""Tests for API-key provider support (z.ai/GLM, Kimi, MiniMax)."""
+
+import os
+import sys
+import types
+
+import pytest
+
+# Ensure dotenv doesn't interfere
+if "dotenv" not in sys.modules:
+ fake_dotenv = types.ModuleType("dotenv")
+ fake_dotenv.load_dotenv = lambda *args, **kwargs: None
+ sys.modules["dotenv"] = fake_dotenv
+
+from hermes_cli.auth import (
+ PROVIDER_REGISTRY,
+ ProviderConfig,
+ resolve_provider,
+ get_api_key_provider_status,
+ resolve_api_key_provider_credentials,
+ get_auth_status,
+ AuthError,
+)
+
+
+# =============================================================================
+# Provider Registry tests
+# =============================================================================
+
+class TestProviderRegistry:
+ """Test that new providers are correctly registered."""
+
+ @pytest.mark.parametrize("provider_id,name,auth_type", [
+ ("zai", "Z.AI / GLM", "api_key"),
+ ("kimi-coding", "Kimi / Moonshot", "api_key"),
+ ("minimax", "MiniMax", "api_key"),
+ ("minimax-cn", "MiniMax (China)", "api_key"),
+ ])
+ def test_provider_registered(self, provider_id, name, auth_type):
+ assert provider_id in PROVIDER_REGISTRY
+ pconfig = PROVIDER_REGISTRY[provider_id]
+ assert pconfig.name == name
+ assert pconfig.auth_type == auth_type
+ assert pconfig.inference_base_url # must have a default base URL
+
+ def test_zai_env_vars(self):
+ pconfig = PROVIDER_REGISTRY["zai"]
+ assert pconfig.api_key_env_vars == ("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY")
+ assert pconfig.base_url_env_var == "GLM_BASE_URL"
+
+ def test_kimi_env_vars(self):
+ pconfig = PROVIDER_REGISTRY["kimi-coding"]
+ assert pconfig.api_key_env_vars == ("KIMI_API_KEY",)
+ assert pconfig.base_url_env_var == "KIMI_BASE_URL"
+
+ def test_minimax_env_vars(self):
+ pconfig = PROVIDER_REGISTRY["minimax"]
+ assert pconfig.api_key_env_vars == ("MINIMAX_API_KEY",)
+ assert pconfig.base_url_env_var == "MINIMAX_BASE_URL"
+
+ def test_minimax_cn_env_vars(self):
+ pconfig = PROVIDER_REGISTRY["minimax-cn"]
+ assert pconfig.api_key_env_vars == ("MINIMAX_CN_API_KEY",)
+ assert pconfig.base_url_env_var == "MINIMAX_CN_BASE_URL"
+
+ def test_base_urls(self):
+ assert PROVIDER_REGISTRY["zai"].inference_base_url == "https://api.z.ai/api/paas/v4"
+ assert PROVIDER_REGISTRY["kimi-coding"].inference_base_url == "https://api.moonshot.ai/v1"
+ assert PROVIDER_REGISTRY["minimax"].inference_base_url == "https://api.minimax.io/v1"
+ assert PROVIDER_REGISTRY["minimax-cn"].inference_base_url == "https://api.minimaxi.com/v1"
+
+ def test_oauth_providers_unchanged(self):
+ """Ensure we didn't break the existing OAuth providers."""
+ assert "nous" in PROVIDER_REGISTRY
+ assert PROVIDER_REGISTRY["nous"].auth_type == "oauth_device_code"
+ assert "openai-codex" in PROVIDER_REGISTRY
+ assert PROVIDER_REGISTRY["openai-codex"].auth_type == "oauth_external"
+
+
+# =============================================================================
+# Provider Resolution tests
+# =============================================================================
+
+PROVIDER_ENV_VARS = (
+ "OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
+ "GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY",
+ "KIMI_API_KEY", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY",
+ "OPENAI_BASE_URL",
+)
+
+
+@pytest.fixture(autouse=True)
+def _clear_provider_env(monkeypatch):
+ for key in PROVIDER_ENV_VARS:
+ monkeypatch.delenv(key, raising=False)
+
+
+class TestResolveProvider:
+ """Test resolve_provider() with new providers."""
+
+ def test_explicit_zai(self):
+ assert resolve_provider("zai") == "zai"
+
+ def test_explicit_kimi_coding(self):
+ assert resolve_provider("kimi-coding") == "kimi-coding"
+
+ def test_explicit_minimax(self):
+ assert resolve_provider("minimax") == "minimax"
+
+ def test_explicit_minimax_cn(self):
+ assert resolve_provider("minimax-cn") == "minimax-cn"
+
+ def test_alias_glm(self):
+ assert resolve_provider("glm") == "zai"
+
+ def test_alias_z_ai(self):
+ assert resolve_provider("z-ai") == "zai"
+
+ def test_alias_zhipu(self):
+ assert resolve_provider("zhipu") == "zai"
+
+ def test_alias_kimi(self):
+ assert resolve_provider("kimi") == "kimi-coding"
+
+ def test_alias_moonshot(self):
+ assert resolve_provider("moonshot") == "kimi-coding"
+
+ def test_alias_minimax_underscore(self):
+ assert resolve_provider("minimax_cn") == "minimax-cn"
+
+ def test_alias_case_insensitive(self):
+ assert resolve_provider("GLM") == "zai"
+ assert resolve_provider("Z-AI") == "zai"
+ assert resolve_provider("Kimi") == "kimi-coding"
+
+ def test_unknown_provider_raises(self):
+ with pytest.raises(AuthError):
+ resolve_provider("nonexistent-provider-xyz")
+
+ def test_auto_detects_glm_key(self, monkeypatch):
+ monkeypatch.setenv("GLM_API_KEY", "test-glm-key")
+ assert resolve_provider("auto") == "zai"
+
+ def test_auto_detects_zai_key(self, monkeypatch):
+ monkeypatch.setenv("ZAI_API_KEY", "test-zai-key")
+ assert resolve_provider("auto") == "zai"
+
+ def test_auto_detects_z_ai_key(self, monkeypatch):
+ monkeypatch.setenv("Z_AI_API_KEY", "test-z-ai-key")
+ assert resolve_provider("auto") == "zai"
+
+ def test_auto_detects_kimi_key(self, monkeypatch):
+ monkeypatch.setenv("KIMI_API_KEY", "test-kimi-key")
+ assert resolve_provider("auto") == "kimi-coding"
+
+ def test_auto_detects_minimax_key(self, monkeypatch):
+ monkeypatch.setenv("MINIMAX_API_KEY", "test-mm-key")
+ assert resolve_provider("auto") == "minimax"
+
+ def test_auto_detects_minimax_cn_key(self, monkeypatch):
+ monkeypatch.setenv("MINIMAX_CN_API_KEY", "test-mm-cn-key")
+ assert resolve_provider("auto") == "minimax-cn"
+
+ def test_openrouter_takes_priority_over_glm(self, monkeypatch):
+ """OpenRouter API key should win over GLM in auto-detection."""
+ monkeypatch.setenv("OPENROUTER_API_KEY", "or-key")
+ monkeypatch.setenv("GLM_API_KEY", "glm-key")
+ assert resolve_provider("auto") == "openrouter"
+
+
+# =============================================================================
+# API Key Provider Status tests
+# =============================================================================
+
+class TestApiKeyProviderStatus:
+
+ def test_unconfigured_provider(self):
+ status = get_api_key_provider_status("zai")
+ assert status["configured"] is False
+ assert status["logged_in"] is False
+
+ def test_configured_provider(self, monkeypatch):
+ monkeypatch.setenv("GLM_API_KEY", "test-key-123")
+ status = get_api_key_provider_status("zai")
+ assert status["configured"] is True
+ assert status["logged_in"] is True
+ assert status["key_source"] == "GLM_API_KEY"
+ assert "z.ai" in status["base_url"].lower() or "api.z.ai" in status["base_url"]
+
+ def test_fallback_env_var(self, monkeypatch):
+ """ZAI_API_KEY should work when GLM_API_KEY is not set."""
+ monkeypatch.setenv("ZAI_API_KEY", "zai-fallback-key")
+ status = get_api_key_provider_status("zai")
+ assert status["configured"] is True
+ assert status["key_source"] == "ZAI_API_KEY"
+
+ def test_custom_base_url(self, monkeypatch):
+ monkeypatch.setenv("KIMI_API_KEY", "kimi-key")
+ monkeypatch.setenv("KIMI_BASE_URL", "https://custom.kimi.example/v1")
+ status = get_api_key_provider_status("kimi-coding")
+ assert status["base_url"] == "https://custom.kimi.example/v1"
+
+ def test_get_auth_status_dispatches_to_api_key(self, monkeypatch):
+ monkeypatch.setenv("MINIMAX_API_KEY", "mm-key")
+ status = get_auth_status("minimax")
+ assert status["configured"] is True
+ assert status["provider"] == "minimax"
+
+ def test_non_api_key_provider(self):
+ status = get_api_key_provider_status("nous")
+ assert status["configured"] is False
+
+
+# =============================================================================
+# Credential Resolution tests
+# =============================================================================
+
+class TestResolveApiKeyProviderCredentials:
+
+ def test_resolve_zai_with_key(self, monkeypatch):
+ monkeypatch.setenv("GLM_API_KEY", "glm-secret-key")
+ creds = resolve_api_key_provider_credentials("zai")
+ assert creds["provider"] == "zai"
+ assert creds["api_key"] == "glm-secret-key"
+ assert creds["base_url"] == "https://api.z.ai/api/paas/v4"
+ assert creds["source"] == "GLM_API_KEY"
+
+ def test_resolve_kimi_with_key(self, monkeypatch):
+ monkeypatch.setenv("KIMI_API_KEY", "kimi-secret-key")
+ creds = resolve_api_key_provider_credentials("kimi-coding")
+ assert creds["provider"] == "kimi-coding"
+ assert creds["api_key"] == "kimi-secret-key"
+ assert creds["base_url"] == "https://api.moonshot.ai/v1"
+
+ def test_resolve_minimax_with_key(self, monkeypatch):
+ monkeypatch.setenv("MINIMAX_API_KEY", "mm-secret-key")
+ creds = resolve_api_key_provider_credentials("minimax")
+ assert creds["provider"] == "minimax"
+ assert creds["api_key"] == "mm-secret-key"
+ assert creds["base_url"] == "https://api.minimax.io/v1"
+
+ def test_resolve_minimax_cn_with_key(self, monkeypatch):
+ monkeypatch.setenv("MINIMAX_CN_API_KEY", "mmcn-secret-key")
+ creds = resolve_api_key_provider_credentials("minimax-cn")
+ assert creds["provider"] == "minimax-cn"
+ assert creds["api_key"] == "mmcn-secret-key"
+ assert creds["base_url"] == "https://api.minimaxi.com/v1"
+
+ def test_resolve_with_custom_base_url(self, monkeypatch):
+ monkeypatch.setenv("GLM_API_KEY", "glm-key")
+ monkeypatch.setenv("GLM_BASE_URL", "https://custom.glm.example/v4")
+ creds = resolve_api_key_provider_credentials("zai")
+ assert creds["base_url"] == "https://custom.glm.example/v4"
+
+ def test_resolve_without_key_returns_empty(self):
+ creds = resolve_api_key_provider_credentials("zai")
+ assert creds["api_key"] == ""
+ assert creds["source"] == "default"
+
+ def test_resolve_invalid_provider_raises(self):
+ with pytest.raises(AuthError):
+ resolve_api_key_provider_credentials("nous")
+
+ def test_glm_key_priority(self, monkeypatch):
+ """GLM_API_KEY takes priority over ZAI_API_KEY."""
+ monkeypatch.setenv("GLM_API_KEY", "primary")
+ monkeypatch.setenv("ZAI_API_KEY", "secondary")
+ creds = resolve_api_key_provider_credentials("zai")
+ assert creds["api_key"] == "primary"
+ assert creds["source"] == "GLM_API_KEY"
+
+ def test_zai_key_fallback(self, monkeypatch):
+ """ZAI_API_KEY used when GLM_API_KEY not set."""
+ monkeypatch.setenv("ZAI_API_KEY", "secondary")
+ creds = resolve_api_key_provider_credentials("zai")
+ assert creds["api_key"] == "secondary"
+ assert creds["source"] == "ZAI_API_KEY"
+
+
+# =============================================================================
+# Runtime Provider Resolution tests
+# =============================================================================
+
+class TestRuntimeProviderResolution:
+
+ def test_runtime_zai(self, monkeypatch):
+ monkeypatch.setenv("GLM_API_KEY", "glm-key")
+ from hermes_cli.runtime_provider import resolve_runtime_provider
+ result = resolve_runtime_provider(requested="zai")
+ assert result["provider"] == "zai"
+ assert result["api_mode"] == "chat_completions"
+ assert result["api_key"] == "glm-key"
+ assert "z.ai" in result["base_url"] or "api.z.ai" in result["base_url"]
+
+ def test_runtime_kimi(self, monkeypatch):
+ monkeypatch.setenv("KIMI_API_KEY", "kimi-key")
+ from hermes_cli.runtime_provider import resolve_runtime_provider
+ result = resolve_runtime_provider(requested="kimi-coding")
+ assert result["provider"] == "kimi-coding"
+ assert result["api_mode"] == "chat_completions"
+ assert result["api_key"] == "kimi-key"
+
+ def test_runtime_minimax(self, monkeypatch):
+ monkeypatch.setenv("MINIMAX_API_KEY", "mm-key")
+ from hermes_cli.runtime_provider import resolve_runtime_provider
+ result = resolve_runtime_provider(requested="minimax")
+ assert result["provider"] == "minimax"
+ assert result["api_key"] == "mm-key"
+
+ def test_runtime_auto_detects_api_key_provider(self, monkeypatch):
+ monkeypatch.setenv("KIMI_API_KEY", "auto-kimi-key")
+ from hermes_cli.runtime_provider import resolve_runtime_provider
+ result = resolve_runtime_provider(requested="auto")
+ assert result["provider"] == "kimi-coding"
+ assert result["api_key"] == "auto-kimi-key"
+
+
+# =============================================================================
+# _has_any_provider_configured tests
+# =============================================================================
+
+class TestHasAnyProviderConfigured:
+
+ def test_glm_key_counts(self, monkeypatch, tmp_path):
+ from hermes_cli import config as config_module
+ monkeypatch.setenv("GLM_API_KEY", "test-key")
+ hermes_home = tmp_path / ".hermes"
+ hermes_home.mkdir()
+ monkeypatch.setattr(config_module, "get_env_path", lambda: hermes_home / ".env")
+ monkeypatch.setattr(config_module, "get_hermes_home", lambda: hermes_home)
+ from hermes_cli.main import _has_any_provider_configured
+ assert _has_any_provider_configured() is True
+
+ def test_minimax_key_counts(self, monkeypatch, tmp_path):
+ from hermes_cli import config as config_module
+ monkeypatch.setenv("MINIMAX_API_KEY", "test-key")
+ hermes_home = tmp_path / ".hermes"
+ hermes_home.mkdir()
+ monkeypatch.setattr(config_module, "get_env_path", lambda: hermes_home / ".env")
+ monkeypatch.setattr(config_module, "get_hermes_home", lambda: hermes_home)
+ from hermes_cli.main import _has_any_provider_configured
+ assert _has_any_provider_configured() is True
diff --git a/website/docs/reference/cli-commands.md b/website/docs/reference/cli-commands.md
index 7606d9a8c..d142bb4bf 100644
--- a/website/docs/reference/cli-commands.md
+++ b/website/docs/reference/cli-commands.md
@@ -19,7 +19,7 @@ These are commands you run from your shell.
| `hermes chat --continue` / `-c` | Resume the most recent session |
| `hermes chat --resume ` / `-r ` | Resume a specific session |
| `hermes chat --model ` | Use a specific model |
-| `hermes chat --provider ` | Force a provider (`nous`, `openrouter`) |
+| `hermes chat --provider ` | Force a provider (`nous`, `openrouter`, `zai`, `kimi-coding`, `minimax`, `minimax-cn`) |
| `hermes chat --toolsets "web,terminal"` / `-t` | Use specific toolsets |
| `hermes chat --verbose` | Enable verbose/debug output |
diff --git a/website/docs/reference/environment-variables.md b/website/docs/reference/environment-variables.md
index c35baeeaf..e82c14933 100644
--- a/website/docs/reference/environment-variables.md
+++ b/website/docs/reference/environment-variables.md
@@ -15,6 +15,14 @@ All variables go in `~/.hermes/.env`. You can also set them with `hermes config
| `OPENROUTER_API_KEY` | OpenRouter API key (recommended for flexibility) |
| `OPENAI_API_KEY` | API key for custom OpenAI-compatible endpoints (used with `OPENAI_BASE_URL`) |
| `OPENAI_BASE_URL` | Base URL for custom endpoint (VLLM, SGLang, etc.) |
+| `GLM_API_KEY` | z.ai / ZhipuAI GLM API key ([z.ai](https://z.ai)) |
+| `GLM_BASE_URL` | Override z.ai base URL (default: `https://api.z.ai/api/paas/v4`) |
+| `KIMI_API_KEY` | Kimi / Moonshot AI API key ([moonshot.ai](https://platform.moonshot.ai)) |
+| `KIMI_BASE_URL` | Override Kimi base URL (default: `https://api.moonshot.ai/v1`) |
+| `MINIMAX_API_KEY` | MiniMax API key — global endpoint ([minimax.io](https://www.minimax.io)) |
+| `MINIMAX_BASE_URL` | Override MiniMax base URL (default: `https://api.minimax.io/v1`) |
+| `MINIMAX_CN_API_KEY` | MiniMax API key — China endpoint ([minimaxi.com](https://www.minimaxi.com)) |
+| `MINIMAX_CN_BASE_URL` | Override MiniMax China base URL (default: `https://api.minimaxi.com/v1`) |
| `HERMES_MODEL` | Preferred model name (checked before `LLM_MODEL`, used by gateway) |
| `LLM_MODEL` | Default model name (fallback when not set in config.yaml) |
| `VOICE_TOOLS_OPENAI_KEY` | OpenAI key for TTS and voice transcription (separate from custom endpoint) |
@@ -24,7 +32,7 @@ All variables go in `~/.hermes/.env`. You can also set them with `hermes config
| Variable | Description |
|----------|-------------|
-| `HERMES_INFERENCE_PROVIDER` | Override provider selection: `auto`, `openrouter`, `nous` (default: `auto`) |
+| `HERMES_INFERENCE_PROVIDER` | Override provider selection: `auto`, `openrouter`, `nous`, `zai`, `kimi-coding`, `minimax`, `minimax-cn` (default: `auto`) |
| `HERMES_PORTAL_BASE_URL` | Override Nous Portal URL (for development/testing) |
| `NOUS_INFERENCE_BASE_URL` | Override Nous inference API URL |
| `HERMES_NOUS_MIN_KEY_TTL_SECONDS` | Min agent key TTL before re-mint (default: 1800 = 30min) |
diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md
index ac045c8d4..6d6897794 100644
--- a/website/docs/user-guide/configuration.md
+++ b/website/docs/user-guide/configuration.md
@@ -64,6 +64,10 @@ You need at least one way to connect to an LLM. Use `hermes model` to switch pro
| **Nous Portal** | `hermes model` (OAuth, subscription-based) |
| **OpenAI Codex** | `hermes model` (ChatGPT OAuth, uses Codex models) |
| **OpenRouter** | `OPENROUTER_API_KEY` in `~/.hermes/.env` |
+| **z.ai / GLM** | `GLM_API_KEY` in `~/.hermes/.env` (provider: `zai`) |
+| **Kimi / Moonshot** | `KIMI_API_KEY` in `~/.hermes/.env` (provider: `kimi-coding`) |
+| **MiniMax** | `MINIMAX_API_KEY` in `~/.hermes/.env` (provider: `minimax`) |
+| **MiniMax China** | `MINIMAX_CN_API_KEY` in `~/.hermes/.env` (provider: `minimax-cn`) |
| **Custom Endpoint** | `OPENAI_BASE_URL` + `OPENAI_API_KEY` in `~/.hermes/.env` |
:::info Codex Note
@@ -74,6 +78,37 @@ The OpenAI Codex provider authenticates via device code (open a URL, enter a cod
Even when using Nous Portal, Codex, or a custom endpoint, some tools (vision, web summarization, MoA) use OpenRouter independently. An `OPENROUTER_API_KEY` enables these tools.
:::
+### First-Class Chinese AI Providers
+
+These providers have built-in support with dedicated provider IDs. Set the API key and use `--provider` to select:
+
+```bash
+# z.ai / ZhipuAI GLM
+hermes chat --provider zai --model glm-4-plus
+# Requires: GLM_API_KEY in ~/.hermes/.env
+
+# Kimi / Moonshot AI
+hermes chat --provider kimi-coding --model moonshot-v1-auto
+# Requires: KIMI_API_KEY in ~/.hermes/.env
+
+# MiniMax (global endpoint)
+hermes chat --provider minimax --model MiniMax-Text-01
+# Requires: MINIMAX_API_KEY in ~/.hermes/.env
+
+# MiniMax (China endpoint)
+hermes chat --provider minimax-cn --model MiniMax-Text-01
+# Requires: MINIMAX_CN_API_KEY in ~/.hermes/.env
+```
+
+Or set the provider permanently in `config.yaml`:
+```yaml
+model:
+ provider: "zai" # or: kimi-coding, minimax, minimax-cn
+ default: "glm-4-plus"
+```
+
+Base URLs can be overridden with `GLM_BASE_URL`, `KIMI_BASE_URL`, `MINIMAX_BASE_URL`, or `MINIMAX_CN_BASE_URL` environment variables.
+
## Custom & Self-Hosted LLM Providers
Hermes Agent works with **any OpenAI-compatible API endpoint**. If a server implements `/v1/chat/completions`, you can point Hermes at it. This means you can use local models, GPU inference servers, multi-provider routers, or any third-party API.
@@ -290,6 +325,7 @@ LLM_MODEL=meta-llama/Llama-3.1-70B-Instruct-Turbo
| **Cost optimization** | ClawRouter or OpenRouter with `sort: "price"` |
| **Maximum privacy** | Ollama, vLLM, or llama.cpp (fully local) |
| **Enterprise / Azure** | Azure OpenAI with custom endpoint |
+| **Chinese AI models** | z.ai (GLM), Kimi/Moonshot, or MiniMax (first-class providers) |
:::tip
You can switch between providers at any time with `hermes model` — no restart required. Your conversation history, memory, and skills carry over regardless of which provider you use.