From b193f6b91739c1a77fc4050be2418ab077e23d78 Mon Sep 17 00:00:00 2001 From: ymy Date: Wed, 22 Apr 2026 13:07:27 +0530 Subject: [PATCH 1/3] =?UTF-8?q?feat(providers):=20split=20zai=20into=204?= =?UTF-8?q?=20plans=20(Global/China=20=C3=97=20direct=20API/Coding=20Plan)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the single zai provider (with fragile runtime HTTP endpoint probing) with 4 explicit static providers: | Hermes id | Endpoint | Env var | |--------------------|-------------------------------------------|--------------------| | zai | api.z.ai/api/paas/v4 | ZAI_API_KEY | | zai-cn | open.bigmodel.cn/api/paas/v4 | GLM_API_KEY | | zai-coding-global | api.z.ai/api/coding/paas/v4 | ZAI_CODING_API_KEY | | zai-coding-cn | open.bigmodel.cn/api/coding/paas/v4 | GLM_CODING_API_KEY | - Remove detect_zai_endpoint / _resolve_zai_base_url (eliminates ~8s probes) - Thread 4 plans through auth, providers, models, status, doctor, setup, auxiliary routing, model normalization, trajectory compressor - Legacy GLM_API_KEY fallback on zai provider for upgrade safety - zai-cn declared before zai in registry so auto-detect with only GLM_API_KEY prefers the China plan (backward compat) Based on #13500 by @yuanmingyi. --- agent/auxiliary_client.py | 13 +- agent/credential_pool.py | 3 - agent/model_metadata.py | 3 +- agent/models_dev.py | 3 + cli.py | 4 +- gateway/platforms/qqbot/adapter.py | 23 +- hermes_cli/auth.py | 149 +++------- hermes_cli/config.py | 60 ++-- hermes_cli/doctor.py | 13 +- hermes_cli/dump.py | 5 +- hermes_cli/main.py | 4 +- hermes_cli/model_normalize.py | 3 + hermes_cli/models.py | 36 ++- hermes_cli/providers.py | 28 +- hermes_cli/setup.py | 10 +- hermes_cli/status.py | 20 +- run_agent.py | 2 +- tests/agent/test_auxiliary_client.py | 71 +++++ tests/hermes_cli/test_api_key_providers.py | 269 +++++++++++++----- .../test_model_provider_persistence.py | 46 +-- tests/hermes_cli/test_model_validation.py | 10 +- trajectory_compressor.py | 14 +- 22 files changed, 520 insertions(+), 269 deletions(-) diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index e3957dab56bd2..0d0e72ebf6657 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -62,10 +62,10 @@ "x-ai": "xai", "x.ai": "xai", "grok": "xai", - "glm": "zai", "z-ai": "zai", "z.ai": "zai", - "zhipu": "zai", + "zhipu": "zai-cn", + "glm": "zai-cn", "kimi": "kimi-coding", "moonshot": "kimi-coding", "kimi-cn": "kimi-coding-cn", @@ -132,7 +132,10 @@ def _fixed_temperature_for_model( # Default auxiliary models for direct API-key providers (cheap/fast for side tasks) _API_KEY_PROVIDER_AUX_MODELS: Dict[str, str] = { "gemini": "gemini-3-flash-preview", - "zai": "glm-4.5-flash", + "zai": "glm-4.7-flashx", + "zai-cn": "glm-4.7-flashx", + "zai-coding-cn": "glm-4.7", + "zai-coding-global": "glm-4.7", "kimi-coding": "kimi-k2-turbo-preview", "kimi-coding-cn": "kimi-k2-turbo-preview", "minimax": "MiniMax-M2.7", @@ -152,6 +155,7 @@ def _fixed_temperature_for_model( _PROVIDER_VISION_MODELS: Dict[str, str] = { "xiaomi": "mimo-v2-omni", "zai": "glm-5v-turbo", + "zai-cn": "glm-5v-turbo", } # OpenRouter app attribution headers @@ -1534,7 +1538,8 @@ def resolve_provider_client( Args: provider: Provider identifier. One of: "openrouter", "nous", "openai-codex" (or "codex"), - "zai", "kimi-coding", "minimax", "minimax-cn", + "zai", "zai-cn", "zai-coding-cn", "zai-coding-global", + "kimi-coding", "minimax", "minimax-cn", "custom" (OPENAI_BASE_URL + OPENAI_API_KEY), "auto" (full auto-detection chain). model: Model slug override. If None, uses the provider's default diff --git a/agent/credential_pool.py b/agent/credential_pool.py index de8d03185a8a2..8ee53e2297b4c 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -25,7 +25,6 @@ _load_auth_store, _load_provider_state, _resolve_kimi_base_url, - _resolve_zai_base_url, _save_auth_store, _save_provider_state, read_credential_pool, @@ -1216,8 +1215,6 @@ def _is_source_suppressed(_p, _s): # type: ignore[misc] base_url = env_url or pconfig.inference_base_url if provider == "kimi-coding": base_url = _resolve_kimi_base_url(token, pconfig.inference_base_url, env_url) - elif provider == "zai": - base_url = _resolve_zai_base_url(token, pconfig.inference_base_url, env_url) changed |= _upsert_entry( entries, provider, diff --git a/agent/model_metadata.py b/agent/model_metadata.py index 6506bffe6de46..c2d794a54f67f 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -25,7 +25,7 @@ # 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", "ollama-cloud", "zai", "kimi-coding", "kimi-coding-cn", "minimax", "minimax-cn", "anthropic", "deepseek", + "gemini", "ollama-cloud", "zai", "zai-cn", "zai-coding-cn", "zai-coding-global", "kimi-coding", "kimi-coding-cn", "minimax", "minimax-cn", "anthropic", "deepseek", "opencode-zen", "opencode-go", "ai-gateway", "kilocode", "alibaba", "qwen-oauth", "xiaomi", @@ -234,6 +234,7 @@ def _is_custom_endpoint(base_url: str) -> bool: "chatgpt.com": "openai", "api.anthropic.com": "anthropic", "api.z.ai": "zai", + "open.bigmodel.cn": "zai-cn", "api.moonshot.ai": "kimi-coding", "api.moonshot.cn": "kimi-coding-cn", "api.kimi.com": "kimi-coding", diff --git a/agent/models_dev.py b/agent/models_dev.py index 3e5c911e7eec9..b9add1027a95c 100644 --- a/agent/models_dev.py +++ b/agent/models_dev.py @@ -145,6 +145,9 @@ class ProviderInfo: "openai": "openai", "openai-codex": "openai", "zai": "zai", + "zai-cn": "zhipuai", + "zai-coding-global": "zai-coding-plan", + "zai-coding-cn": "zhipuai-coding-plan", "kimi-coding": "kimi-for-coding", "kimi-coding-cn": "kimi-for-coding", "minimax": "minimax", diff --git a/cli.py b/cli.py index 588988d8c0f40..19444cc392f95 100644 --- a/cli.py +++ b/cli.py @@ -1753,7 +1753,7 @@ def __init__( 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", "zai", "kimi-coding", "minimax", "minimax-cn") + provider: Inference provider ("auto", "openrouter", "nous", "openai-codex", "zai", "zai-cn", "zai-coding-cn", "zai-coding-global", "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 shared with subagents (default: 90) @@ -10765,7 +10765,7 @@ def main( toolsets: Comma-separated list of toolsets to enable (e.g., "web,terminal") skills: Comma-separated or repeated list of skills to preload for the session model: Model to use (default: anthropic/claude-opus-4-20250514) - provider: Inference provider ("auto", "openrouter", "nous", "openai-codex", "zai", "kimi-coding", "minimax", "minimax-cn") + provider: Inference provider ("auto", "openrouter", "nous", "openai-codex", "zai", "zai-cn", "zai-coding-cn", "zai-coding-global", "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/gateway/platforms/qqbot/adapter.py b/gateway/platforms/qqbot/adapter.py index df3987f2ebdd2..08645b437f26b 100644 --- a/gateway/platforms/qqbot/adapter.py +++ b/gateway/platforms/qqbot/adapter.py @@ -18,7 +18,7 @@ group_allow_from: ["group_openid_1"] stt: # Voice-to-text config (optional) provider: "zai" # zai (GLM-ASR), openai (Whisper), etc. - baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" + baseUrl: "https://open.bigmodel.cn/api/paas/v4" apiKey: "your-stt-api-key" # or set QQ_STT_API_KEY env var model: "glm-asr" # glm-asr, whisper-1, etc. @@ -1612,10 +1612,19 @@ def _resolve_stt_config(self) -> Optional[Dict[str, str]]: if api_key: provider = stt_cfg.get("provider", "zai") # Map provider to base URL + # GLM-ASR is only served from open.bigmodel.cn/api/paas/v4 — it + # is not hosted on the coding/paas endpoints. Map both direct + # API and Coding Plan providers to that URL so STT still works + # regardless of which zai-family plan the user has configured. _PROVIDER_BASE_URLS = { - "zai": "https://open.bigmodel.cn/api/coding/paas/v4", + "zai": "https://open.bigmodel.cn/api/paas/v4", "openai": "https://api.openai.com/v1", - "glm": "https://open.bigmodel.cn/api/coding/paas/v4", + "zai-cn": "https://open.bigmodel.cn/api/paas/v4", + "zai-coding-cn": "https://open.bigmodel.cn/api/paas/v4", + "zai-coding-global": "https://open.bigmodel.cn/api/paas/v4", + # Legacy alias — kept for forward compatibility with older + # channels.qqbot.stt.provider values. + "glm": "https://open.bigmodel.cn/api/paas/v4", } base_url = _PROVIDER_BASE_URLS.get(provider, "") if base_url: @@ -1623,7 +1632,11 @@ def _resolve_stt_config(self) -> Optional[Dict[str, str]]: "base_url": base_url, "api_key": api_key, "model": model - or ("glm-asr" if provider in ("zai", "glm") else "whisper-1"), + or ( + "glm-asr" + if provider in ("zai", "zai-cn", "zai-coding-cn", "zai-coding-global", "glm") + else "whisper-1" + ), } # 2. QQ-specific env vars (set by `hermes setup gateway` / `hermes gateway`) @@ -1631,7 +1644,7 @@ def _resolve_stt_config(self) -> Optional[Dict[str, str]]: if qq_stt_key: base_url = os.getenv( "QQ_STT_BASE_URL", - "https://open.bigmodel.cn/api/coding/paas/v4", + "https://open.bigmodel.cn/api/paas/v4", ) model = os.getenv("QQ_STT_MODEL", "glm-asr") return { diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 98dfa60597327..c1af5b510f279 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -156,13 +156,43 @@ class ProviderConfig: api_key_env_vars=("GOOGLE_API_KEY", "GEMINI_API_KEY"), base_url_env_var="GEMINI_BASE_URL", ), + # zai-cn is declared before zai so auto-detect (which iterates this dict + # in insertion order) prefers `zai-cn` when only GLM_API_KEY is set — + # GLM_API_KEY appears as a legacy fallback in zai's env tuple too. + "zai-cn": ProviderConfig( + id="zai-cn", + name="Zhipu AI", + auth_type="api_key", + inference_base_url="https://open.bigmodel.cn/api/paas/v4", + api_key_env_vars=("GLM_API_KEY",), + base_url_env_var="GLM_BASE_URL", + ), "zai": ProviderConfig( id="zai", - name="Z.AI / GLM", + name="Z.AI", 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", + # GLM_API_KEY is the legacy env var — kept here as a last-resort + # fallback so existing `provider: zai` configs with only GLM_API_KEY + # set don't break on upgrade. New installs should prefer ZAI_API_KEY. + api_key_env_vars=("ZAI_API_KEY", "Z_AI_API_KEY", "GLM_API_KEY"), + base_url_env_var="ZAI_BASE_URL", + ), + "zai-coding-cn": ProviderConfig( + id="zai-coding-cn", + name="Zhipu AI Coding Plan", + auth_type="api_key", + inference_base_url="https://open.bigmodel.cn/api/coding/paas/v4", + api_key_env_vars=("GLM_CODING_API_KEY",), + base_url_env_var="GLM_CODING_BASE_URL", + ), + "zai-coding-global": ProviderConfig( + id="zai-coding-global", + name="Z.AI Coding Plan", + auth_type="api_key", + inference_base_url="https://api.z.ai/api/coding/paas/v4", + api_key_env_vars=("ZAI_CODING_API_KEY",), + base_url_env_var="ZAI_CODING_BASE_URL", ), "kimi-coding": ProviderConfig( id="kimi-coding", @@ -354,7 +384,6 @@ def get_anthropic_key() -> str: # "/coding/v1/v1/messages" (a 404). KIMI_CODE_BASE_URL = "https://api.kimi.com/coding" - def _resolve_kimi_base_url(api_key: str, default_url: str, env_override: str) -> str: """Return the correct Kimi base URL based on the API key prefix. @@ -424,113 +453,6 @@ def _resolve_api_key_provider_secret( return "", "" -# ============================================================================= -# Z.AI Endpoint Detection -# ============================================================================= - -# Z.AI has separate billing for general vs coding plans, and global vs China -# endpoints. A key that works on one may return "Insufficient balance" on -# another. We probe at setup time and store the working endpoint. -# Each entry lists candidate models to try in order — newer coding plan accounts -# may only have access to recent models (glm-5.1, glm-5v-turbo) while older -# ones still use glm-4.7. - -ZAI_ENDPOINTS = [ - # (id, base_url, probe_models, label) - ("global", "https://api.z.ai/api/paas/v4", ["glm-5"], "Global"), - ("cn", "https://open.bigmodel.cn/api/paas/v4", ["glm-5"], "China"), - ("coding-global", "https://api.z.ai/api/coding/paas/v4", ["glm-5.1", "glm-5v-turbo", "glm-4.7"], "Global (Coding Plan)"), - ("coding-cn", "https://open.bigmodel.cn/api/coding/paas/v4", ["glm-5.1", "glm-5v-turbo", "glm-4.7"], "China (Coding Plan)"), -] - - -def detect_zai_endpoint(api_key: str, timeout: float = 8.0) -> Optional[Dict[str, str]]: - """Probe z.ai endpoints to find one that accepts this API key. - - Returns {"id": ..., "base_url": ..., "model": ..., "label": ...} for the - first working endpoint, or None if all fail. For endpoints with multiple - candidate models, tries each in order and returns the first that succeeds. - """ - for ep_id, base_url, probe_models, label in ZAI_ENDPOINTS: - for model in probe_models: - try: - resp = httpx.post( - f"{base_url}/chat/completions", - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - }, - json={ - "model": model, - "stream": False, - "max_tokens": 1, - "messages": [{"role": "user", "content": "ping"}], - }, - timeout=timeout, - ) - if resp.status_code == 200: - logger.debug("Z.AI endpoint probe: %s (%s) model=%s OK", ep_id, base_url, model) - return { - "id": ep_id, - "base_url": base_url, - "model": model, - "label": label, - } - logger.debug("Z.AI endpoint probe: %s model=%s returned %s", ep_id, model, resp.status_code) - except Exception as exc: - logger.debug("Z.AI endpoint probe: %s model=%s failed: %s", ep_id, model, exc) - return None - - -def _resolve_zai_base_url(api_key: str, default_url: str, env_override: str) -> str: - """Return the correct Z.AI base URL by probing endpoints. - - If the user has explicitly set GLM_BASE_URL, that always wins. - Otherwise, probe the candidate endpoints to find one that accepts the - key. The detected endpoint is cached in provider state (auth.json) keyed - on a hash of the API key so subsequent starts skip the probe. - """ - if env_override: - return env_override - - # No API key set → don't probe (would fire N×M HTTPS requests with an - # empty Bearer token, all returning 401). This path is hit during - # auxiliary-client auto-detection when the user has no Z.AI credentials - # at all — the caller discards the result immediately, so the probe is - # pure latency for every AIAgent construction. - if not api_key: - return default_url - - # Check provider-state cache for a previously-detected endpoint. - auth_store = _load_auth_store() - state = _load_provider_state(auth_store, "zai") or {} - cached = state.get("detected_endpoint") - if isinstance(cached, dict) and cached.get("base_url"): - key_hash = cached.get("key_hash", "") - if key_hash == hashlib.sha256(api_key.encode()).hexdigest()[:16]: - logger.debug("Z.AI: using cached endpoint %s", cached["base_url"]) - return cached["base_url"] - - # Probe — may take up to ~8s per endpoint. - detected = detect_zai_endpoint(api_key) - if detected and detected.get("base_url"): - # Persist the detection result keyed on the API key hash. - key_hash = hashlib.sha256(api_key.encode()).hexdigest()[:16] - state["detected_endpoint"] = { - "base_url": detected["base_url"], - "endpoint_id": detected.get("id", ""), - "model": detected.get("model", ""), - "label": detected.get("label", ""), - "key_hash": key_hash, - } - _save_provider_state(auth_store, "zai", state) - logger.info("Z.AI: auto-detected endpoint %s (%s)", detected["label"], detected["base_url"]) - return detected["base_url"] - - logger.debug("Z.AI: probe failed, falling back to default %s", default_url) - return default_url - - # ============================================================================= # Error Types # ============================================================================= @@ -987,7 +909,8 @@ def resolve_provider( # Normalize provider aliases _PROVIDER_ALIASES = { - "glm": "zai", "z-ai": "zai", "z.ai": "zai", "zhipu": "zai", + "z-ai": "zai", "z.ai": "zai", "zhipu": "zai-cn", "glm": "zai-cn", + "glm-coding-cn": "zai-coding-cn", "glm-coding-global": "zai-coding-global", "google": "gemini", "google-gemini": "gemini", "google-ai-studio": "gemini", "x-ai": "xai", "x.ai": "xai", "grok": "xai", "kimi": "kimi-coding", "kimi-for-coding": "kimi-coding", "moonshot": "kimi-coding", @@ -2641,8 +2564,6 @@ def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]: if provider_id in ("kimi-coding", "kimi-coding-cn"): base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url) - elif provider_id == "zai": - base_url = _resolve_zai_base_url(api_key, pconfig.inference_base_url, env_url) elif env_url: base_url = env_url.rstrip("/") else: diff --git a/hermes_cli/config.py b/hermes_cli/config.py index c87b9f5a93bb8..3e8535c7d5329 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -995,15 +995,15 @@ def _ensure_hermes_home_managed(home: Path): "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/", + "description": "Zhipu AI (open.bigmodel.cn) API key", + "prompt": "Zhipu AI API key", + "url": "https://open.bigmodel.cn/", "password": True, "category": "provider", "advanced": True, }, "ZAI_API_KEY": { - "description": "Z.AI API key (alias for GLM_API_KEY)", + "description": "Z.AI (api.z.ai) API key", "prompt": "Z.AI API key", "url": "https://z.ai/", "password": True, @@ -1011,7 +1011,7 @@ def _ensure_hermes_home_managed(home: Path): "advanced": True, }, "Z_AI_API_KEY": { - "description": "Z.AI API key (alias for GLM_API_KEY)", + "description": "Z.AI API key (alias for ZAI_API_KEY)", "prompt": "Z.AI API key", "url": "https://z.ai/", "password": True, @@ -1019,8 +1019,16 @@ def _ensure_hermes_home_managed(home: Path): "advanced": True, }, "GLM_BASE_URL": { - "description": "Z.AI / GLM base URL override", - "prompt": "Z.AI / GLM base URL (leave empty for default)", + "description": "Zhipu AI base URL override", + "prompt": "Zhipu AI base URL (leave empty for default)", + "url": None, + "password": False, + "category": "provider", + "advanced": True, + }, + "ZAI_BASE_URL": { + "description": "Z.AI Global base URL override", + "prompt": "Z.AI Global base URL (leave empty for default)", "url": None, "password": False, "category": "provider", @@ -3043,14 +3051,17 @@ def load_config() -> Dict[str, Any]: # overload (529), service errors (503), or connection failures. # # Supported providers: -# openrouter (OPENROUTER_API_KEY) — routes to any model -# openai-codex (OAuth — hermes auth) — OpenAI Codex -# nous (OAuth — hermes auth) — Nous Portal -# zai (ZAI_API_KEY) — Z.AI / GLM -# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot -# kimi-coding-cn (KIMI_CN_API_KEY) — Kimi / Moonshot (China) -# minimax (MINIMAX_API_KEY) — MiniMax -# minimax-cn (MINIMAX_CN_API_KEY) — MiniMax (China) +# openrouter (OPENROUTER_API_KEY) — routes to any model +# openai-codex (OAuth — hermes auth) — OpenAI Codex +# nous (OAuth — hermes auth) — Nous Portal +# zai (ZAI_API_KEY) — Z.AI — api.z.ai +# zai-cn (GLM_API_KEY) — Zhipu AI — open.bigmodel.cn +# zai-coding-global (ZAI_CODING_API_KEY) — Z.AI Coding Plan +# zai-coding-cn (GLM_CODING_API_KEY) — Zhipu AI Coding Plan +# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot +# kimi-coding-cn (KIMI_CN_API_KEY) — Kimi / Moonshot (China) +# minimax (MINIMAX_API_KEY) — MiniMax +# minimax-cn (MINIMAX_CN_API_KEY) — MiniMax (China) # # For custom OpenAI-compatible endpoints, add base_url and key_env. # @@ -3074,14 +3085,17 @@ def load_config() -> Dict[str, Any]: # overload (529), service errors (503), or connection failures. # # Supported providers: -# openrouter (OPENROUTER_API_KEY) — routes to any model -# openai-codex (OAuth — hermes auth) — OpenAI Codex -# nous (OAuth — hermes auth) — Nous Portal -# zai (ZAI_API_KEY) — Z.AI / GLM -# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot -# kimi-coding-cn (KIMI_CN_API_KEY) — Kimi / Moonshot (China) -# minimax (MINIMAX_API_KEY) — MiniMax -# minimax-cn (MINIMAX_CN_API_KEY) — MiniMax (China) +# openrouter (OPENROUTER_API_KEY) — routes to any model +# openai-codex (OAuth — hermes auth) — OpenAI Codex +# nous (OAuth — hermes auth) — Nous Portal +# zai (ZAI_API_KEY) — Z.AI — api.z.ai +# zai-cn (GLM_API_KEY) — Zhipu AI — open.bigmodel.cn +# zai-coding-global (ZAI_CODING_API_KEY) — Z.AI Coding Plan +# zai-coding-cn (GLM_CODING_API_KEY) — Zhipu AI Coding Plan +# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot +# kimi-coding-cn (KIMI_CN_API_KEY) — Kimi / Moonshot (China) +# minimax (MINIMAX_API_KEY) — MiniMax +# minimax-cn (MINIMAX_CN_API_KEY) — MiniMax (China) # # For custom OpenAI-compatible endpoints, add base_url and key_env. # diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 2fc50321f68a6..708a40d9aec7b 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -41,8 +41,14 @@ "OPENAI_BASE_URL", "NOUS_API_KEY", "GLM_API_KEY", + "GLM_BASE_URL", "ZAI_API_KEY", "Z_AI_API_KEY", + "ZAI_BASE_URL", + "GLM_CODING_API_KEY", + "GLM_CODING_BASE_URL", + "ZAI_CODING_API_KEY", + "ZAI_CODING_BASE_URL", "KIMI_API_KEY", "KIMI_CN_API_KEY", "MINIMAX_API_KEY", @@ -910,7 +916,10 @@ def run_doctor(args): # Tuple: (name, env_vars, default_url, base_env, supports_models_endpoint) # 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", ("ZAI_API_KEY", "Z_AI_API_KEY"), "https://api.z.ai/api/paas/v4/models", "ZAI_BASE_URL", True), + ("Zhipu AI", ("GLM_API_KEY",), "https://open.bigmodel.cn/api/paas/v4/models", "GLM_BASE_URL", True), + ("Z.AI Coding Plan", ("ZAI_CODING_API_KEY",), "https://api.z.ai/api/coding/paas/v4/models", "ZAI_CODING_BASE_URL", True), + ("Zhipu AI Coding Plan", ("GLM_CODING_API_KEY",), "https://open.bigmodel.cn/api/coding/paas/v4/models", "GLM_CODING_BASE_URL", True), ("Kimi / Moonshot", ("KIMI_API_KEY",), "https://api.moonshot.ai/v1/models", "KIMI_BASE_URL", True), ("Kimi / Moonshot (China)", ("KIMI_CN_API_KEY",), "https://api.moonshot.cn/v1/models", None, True), ("Arcee AI", ("ARCEEAI_API_KEY",), "https://api.arcee.ai/api/v1/models", "ARCEE_BASE_URL", True), @@ -934,7 +943,7 @@ def run_doctor(args): if _key: break if _key: - _label = _pname.ljust(20) + _label = _pname.ljust(24) # Some providers (like MiniMax) don't support /models endpoint if not _supports_health_check: print(f" {color('✓', Colors.GREEN)} {_label} {color('(key configured)', Colors.DIM)}") diff --git a/hermes_cli/dump.py b/hermes_cli/dump.py index 90364a261ac36..89525a81833c0 100644 --- a/hermes_cli/dump.py +++ b/hermes_cli/dump.py @@ -267,8 +267,11 @@ def run_dump(args): ("ANTHROPIC_API_KEY", "anthropic"), ("ANTHROPIC_TOKEN", "anthropic_token"), ("NOUS_API_KEY", "nous"), - ("GLM_API_KEY", "glm/zai"), + ("GLM_API_KEY", "zai-cn"), ("ZAI_API_KEY", "zai"), + ("Z_AI_API_KEY", "zai"), + ("GLM_CODING_API_KEY", "zai-coding-cn"), + ("ZAI_CODING_API_KEY", "zai-coding-global"), ("KIMI_API_KEY", "kimi"), ("MINIMAX_API_KEY", "minimax"), ("DEEPSEEK_API_KEY", "deepseek"), diff --git a/hermes_cli/main.py b/hermes_cli/main.py index fe2fdd378b19d..4ab492d092eb0 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1572,7 +1572,7 @@ def _named_custom_provider_map(cfg) -> dict[str, dict[str, str]]: "gemini", "deepseek", "xai", - "zai", + "zai", "zai-cn", "zai-coding-cn", "zai-coding-global", "kimi-coding-cn", "minimax", "minimax-cn", @@ -6528,7 +6528,7 @@ def main(): "ollama-cloud", "huggingface", "zai", - "kimi-coding", + "zai-cn", "zai-coding-cn", "zai-coding-global", "kimi-coding", "kimi-coding-cn", "minimax", "minimax-cn", diff --git a/hermes_cli/model_normalize.py b/hermes_cli/model_normalize.py index 76dace065a3aa..2fc70700e998c 100644 --- a/hermes_cli/model_normalize.py +++ b/hermes_cli/model_normalize.py @@ -88,6 +88,9 @@ # provider/ prefix when users copy the aggregator form into config.yaml. _MATCHING_PREFIX_STRIP_PROVIDERS: frozenset[str] = frozenset({ "zai", + "zai-cn", + "zai-coding-cn", + "zai-coding-global", "kimi-coding", "kimi-coding-cn", "minimax", diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 186119b24d0db..8800cd4f42c52 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -173,8 +173,33 @@ def _codex_curated_models() -> list[str]: "glm-5v-turbo", "glm-5-turbo", "glm-4.7", + "glm-4.7-flashx", + "glm-4.6", + "glm-4.5", + "glm-4.5-air", + ], + "zai-cn": [ + "glm-5.1", + "glm-5", + "glm-5v-turbo", + "glm-5-turbo", + "glm-4.7", + "glm-4.7-flashx", + "glm-4.6", "glm-4.5", - "glm-4.5-flash", + "glm-4.5-air", + ], + "zai-coding-cn": [ + "glm-5.1", + "glm-5-turbo", + "glm-4.7", + "glm-4.5-air", + ], + "zai-coding-global": [ + "glm-5.1", + "glm-5-turbo", + "glm-4.7", + "glm-4.5-air", ], "xai": [ "grok-4.20-reasoning", @@ -696,7 +721,10 @@ class ProviderEntry(NamedTuple): ProviderEntry("google-gemini-cli", "Google Gemini (OAuth)", "Google Gemini via OAuth + Code Assist (free tier supported; no API key needed)"), ProviderEntry("deepseek", "DeepSeek", "DeepSeek (DeepSeek-V3, R1, coder — direct API)"), ProviderEntry("xai", "xAI", "xAI (Grok models — direct API)"), - ProviderEntry("zai", "Z.AI / GLM", "Z.AI / GLM (Zhipu AI direct API)"), + ProviderEntry("zai", "Z.AI", "Z.AI (Global) — api.z.ai"), + ProviderEntry("zai-cn", "Zhipu AI", "Zhipu AI (China) — open.bigmodel.cn"), + ProviderEntry("zai-coding-global", "Z.AI Coding Plan", "Z.AI Coding Plan (Global) — api.z.ai"), + ProviderEntry("zai-coding-cn", "Zhipu AI Coding Plan", "Zhipu AI Coding Plan (China) — open.bigmodel.cn"), ProviderEntry("kimi-coding", "Kimi / Kimi Coding Plan", "Kimi Coding Plan (api.kimi.com) & Moonshot API"), ProviderEntry("kimi-coding-cn", "Kimi / Moonshot (China)", "Kimi / Moonshot China (Moonshot CN direct API)"), ProviderEntry("minimax", "MiniMax", "MiniMax (global direct API)"), @@ -716,10 +744,10 @@ class ProviderEntry(NamedTuple): _PROVIDER_ALIASES = { - "glm": "zai", "z-ai": "zai", "z.ai": "zai", - "zhipu": "zai", + "zhipu": "zai-cn", + "glm": "zai-cn", "github": "copilot", "github-copilot": "copilot", "github-models": "copilot", diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index 00c3f64bcf9e8..760a72bbd0f0b 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -87,9 +87,27 @@ class HermesOverlay: ), "zai": HermesOverlay( transport="openai_chat", - extra_env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"), + base_url_override="https://api.z.ai/api/paas/v4", + # GLM_API_KEY is a legacy fallback — see PROVIDER_REGISTRY["zai"]. + extra_env_vars=("ZAI_API_KEY", "Z_AI_API_KEY", "GLM_API_KEY"), + base_url_env_var="ZAI_BASE_URL", + ), + "zai-cn": HermesOverlay( + transport="openai_chat", + base_url_override="https://open.bigmodel.cn/api/paas/v4", + extra_env_vars=("GLM_API_KEY",), base_url_env_var="GLM_BASE_URL", ), + "zai-coding-cn": HermesOverlay( + transport="openai_chat", + extra_env_vars=("GLM_CODING_API_KEY",), + base_url_env_var="GLM_CODING_BASE_URL", + ), + "zai-coding-global": HermesOverlay( + transport="openai_chat", + extra_env_vars=("ZAI_CODING_API_KEY",), + base_url_env_var="ZAI_CODING_BASE_URL", + ), "kimi-for-coding": HermesOverlay( transport="openai_chat", base_url_env_var="KIMI_BASE_URL", @@ -187,11 +205,13 @@ class ProviderDef: # openrouter "openai": "openrouter", # bare "openai" → route through aggregator - # zai - "glm": "zai", + # zai (Z.AI, api.z.ai) "z-ai": "zai", "z.ai": "zai", - "zhipu": "zai", + + # zai-cn (Zhipu AI, open.bigmodel.cn) + "zhipu": "zai-cn", + "glm": "zai-cn", # xai "x-ai": "xai", diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 1a620d62b3079..f59ed3eba7ac5 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -93,7 +93,10 @@ def _supports_same_provider_pool_setup(provider: str) -> bool: "gemini-3.1-pro-preview", "gemini-3-pro-preview", "gemini-3-flash-preview", "gemini-3.1-flash-lite-preview", ], - "zai": ["glm-5.1", "glm-5", "glm-4.7", "glm-4.5", "glm-4.5-flash"], + "zai": ["glm-5.1", "glm-5", "glm-5v-turbo", "glm-5-turbo", "glm-4.7", "glm-4.7-flashx", "glm-4.6", "glm-4.5", "glm-4.5-air"], + "zai-cn": ["glm-5.1", "glm-5", "glm-5v-turbo", "glm-5-turbo", "glm-4.7", "glm-4.7-flashx", "glm-4.6", "glm-4.5", "glm-4.5-air"], + "zai-coding-cn": ["glm-5.1", "glm-5-turbo", "glm-4.7", "glm-4.5-air"], + "zai-coding-global": ["glm-5.1", "glm-5-turbo", "glm-4.7", "glm-4.5-air"], "kimi-coding": ["kimi-k2.6", "kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"], "kimi-coding-cn": ["kimi-k2.6", "kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"], "arcee": ["trinity-large-thinking", "trinity-large-preview", "trinity-mini"], @@ -801,7 +804,10 @@ def setup_model_provider(config: dict, *, quick: bool = False): "nous-api": "Nous Portal API key", "copilot": "GitHub Copilot", "copilot-acp": "GitHub Copilot ACP", - "zai": "Z.AI / GLM", + "zai": "Z.AI", + "zai-cn": "Zhipu AI", + "zai-coding-global": "Z.AI Coding Plan", + "zai-coding-cn": "Zhipu AI Coding Plan", "kimi-coding": "Kimi / Moonshot", "kimi-coding-cn": "Kimi / Moonshot (China)", "minimax": "MiniMax", diff --git a/hermes_cli/status.py b/hermes_cli/status.py index 540afc30375d8..2d283d8c5c8e2 100644 --- a/hermes_cli/status.py +++ b/hermes_cli/status.py @@ -120,7 +120,10 @@ def show_status(args): keys = { "OpenRouter": "OPENROUTER_API_KEY", "OpenAI": "OPENAI_API_KEY", - "Z.AI/GLM": "GLM_API_KEY", + "Z.AI": "ZAI_API_KEY", + "Zhipu AI": "GLM_API_KEY", + "Z.AI Coding Plan": "ZAI_CODING_API_KEY", + "Zhipu AI Coding Plan": "GLM_CODING_API_KEY", "Kimi": "KIMI_API_KEY", "MiniMax": "MINIMAX_API_KEY", "MiniMax-CN": "MINIMAX_CN_API_KEY", @@ -139,12 +142,12 @@ def show_status(args): value = get_env_value(env_var) or "" has_key = bool(value) display = redact_key(value) if not show_all else value - print(f" {name:<12} {check_mark(has_key)} {display}") + print(f" {name:<20} {check_mark(has_key)} {display}") from hermes_cli.auth import get_anthropic_key anthropic_value = get_anthropic_key() anthropic_display = redact_key(anthropic_value) if not show_all else anthropic_value - print(f" {'Anthropic':<12} {check_mark(bool(anthropic_value))} {anthropic_display}") + print(f" {'Anthropic':<20} {check_mark(bool(anthropic_value))} {anthropic_display}") # ========================================================================= # Auth Providers (OAuth) @@ -250,10 +253,13 @@ def show_status(args): 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",), + "Z.AI": ("ZAI_API_KEY", "Z_AI_API_KEY"), + "Zhipu AI": ("GLM_API_KEY",), + "Z.AI Coding Plan": ("ZAI_CODING_API_KEY",), + "Zhipu AI Coding Plan": ("GLM_CODING_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 = "" diff --git a/run_agent.py b/run_agent.py index b88baf2faa948..1bb7814cb0362 100644 --- a/run_agent.py +++ b/run_agent.py @@ -6625,7 +6625,7 @@ def _anthropic_preserve_dots(self) -> bool: if (getattr(self, "provider", "") or "").lower() in { "alibaba", "minimax", "minimax-cn", "opencode-go", "opencode-zen", - "zai", "bedrock", + "zai", "zai-cn", "zai-coding-cn", "zai-coding-global", "bedrock", }: return True base = (getattr(self, "base_url", "") or "").lower() diff --git a/tests/agent/test_auxiliary_client.py b/tests/agent/test_auxiliary_client.py index 4c775b8a6c359..4ffc300794786 100644 --- a/tests/agent/test_auxiliary_client.py +++ b/tests/agent/test_auxiliary_client.py @@ -447,6 +447,77 @@ def test_explicit_anthropic_api_key(self, monkeypatch): adapter = client.chat.completions assert adapter._is_oauth is False + def test_explicit_openrouter(self, monkeypatch): + """provider='openrouter' should use OPENROUTER_API_KEY.""" + monkeypatch.setenv("OPENROUTER_API_KEY", "or-explicit") + with patch("agent.auxiliary_client.OpenAI") as mock_openai: + mock_openai.return_value = MagicMock() + client, model = resolve_provider_client("openrouter") + assert client is not None + + def test_explicit_kimi(self, monkeypatch): + """provider='kimi-coding' should use KIMI_API_KEY.""" + monkeypatch.setenv("KIMI_API_KEY", "kimi-test-key") + with patch("agent.auxiliary_client.OpenAI") as mock_openai: + mock_openai.return_value = MagicMock() + client, model = resolve_provider_client("kimi-coding") + assert client is not None + + def test_explicit_minimax(self, monkeypatch): + """provider='minimax' should use MINIMAX_API_KEY.""" + monkeypatch.setenv("MINIMAX_API_KEY", "mm-test-key") + with patch("agent.auxiliary_client.OpenAI") as mock_openai: + mock_openai.return_value = MagicMock() + client, model = resolve_provider_client("minimax") + assert client is not None + + def test_explicit_deepseek(self, monkeypatch): + """provider='deepseek' should use DEEPSEEK_API_KEY.""" + monkeypatch.setenv("DEEPSEEK_API_KEY", "ds-test-key") + with patch("agent.auxiliary_client.OpenAI") as mock_openai: + mock_openai.return_value = MagicMock() + client, model = resolve_provider_client("deepseek") + assert client is not None + + def test_explicit_zai(self, monkeypatch): + """provider='zai' should use ZAI_API_KEY.""" + monkeypatch.setenv("ZAI_API_KEY", "zai-test-key") + with patch("agent.auxiliary_client.OpenAI") as mock_openai: + mock_openai.return_value = MagicMock() + client, model = resolve_provider_client("zai") + assert client is not None + + def test_explicit_zai_cn(self, monkeypatch): + """provider='zai-cn' should use GLM_API_KEY.""" + monkeypatch.setenv("GLM_API_KEY", "glm-test-key") + with patch("agent.auxiliary_client.OpenAI") as mock_openai: + mock_openai.return_value = MagicMock() + client, model = resolve_provider_client("zai-cn") + assert client is not None + + def test_explicit_google_alias_uses_gemini_credentials(self): + """provider='google' should route through the gemini API-key provider.""" + with ( + patch("hermes_cli.auth.resolve_api_key_provider_credentials", return_value={ + "api_key": "gemini-key", + "base_url": "https://generativelanguage.googleapis.com/v1beta/openai", + }), + patch("agent.auxiliary_client.OpenAI") as mock_openai, + ): + mock_openai.return_value = MagicMock() + client, model = resolve_provider_client("google", model="gemini-3.1-pro-preview") + + assert client is not None + assert model == "gemini-3.1-pro-preview" + assert mock_openai.call_args.kwargs["api_key"] == "gemini-key" + assert mock_openai.call_args.kwargs["base_url"] == "https://generativelanguage.googleapis.com/v1beta/openai" + + def test_explicit_unknown_returns_none(self, monkeypatch): + """Unknown provider should return None.""" + client, model = resolve_provider_client("nonexistent-provider") + assert client is None + + class TestGetTextAuxiliaryClient: """Test the full resolution chain for get_text_auxiliary_client.""" diff --git a/tests/hermes_cli/test_api_key_providers.py b/tests/hermes_cli/test_api_key_providers.py index 7d0674b038583..d8f5c2fbf7f59 100644 --- a/tests/hermes_cli/test_api_key_providers.py +++ b/tests/hermes_cli/test_api_key_providers.py @@ -31,7 +31,10 @@ class TestProviderRegistry: ("copilot-acp", "GitHub Copilot ACP", "external_process"), ("copilot", "GitHub Copilot", "api_key"), ("huggingface", "Hugging Face", "api_key"), - ("zai", "Z.AI / GLM", "api_key"), + ("zai", "Z.AI", "api_key"), + ("zai-cn", "Zhipu AI", "api_key"), + ("zai-coding-cn", "Zhipu AI Coding Plan", "api_key"), + ("zai-coding-global", "Z.AI Coding Plan", "api_key"), ("xai", "xAI", "api_key"), ("nvidia", "NVIDIA NIM", "api_key"), ("kimi-coding", "Kimi / Moonshot", "api_key"), @@ -49,8 +52,15 @@ def test_provider_registered(self, provider_id, name, auth_type): 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") + # GLM_API_KEY is a legacy fallback kept to avoid breaking upgrades. + assert pconfig.api_key_env_vars == ("ZAI_API_KEY", "Z_AI_API_KEY", "GLM_API_KEY") + assert pconfig.base_url_env_var == "ZAI_BASE_URL" + + def test_zai_cn_env_vars(self): + pconfig = PROVIDER_REGISTRY["zai-cn"] + assert pconfig.api_key_env_vars == ("GLM_API_KEY",) assert pconfig.base_url_env_var == "GLM_BASE_URL" + assert pconfig.inference_base_url == "https://open.bigmodel.cn/api/paas/v4" def test_xai_env_vars(self): pconfig = PROVIDER_REGISTRY["xai"] @@ -129,8 +139,9 @@ def test_oauth_providers_unchanged(self): PROVIDER_ENV_VARS = ( "OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", - "GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY", - "KIMI_API_KEY", "KIMI_BASE_URL", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", + "GLM_API_KEY", "GLM_BASE_URL", "ZAI_API_KEY", "Z_AI_API_KEY", "ZAI_BASE_URL", + "GLM_CODING_API_KEY", "GLM_CODING_BASE_URL", "ZAI_CODING_API_KEY", "ZAI_CODING_BASE_URL", + "KIMI_API_KEY", "KIMI_CN_API_KEY", "KIMI_BASE_URL", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", "AI_GATEWAY_API_KEY", "AI_GATEWAY_BASE_URL", "KILOCODE_API_KEY", "KILOCODE_BASE_URL", "DASHSCOPE_API_KEY", "OPENCODE_ZEN_API_KEY", "OPENCODE_GO_API_KEY", @@ -165,14 +176,17 @@ def test_explicit_minimax_cn(self): def test_explicit_ai_gateway(self): assert resolve_provider("ai-gateway") == "ai-gateway" - def test_alias_glm(self): - assert resolve_provider("glm") == "zai" + def test_explicit_zai_cn(self): + assert resolve_provider("zai-cn") == "zai-cn" def test_alias_z_ai(self): assert resolve_provider("z-ai") == "zai" def test_alias_zhipu(self): - assert resolve_provider("zhipu") == "zai" + assert resolve_provider("zhipu") == "zai-cn" + + def test_alias_glm(self): + assert resolve_provider("glm") == "zai-cn" def test_alias_kimi(self): assert resolve_provider("kimi") == "kimi-coding" @@ -202,7 +216,7 @@ def test_alias_kilo_gateway(self): assert resolve_provider("kilo-gateway") == "kilocode" def test_alias_case_insensitive(self): - assert resolve_provider("GLM") == "zai" + assert resolve_provider("GLM") == "zai-cn" assert resolve_provider("Z-AI") == "zai" assert resolve_provider("Kimi") == "kimi-coding" @@ -234,7 +248,7 @@ def test_unknown_provider_raises(self): def test_auto_detects_glm_key(self, monkeypatch): monkeypatch.setenv("GLM_API_KEY", "test-glm-key") - assert resolve_provider("auto") == "zai" + assert resolve_provider("auto") == "zai-cn" def test_auto_detects_zai_key(self, monkeypatch): monkeypatch.setenv("ZAI_API_KEY", "test-zai-key") @@ -274,6 +288,12 @@ def test_openrouter_takes_priority_over_glm(self, monkeypatch): monkeypatch.setenv("GLM_API_KEY", "glm-key") assert resolve_provider("auto") == "openrouter" + def test_openrouter_takes_priority_over_zai(self, monkeypatch): + """OpenRouter API key should win over ZAI in auto-detection.""" + monkeypatch.setenv("OPENROUTER_API_KEY", "or-key") + monkeypatch.setenv("ZAI_API_KEY", "zai-key") + assert resolve_provider("auto") == "openrouter" + def test_auto_does_not_select_copilot_from_github_token(self, monkeypatch): monkeypatch.setenv("GITHUB_TOKEN", "gh-test-token") with pytest.raises(AuthError, match="No inference provider configured"): @@ -292,19 +312,27 @@ def test_unconfigured_provider(self): assert status["logged_in"] is False def test_configured_provider(self, monkeypatch): - monkeypatch.setenv("GLM_API_KEY", "test-key-123") + monkeypatch.setenv("ZAI_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 status["key_source"] == "ZAI_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") + """Z_AI_API_KEY should work when ZAI_API_KEY is not set.""" + monkeypatch.setenv("Z_AI_API_KEY", "z-ai-fallback-key") status = get_api_key_provider_status("zai") assert status["configured"] is True - assert status["key_source"] == "ZAI_API_KEY" + assert status["key_source"] == "Z_AI_API_KEY" + + def test_zai_cn_configured_provider(self, monkeypatch): + monkeypatch.setenv("GLM_API_KEY", "glm-test-key-123") + status = get_api_key_provider_status("zai-cn") + assert status["configured"] is True + assert status["logged_in"] is True + assert status["key_source"] == "GLM_API_KEY" + assert "bigmodel.cn" in status["base_url"].lower() def test_custom_base_url(self, monkeypatch): monkeypatch.setenv("KIMI_API_KEY", "kimi-key") @@ -359,14 +387,43 @@ def test_non_api_key_provider(self): class TestResolveApiKeyProviderCredentials: def test_resolve_zai_with_key(self, monkeypatch): - monkeypatch.setenv("GLM_API_KEY", "glm-secret-key") - monkeypatch.setattr("hermes_cli.auth.detect_zai_endpoint", lambda *a, **kw: None) + monkeypatch.setenv("ZAI_API_KEY", "zai-secret-key") creds = resolve_api_key_provider_credentials("zai") assert creds["provider"] == "zai" - assert creds["api_key"] == "glm-secret-key" + assert creds["api_key"] == "zai-secret-key" assert creds["base_url"] == "https://api.z.ai/api/paas/v4" + assert creds["source"] == "ZAI_API_KEY" + + def test_resolve_zai_cn_with_key(self, monkeypatch): + monkeypatch.setenv("GLM_API_KEY", "glm-secret-key") + creds = resolve_api_key_provider_credentials("zai-cn") + assert creds["provider"] == "zai-cn" + assert creds["api_key"] == "glm-secret-key" + assert creds["base_url"] == "https://open.bigmodel.cn/api/paas/v4" assert creds["source"] == "GLM_API_KEY" + def test_legacy_zai_falls_back_to_glm_key(self, monkeypatch): + """Pre-refactor configs (provider: zai + GLM_API_KEY) must not break. + + The `zai` provider still routes to api.z.ai; the endpoint will 401 + if the GLM key only works against open.bigmodel.cn, but at least + credential resolution no longer returns empty. + """ + monkeypatch.setenv("GLM_API_KEY", "legacy-glm-key") + creds = resolve_api_key_provider_credentials("zai") + assert creds["provider"] == "zai" + assert creds["api_key"] == "legacy-glm-key" + assert creds["source"] == "GLM_API_KEY" + assert creds["base_url"] == "https://api.z.ai/api/paas/v4" + + def test_zai_prefers_zai_key_over_glm(self, monkeypatch): + """When both keys are present, ZAI_API_KEY wins over GLM_API_KEY.""" + monkeypatch.setenv("ZAI_API_KEY", "native-zai") + monkeypatch.setenv("GLM_API_KEY", "legacy-glm") + creds = resolve_api_key_provider_credentials("zai") + assert creds["api_key"] == "native-zai" + assert creds["source"] == "ZAI_API_KEY" + def test_resolve_copilot_with_github_token(self, monkeypatch): monkeypatch.setenv("GITHUB_TOKEN", "gh-env-secret") creds = resolve_api_key_provider_credentials("copilot") @@ -464,9 +521,15 @@ def test_resolve_kilocode_custom_base_url(self, monkeypatch): assert creds["base_url"] == "https://custom.kilo.example/v1" def test_resolve_with_custom_base_url(self, monkeypatch): + monkeypatch.setenv("ZAI_API_KEY", "zai-key") + monkeypatch.setenv("ZAI_BASE_URL", "https://custom.zai.example/v4") + creds = resolve_api_key_provider_credentials("zai") + assert creds["base_url"] == "https://custom.zai.example/v4" + + def test_resolve_zai_cn_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") + creds = resolve_api_key_provider_credentials("zai-cn") assert creds["base_url"] == "https://custom.glm.example/v4" def test_resolve_without_key_returns_empty(self): @@ -478,22 +541,20 @@ 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") - monkeypatch.setattr("hermes_cli.auth.detect_zai_endpoint", lambda *a, **kw: None) + def test_zai_key_priority(self, monkeypatch): + """ZAI_API_KEY takes priority over Z_AI_API_KEY.""" + monkeypatch.setenv("ZAI_API_KEY", "primary") + monkeypatch.setenv("Z_AI_API_KEY", "secondary") creds = resolve_api_key_provider_credentials("zai") assert creds["api_key"] == "primary" - assert creds["source"] == "GLM_API_KEY" + assert creds["source"] == "ZAI_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") - monkeypatch.setattr("hermes_cli.auth.detect_zai_endpoint", lambda *a, **kw: None) + def test_z_ai_key_fallback(self, monkeypatch): + """Z_AI_API_KEY used when ZAI_API_KEY not set.""" + monkeypatch.setenv("Z_AI_API_KEY", "secondary") creds = resolve_api_key_provider_credentials("zai") assert creds["api_key"] == "secondary" - assert creds["source"] == "ZAI_API_KEY" + assert creds["source"] == "Z_AI_API_KEY" # ============================================================================= @@ -503,14 +564,23 @@ def test_zai_key_fallback(self, monkeypatch): class TestRuntimeProviderResolution: def test_runtime_zai(self, monkeypatch): - monkeypatch.setenv("GLM_API_KEY", "glm-key") + monkeypatch.setenv("ZAI_API_KEY", "zai-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 result["api_key"] == "zai-key" assert "z.ai" in result["base_url"] or "api.z.ai" in result["base_url"] + def test_runtime_zai_cn(self, monkeypatch): + monkeypatch.setenv("GLM_API_KEY", "glm-key") + from hermes_cli.runtime_provider import resolve_runtime_provider + result = resolve_runtime_provider(requested="zai-cn") + assert result["provider"] == "zai-cn" + assert result["api_mode"] == "chat_completions" + assert result["api_key"] == "glm-key" + assert "bigmodel.cn" 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 @@ -519,6 +589,15 @@ def test_runtime_kimi(self, monkeypatch): assert result["api_mode"] == "chat_completions" assert result["api_key"] == "kimi-key" + def test_runtime_kimi_cn(self, monkeypatch): + monkeypatch.setenv("KIMI_CN_API_KEY", "kimi-cn-key") + from hermes_cli.runtime_provider import resolve_runtime_provider + result = resolve_runtime_provider(requested="kimi-coding-cn") + assert result["provider"] == "kimi-coding-cn" + assert result["api_mode"] == "chat_completions" + assert result["api_key"] == "kimi-cn-key" + assert "moonshot.cn" in result["base_url"] + def test_runtime_minimax(self, monkeypatch): monkeypatch.setenv("MINIMAX_API_KEY", "mm-key") from hermes_cli.runtime_provider import resolve_runtime_provider @@ -858,58 +937,114 @@ def test_env_override_wins(self, monkeypatch): assert creds["base_url"] == "https://override.example/v1" def test_non_kimi_providers_unaffected(self, monkeypatch): - """Ensure the auto-detect logic doesn't leak to other providers.""" - monkeypatch.setenv("GLM_API_KEY", "sk-kim...isnt") - monkeypatch.setattr("hermes_cli.auth.detect_zai_endpoint", lambda *a, **kw: None) + """Ensure the Kimi auto-detect logic doesn't leak to other providers.""" + monkeypatch.setenv("ZAI_API_KEY", "sk-kim...isnt") creds = resolve_api_key_provider_credentials("zai") assert creds["base_url"] == "https://api.z.ai/api/paas/v4" -class TestZaiEndpointAutoDetect: - """Test that resolve_api_key_provider_credentials auto-detects Z.AI endpoints.""" +class TestZaiAndZaiCnStaticEndpoints: + """Both zai and zai-cn now use static endpoints — no probing.""" - def test_probe_success_returns_detected_url(self, monkeypatch): - monkeypatch.setenv("GLM_API_KEY", "glm-coding-key") - monkeypatch.setattr( - "hermes_cli.auth.detect_zai_endpoint", - lambda *a, **kw: { - "id": "coding-global", - "base_url": "https://api.z.ai/api/coding/paas/v4", - "model": "glm-4.7", - "label": "Global (Coding Plan)", - }, - ) + def test_zai_always_global(self, monkeypatch): + """ZAI_API_KEY should always route to api.z.ai (Global).""" + monkeypatch.setenv("ZAI_API_KEY", "zai-key") creds = resolve_api_key_provider_credentials("zai") - assert creds["base_url"] == "https://api.z.ai/api/coding/paas/v4" + assert creds["base_url"] == "https://api.z.ai/api/paas/v4" - def test_probe_failure_falls_back_to_default(self, monkeypatch): + def test_zai_cn_always_china(self, monkeypatch): + """GLM_API_KEY should always route to open.bigmodel.cn (China).""" monkeypatch.setenv("GLM_API_KEY", "glm-key") - monkeypatch.setattr("hermes_cli.auth.detect_zai_endpoint", lambda *a, **kw: None) + creds = resolve_api_key_provider_credentials("zai-cn") + assert creds["base_url"] == "https://open.bigmodel.cn/api/paas/v4" + + def test_zai_env_override(self, monkeypatch): + """ZAI_BASE_URL overrides the default zai base URL.""" + monkeypatch.setenv("ZAI_API_KEY", "zai-key") + monkeypatch.setenv("ZAI_BASE_URL", "https://custom.zai.example/v4") creds = resolve_api_key_provider_credentials("zai") - assert creds["base_url"] == "https://api.z.ai/api/paas/v4" + assert creds["base_url"] == "https://custom.zai.example/v4" - def test_env_override_skips_probe(self, monkeypatch): - """GLM_BASE_URL should always win without probing.""" + def test_zai_cn_env_override(self, monkeypatch): + """GLM_BASE_URL overrides the default zai-cn base URL.""" monkeypatch.setenv("GLM_API_KEY", "glm-key") - monkeypatch.setenv("GLM_BASE_URL", "https://custom.example/v4") - probe_called = False - - def _never_called(*a, **kw): - nonlocal probe_called - probe_called = True - return None + monkeypatch.setenv("GLM_BASE_URL", "https://custom.glm.example/v4") + creds = resolve_api_key_provider_credentials("zai-cn") + assert creds["base_url"] == "https://custom.glm.example/v4" - monkeypatch.setattr("hermes_cli.auth.detect_zai_endpoint", _never_called) + def test_zai_no_key(self): creds = resolve_api_key_provider_credentials("zai") - assert creds["base_url"] == "https://custom.example/v4" - assert not probe_called + assert creds["api_key"] == "" + assert creds["base_url"] == "https://api.z.ai/api/paas/v4" - def test_no_key_skips_probe(self, monkeypatch): - """Without an API key, no probe should occur.""" - monkeypatch.setattr("hermes_cli.auth.detect_zai_endpoint", lambda *a, **kw: None) - creds = resolve_api_key_provider_credentials("zai") + def test_zai_cn_no_key(self): + creds = resolve_api_key_provider_credentials("zai-cn") assert creds["api_key"] == "" + assert creds["base_url"] == "https://open.bigmodel.cn/api/paas/v4" + + +# ============================================================================= +# Z.AI Coding Plan provider tests +# ============================================================================= + +class TestZaiCodingPlanProviders: + """Test that zai-coding-* providers use static endpoints (no probing).""" + + @pytest.mark.parametrize("provider_id,expected_base,key_env", [ + ("zai-coding-cn", "https://open.bigmodel.cn/api/coding/paas/v4", "GLM_CODING_API_KEY"), + ("zai-coding-global", "https://api.z.ai/api/coding/paas/v4", "ZAI_CODING_API_KEY"), + ]) + def test_static_base_url(self, provider_id, expected_base, key_env, monkeypatch): + monkeypatch.setenv(key_env, "test-key") + creds = resolve_api_key_provider_credentials(provider_id) + assert creds["base_url"] == expected_base + + @pytest.mark.parametrize("provider_id,key_env", [ + ("zai-coding-cn", "GLM_CODING_API_KEY"), + ("zai-coding-global", "ZAI_CODING_API_KEY"), + ]) + def test_uses_own_api_key(self, provider_id, key_env, monkeypatch): + monkeypatch.setenv(key_env, "coding-key") + creds = resolve_api_key_provider_credentials(provider_id) + assert creds["api_key"] == "coding-key" + assert creds["source"] == key_env + + def test_non_coding_env_var_does_not_leak(self, monkeypatch): + """GLM_BASE_URL (zai-cn's env) should NOT affect zai-coding-cn.""" + monkeypatch.setenv("GLM_CODING_API_KEY", "glm-key") + monkeypatch.setenv("GLM_BASE_URL", "https://custom.example/v4") + creds = resolve_api_key_provider_credentials("zai-coding-cn") + assert creds["base_url"] == "https://open.bigmodel.cn/api/coding/paas/v4" + + @pytest.mark.parametrize("provider_id,key_env,base_env", [ + ("zai-coding-cn", "GLM_CODING_API_KEY", "GLM_CODING_BASE_URL"), + ("zai-coding-global", "ZAI_CODING_API_KEY", "ZAI_CODING_BASE_URL"), + ]) + def test_own_base_url_env_overrides_default(self, provider_id, key_env, base_env, monkeypatch): + """Coding plan providers honor their own base_url env var when set.""" + monkeypatch.setenv(key_env, "coding-key") + monkeypatch.setenv(base_env, "https://proxy.example/coding/v4") + creds = resolve_api_key_provider_credentials(provider_id) + assert creds["base_url"] == "https://proxy.example/coding/v4" + + @pytest.mark.parametrize("provider_id,key_env,base_env", [ + ("zai-coding-cn", "GLM_CODING_API_KEY", "GLM_CODING_BASE_URL"), + ("zai-coding-global", "ZAI_CODING_API_KEY", "ZAI_CODING_BASE_URL"), + ]) + def test_provider_registered(self, provider_id, key_env, base_env): + assert provider_id in PROVIDER_REGISTRY + pconfig = PROVIDER_REGISTRY[provider_id] + assert pconfig.auth_type == "api_key" + assert pconfig.api_key_env_vars == (key_env,) + assert pconfig.base_url_env_var == base_env + + def test_resolve_provider_aliases(self): + assert resolve_provider("glm-coding-cn") == "zai-coding-cn" + assert resolve_provider("glm-coding-global") == "zai-coding-global" + def test_zai_coding_anthropic_not_registered(self): + """zai-coding-anthropic is no longer a separate provider — protocol is a sub-choice.""" + assert "zai-coding-anthropic" not in PROVIDER_REGISTRY # ============================================================================= # Kimi / Moonshot model list isolation tests diff --git a/tests/hermes_cli/test_model_provider_persistence.py b/tests/hermes_cli/test_model_provider_persistence.py index a06facd300ab4..a8c2731fe328d 100644 --- a/tests/hermes_cli/test_model_provider_persistence.py +++ b/tests/hermes_cli/test_model_provider_persistence.py @@ -266,25 +266,25 @@ def test_invalid_base_url_rejected(self, config_home, monkeypatch, capsys): """Typing a non-URL string should not be saved as the base URL.""" from hermes_cli.auth import PROVIDER_REGISTRY - pconfig = PROVIDER_REGISTRY.get("zai") + pconfig = PROVIDER_REGISTRY.get("kimi-coding") if not pconfig: - pytest.skip("zai not in PROVIDER_REGISTRY") + pytest.skip("kimi-coding not in PROVIDER_REGISTRY") - monkeypatch.setenv("GLM_API_KEY", "test-key") + monkeypatch.setenv("KIMI_API_KEY", "test-key") from hermes_cli.main import _model_flow_api_key_provider from hermes_cli.config import load_config, get_env_value # User types a shell command instead of a URL at the base URL prompt - with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + with patch("hermes_cli.auth._prompt_model_selection", return_value="kimi-k2.5"), \ patch("hermes_cli.auth.deactivate_provider"), \ patch("builtins.input", return_value="nano ~/.hermes/.env"): - _model_flow_api_key_provider(load_config(), "zai", "old-model") + _model_flow_api_key_provider(load_config(), "kimi-coding", "old-model") # The garbage value should NOT have been saved - saved = get_env_value("GLM_BASE_URL") or "" + saved = get_env_value("KIMI_BASE_URL") or "" assert not saved or saved.startswith(("http://", "https://")), \ - f"Non-URL value was saved as GLM_BASE_URL: {saved}" + f"Non-URL value was saved as KIMI_BASE_URL: {saved}" captured = capsys.readouterr() assert "Invalid URL" in captured.out @@ -292,41 +292,43 @@ def test_valid_base_url_accepted(self, config_home, monkeypatch): """A proper URL should be saved normally.""" from hermes_cli.auth import PROVIDER_REGISTRY - pconfig = PROVIDER_REGISTRY.get("zai") + pconfig = PROVIDER_REGISTRY.get("kimi-coding") if not pconfig: - pytest.skip("zai not in PROVIDER_REGISTRY") + pytest.skip("kimi-coding not in PROVIDER_REGISTRY") - monkeypatch.setenv("GLM_API_KEY", "test-key") + monkeypatch.setenv("KIMI_API_KEY", "test-key") from hermes_cli.main import _model_flow_api_key_provider from hermes_cli.config import load_config, get_env_value - with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + with patch("hermes_cli.auth._prompt_model_selection", return_value="kimi-k2.5"), \ patch("hermes_cli.auth.deactivate_provider"), \ - patch("builtins.input", return_value="https://custom.z.ai/api/paas/v4"): - _model_flow_api_key_provider(load_config(), "zai", "old-model") + patch("builtins.input", return_value="https://custom.moonshot.ai/v1"): + _model_flow_api_key_provider(load_config(), "kimi-coding", "old-model") - saved = get_env_value("GLM_BASE_URL") or "" - assert saved == "https://custom.z.ai/api/paas/v4" + saved = get_env_value("KIMI_BASE_URL") or "" + assert saved == "https://custom.moonshot.ai/v1" def test_empty_base_url_keeps_default(self, config_home, monkeypatch): """Pressing Enter (empty) should not change the base URL.""" from hermes_cli.auth import PROVIDER_REGISTRY - pconfig = PROVIDER_REGISTRY.get("zai") + pconfig = PROVIDER_REGISTRY.get("kimi-coding") if not pconfig: - pytest.skip("zai not in PROVIDER_REGISTRY") + pytest.skip("kimi-coding not in PROVIDER_REGISTRY") - monkeypatch.setenv("GLM_API_KEY", "test-key") - monkeypatch.delenv("GLM_BASE_URL", raising=False) + monkeypatch.setenv("KIMI_API_KEY", "test-key") + monkeypatch.delenv("KIMI_BASE_URL", raising=False) from hermes_cli.main import _model_flow_api_key_provider from hermes_cli.config import load_config, get_env_value - with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + with patch("hermes_cli.auth._prompt_model_selection", return_value="kimi-k2.5"), \ patch("hermes_cli.auth.deactivate_provider"), \ patch("builtins.input", return_value=""): - _model_flow_api_key_provider(load_config(), "zai", "old-model") + _model_flow_api_key_provider(load_config(), "kimi-coding", "old-model") - saved = get_env_value("GLM_BASE_URL") or "" + saved = get_env_value("KIMI_BASE_URL") or "" assert saved == "", "Empty input should not save a base URL" + + diff --git a/tests/hermes_cli/test_model_validation.py b/tests/hermes_cli/test_model_validation.py index 72ffc5216dc6a..0f6b7b5699620 100644 --- a/tests/hermes_cli/test_model_validation.py +++ b/tests/hermes_cli/test_model_validation.py @@ -59,10 +59,15 @@ def test_provider_colon_model_switches_provider(self): assert model == "anthropic/claude-sonnet-4.5" def test_provider_alias_resolved(self): - provider, model = parse_model_input("glm:glm-5", "openrouter") + provider, model = parse_model_input("z-ai:glm-5", "openrouter") assert provider == "zai" assert model == "glm-5" + def test_zai_cn_is_own_provider(self): + provider, model = parse_model_input("zai-cn:glm-5", "openrouter") + assert provider == "zai-cn" + assert model == "glm-5" + def test_no_slash_no_colon_keeps_provider(self): provider, model = parse_model_input("gpt-5.4", "openrouter") assert provider == "openrouter" @@ -151,7 +156,8 @@ def test_defaults_to_openrouter(self): assert normalize_provider("") == "openrouter" def test_known_aliases(self): - assert normalize_provider("glm") == "zai" + assert normalize_provider("glm") == "zai-cn" + assert normalize_provider("z-ai") == "zai" assert normalize_provider("kimi") == "kimi-coding" assert normalize_provider("moonshot") == "kimi-coding" assert normalize_provider("github-copilot") == "copilot" diff --git a/trajectory_compressor.py b/trajectory_compressor.py index ff2dcc6266f28..a28f65b5e9ae7 100644 --- a/trajectory_compressor.py +++ b/trajectory_compressor.py @@ -435,17 +435,25 @@ def _get_async_client(self): def _detect_provider(self) -> str: """Detect the provider name from the configured base_url.""" url = self.config.base_url or "" + url_lower = url.lower() if base_url_host_matches(url, "openrouter.ai"): return "openrouter" if base_url_host_matches(url, "nousresearch.com"): return "nous" if ( base_url_hostname(url) == "chatgpt.com" - and "/backend-api/codex" in url.lower() + and "/backend-api/codex" in url_lower ): return "codex" - if base_url_host_matches(url, "z.ai"): - return "zai" + # Z.AI family — distinguish Coding Plan from direct API by path. + # Host alone isn't enough: api.z.ai hosts both /api/paas/v4 (direct) + # and /api/coding/paas/v4 (Coding Plan); same for open.bigmodel.cn. + # base_url_host_matches compares *hostnames only*, so path suffixes + # passed to it would never match — we inspect the path ourselves. + if base_url_host_matches(url, "api.z.ai"): + return "zai-coding-global" if "/coding/paas" in url_lower else "zai" + if base_url_host_matches(url, "open.bigmodel.cn"): + return "zai-coding-cn" if "/coding/paas" in url_lower else "zai-cn" if ( base_url_host_matches(url, "moonshot.ai") or base_url_host_matches(url, "moonshot.cn") From 296132cbc514dd8c681db63caf279ca05452f8fb Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:08:07 +0530 Subject: [PATCH 2/3] fix: add base_url_override to coding plan overlays Hardcode the coding plan endpoint URLs in HERMES_OVERLAYS rather than relying solely on models.dev API responses. Matches the pattern used by zai and zai-cn direct plan overlays for resilience. --- hermes_cli/providers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index 760a72bbd0f0b..fd39fe44df75d 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -100,11 +100,13 @@ class HermesOverlay: ), "zai-coding-cn": HermesOverlay( transport="openai_chat", + base_url_override="https://open.bigmodel.cn/api/coding/paas/v4", extra_env_vars=("GLM_CODING_API_KEY",), base_url_env_var="GLM_CODING_BASE_URL", ), "zai-coding-global": HermesOverlay( transport="openai_chat", + base_url_override="https://api.z.ai/api/coding/paas/v4", extra_env_vars=("ZAI_CODING_API_KEY",), base_url_env_var="ZAI_CODING_BASE_URL", ), From bef29f14b11abfa2e6133921228b43da2f242a96 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:10:35 +0530 Subject: [PATCH 3/3] chore: add yuanmingyi to AUTHOR_MAP --- scripts/release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.py b/scripts/release.py index 0a6f7b88ddc5b..42e3ed36826c3 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -335,6 +335,7 @@ "shalompmc0505@naver.com": "pinion05", "105142614+VTRiot@users.noreply.github.com": "VTRiot", "vivien000812@gmail.com": "iamagenius00", + "mingyi.yuan@aminer.cn": "yuanmingyi", }