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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -1477,7 +1481,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
Expand Down
3 changes: 0 additions & 3 deletions agent/credential_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion agent/model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions agent/models_dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1724,7 +1724,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)
Expand Down Expand Up @@ -10716,7 +10716,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)
Expand Down
23 changes: 18 additions & 5 deletions gateway/platforms/qqbot/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -1612,26 +1612,39 @@ 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:
return {
"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`)
qq_stt_key = os.getenv("QQ_STT_API_KEY", "")
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 {
Expand Down
149 changes: 35 additions & 114 deletions hermes_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -345,7 +375,6 @@ def get_anthropic_key() -> str:
# KIMI_BASE_URL explicitly.
KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1"


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.

Expand Down Expand Up @@ -415,113 +444,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
# =============================================================================
Expand Down Expand Up @@ -978,7 +900,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",
Expand Down Expand Up @@ -2632,8 +2555,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:
Expand Down
Loading