feat(models): GLM 5.1/5.2/5.3 via API and via the Claude Code CLI (Z.ai devpack)

Add GLM 5.1, 5.2 and 5.3 to the Z.ai provider (harness + web). Z.ai GLM can
now run two ways:
- plain API (OpenAI-compatible, api.z.ai/api/paas/v4) as before, and
- agentically through the Claude Code CLI pointed at Z.ai's Anthropic-compatible
  endpoint (the Z.ai devpack / cline / zcode setup) when the subscription
  toggle is on — chat_claude_stream injects ANTHROPIC_BASE_URL +
  ANTHROPIC_AUTH_TOKEN from ZAI_API_KEY for the zai provider.

cli_binary_for("zai") -> claude; cli_login_status checks ZAI_API_KEY instead of
probing the Claude login; web verifies the key against z.ai's /models. GLM is
reasoning-heavy so it keeps the larger token ceiling. Covered by a new test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUPandClaude Opus 4.8 committed 2026-10-15 20:20:18 -03:00
1 parent a094fedc5c
commit 0aaac20a0d
2 files changed
+65 -8

No files matched your search

+59 -6
View File
@@ -39,8 +39,13 @@ pub fn providers() -> Vec<Provider> {
Provider { key: "qwen", label: "Qwen (DashScope)", base_url: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", env_key: "DASHSCOPE_API_KEY", kind: "api",
models: vec!["qwen3.8-max", "qwen3.8-omni-flash", "qwen-max", "qwen2.5-coder-32b-instruct", "qwq-plus"] },
// Z.ai (Zhipu / GLM). OpenAI-compatible open platform endpoint.
Provider { key: "zai", label: "Z.ai (GLM)", base_url: "https://api.z.ai/api/paas/v4", env_key: "ZAI_API_KEY", kind: "api",
models: vec!["glm-5.3", "glm-5.3-flashx", "glm-4.6"] },
// Z.ai GLM. `kind: "cli"` so a run can drive GLM agentically through the
// Claude Code CLI using Z.ai's Anthropic-compatible endpoint (the Z.ai
// devpack "cline"/"zcode" integration) — the base URL + auth token are
// injected for the `claude` binary in chat_claude_stream. The plain HTTP
// API path (api.z.ai/api/paas/v4, OpenAI-compatible) still works too.
Provider { key: "zai", label: "Z.ai (GLM)", base_url: "https://api.z.ai/api/paas/v4", env_key: "ZAI_API_KEY", kind: "cli",
models: vec!["glm-5.3", "glm-5.2", "glm-5.1", "glm-5.3-flashx", "glm-4.6"] },
Provider { key: "groq", label: "Groq", base_url: "https://api.groq.com/openai/v1", env_key: "GROQ_API_KEY", kind: "api",
models: vec!["llama-3.3-70b-versatile", "qwen-2.5-coder-32b"] },
Provider { key: "together", label: "Together AI", base_url: "https://api.together.xyz/v1", env_key: "TOGETHER_API_KEY", kind: "api",
@@ -298,7 +303,7 @@ impl ChatClient {
// Claude Code can stream structured events (tools, commands, files) which
// we surface live as a categorized activity feed, attributed to `label`.
if bin == "claude" {
return self.chat_claude_stream(label, model, &prompt, mcp_config, progress).await;
return self.chat_claude_stream(label, provider, model, &prompt, mcp_config, progress).await;
}
// Codex exec streams JSONL events (`--json`): commands it runs, agent
// messages, file changes, token usage. Surface them live so recon and
@@ -441,6 +446,7 @@ impl ChatClient {
async fn chat_claude_stream(
&self,
label: &str,
provider: &str,
model: &str,
prompt: &str,
mcp_config: Option<&str>,
@@ -450,15 +456,28 @@ impl ChatClient {
cmd.arg("-p").arg("--model").arg(model)
.arg("--output-format").arg("stream-json").arg("--verbose")
.arg("--dangerously-skip-permissions")
.env("IS_SANDBOX", "1")
.env("IS_SANDBOX", "1");
if provider == "zai" {
// Z.ai GLM via the Claude Code CLI (devpack / cline / zcode): point
// the CLI at Z.ai's Anthropic-compatible endpoint and authenticate
// with the Z.ai key, so `claude --model glm-5.3` drives GLM.
let token = std::env::var("ZAI_API_KEY").unwrap_or_default();
if token.trim().is_empty() {
return Err(anyhow!("Z.ai GLM via CLI needs ZAI_API_KEY (get a key at https://z.ai, see https://docs.z.ai/devpack/tool/cline)"));
}
cmd.env("ANTHROPIC_BASE_URL", zai_anthropic_base_url())
.env("ANTHROPIC_AUTH_TOKEN", token)
.env_remove("ANTHROPIC_API_KEY");
} else {
// Subscription mode means the `claude` CLI must authenticate with the
// user's logged-in session. A stray ANTHROPIC_API_KEY in the inherited
// environment (e.g. an invalid/stale key saved in the web console)
// makes the CLI switch to API-key billing and fail with
// "Invalid API key · Fix external API key". Strip it so the CLI
// always uses the subscription login here.
.env_remove("ANTHROPIC_API_KEY")
.env_remove("ANTHROPIC_AUTH_TOKEN");
cmd.env_remove("ANTHROPIC_API_KEY")
.env_remove("ANTHROPIC_AUTH_TOKEN");
}
if let Some(mcp) = mcp_config {
cmd.arg("--mcp-config").arg(mcp);
}
@@ -720,6 +739,11 @@ fn mcp_servers_from(path: &str) -> Vec<(String, String, String)> {
pub fn cli_binary_for(provider: &str) -> Option<&'static str> {
match provider {
"anthropic" => Some("claude"),
// Z.ai GLM runs through the Claude Code CLI pointed at Z.ai's
// Anthropic-compatible endpoint (the Z.ai devpack "cline"/"zcode" setup):
// chat_claude_stream injects ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN
// from ZAI_API_KEY for this provider.
"zai" => Some("claude"),
"openai" => Some("codex"),
"xai" => Some("grok"),
"gemini" => Some("gemini"),
@@ -729,6 +753,14 @@ pub fn cli_binary_for(provider: &str) -> Option<&'static str> {
}
}
/// Z.ai's Anthropic-compatible base URL for driving GLM through the Claude Code
/// CLI (Z.ai devpack / cline / zcode). Overridable for self-hosted gateways.
pub fn zai_anthropic_base_url() -> String {
std::env::var("ZAI_ANTHROPIC_BASE_URL")
.or_else(|_| std::env::var("ZAI_CODING_BASE_URL"))
.unwrap_or_else(|_| "https://api.z.ai/api/anthropic".to_string())
}
/// Is `name` an executable found on PATH?
pub fn binary_in_path(name: &str) -> bool {
std::env::var_os("PATH")
@@ -757,6 +789,14 @@ pub enum LoginStatus {
pub async fn cli_login_status(provider: &str) -> LoginStatus {
let Some(bin) = cli_binary_for(provider) else { return LoginStatus::NotInstalled };
if !binary_in_path(bin) { return LoginStatus::NotInstalled; }
// Z.ai GLM runs through the `claude` binary but authenticates with a Z.ai
// key, not the Claude login — so a trivial `claude` probe here would check
// the wrong credential. It's usable iff the binary exists and ZAI_API_KEY
// is set.
if provider == "zai" {
let has_key = std::env::var("ZAI_API_KEY").map(|v| !v.trim().is_empty()).unwrap_or(false);
return if has_key { LoginStatus::LoggedIn } else { LoginStatus::NotLoggedIn };
}
let mut cmd = Command::new(bin);
// opencode/hermes take the probe prompt as an argv value, not stdin.
let prompt_via_stdin = !matches!(bin, "opencode" | "hermes");
@@ -1037,6 +1077,19 @@ mod response_tests {
use super::*;
use serde_json::json;
#[test]
fn zai_glm_catalog_and_cli_backend() {
// GLM must be offered in all requested versions and be drivable through
// the Claude Code CLI (Z.ai devpack / cline / zcode).
let z = providers().into_iter().find(|p| p.key == "zai").expect("zai provider");
for m in ["glm-5.1", "glm-5.2", "glm-5.3"] {
assert!(z.models.contains(&m), "GLM {m} missing from catalog: {:?}", z.models);
}
assert_eq!(cli_binary_for("zai"), Some("claude"), "GLM runs via the claude CLI");
// GLM is reasoning-heavy → gets the larger token ceiling.
assert_eq!(max_tokens_for("glm-5.3"), 32768);
}
#[test]
fn anthropic_catalog_lists_opus_4_6() {
// Regression: the web console offers claude-opus-4-6, so the harness
+6 -2
View File
@@ -121,8 +121,11 @@ const PROVIDERS = [
models: ['mistral-large-latest', 'codestral-latest'] },
{ key: 'qwen', label: 'Qwen (DashScope)', kind: 'api', envKey: 'DASHSCOPE_API_KEY',
models: ['qwen3.8-max', 'qwen3.8-omni-flash', 'qwen-max', 'qwen2.5-coder-32b-instruct', 'qwq-plus'] },
// Z.ai GLM: always needs a ZAI_API_KEY (so kind:'api' keeps the key field),
// but with the subscription toggle on it runs agentically through the Claude
// Code CLI pointed at Z.ai's Anthropic-compatible endpoint (devpack/cline).
{ key: 'zai', label: 'Z.ai (GLM)', kind: 'api', envKey: 'ZAI_API_KEY',
models: ['glm-5.3', 'glm-5.3-flashx', 'glm-4.6'] },
models: ['glm-5.3', 'glm-5.2', 'glm-5.1', 'glm-5.3-flashx', 'glm-4.6'] },
{ key: 'groq', label: 'Groq', kind: 'api', envKey: 'GROQ_API_KEY',
models: ['llama-3.3-70b-versatile', 'qwen-2.5-coder-32b'] },
{ key: 'together', label: 'Together AI', kind: 'api', envKey: 'TOGETHER_API_KEY',
@@ -163,7 +166,8 @@ function keyProbe(provider, key) {
case 'moonshot': return { url: 'https://api.moonshot.ai/v1/models', headers: bearer };
case 'qwen': return { url: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1/models', headers: bearer };
case 'nvidia_nim': return { url: 'https://integrate.api.nvidia.com/v1/models', headers: bearer };
default: return null; // opencode, nous, zai, litellm, azure, ollama, llamacpp — not auto-verifiable here
case 'zai': return { url: 'https://api.z.ai/api/paas/v4/models', headers: bearer };
default: return null; // opencode, nous, litellm, azure, ollama, llamacpp — not auto-verifiable here
}
}