diff --git a/.env.example b/.env.example index a740906..f248262 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,12 @@ # OpenRouter (multi-provider gateway) # OPENROUTER_API_KEY=sk-or-... +# Z.AI +# ZAI_API_KEY=... + +# Z.AI Global (coding plan endpoint) +# ZAI_GLOBAL_API_KEY=... + # Together AI # TOGETHER_API_KEY=... diff --git a/README.md b/README.md index bb3f177..ef8f909 100644 --- a/README.md +++ b/README.md @@ -265,11 +265,11 @@ Each adapter supports per-channel model overrides, DM/group policies, rate limit --- -## 27 LLM Providers — 123+ Models +## 29 LLM Providers — 130+ Models -3 native drivers (Anthropic, Gemini, OpenAI-compatible) route to 27 providers: +3 native drivers (Anthropic, Gemini, OpenAI-compatible) route to 29 providers: -Anthropic, Gemini, OpenAI, Groq, DeepSeek, OpenRouter, Together, Mistral, Fireworks, Cohere, Perplexity, xAI, AI21, Cerebras, SambaNova, HuggingFace, Replicate, Ollama, vLLM, LM Studio, Qwen, MiniMax, Zhipu, Moonshot, Qianfan, Bedrock, and more. +Anthropic, Gemini, OpenAI, Groq, DeepSeek, OpenRouter, Together, Mistral, Fireworks, Cohere, Perplexity, xAI, AI21, Cerebras, SambaNova, HuggingFace, Replicate, Ollama, vLLM, LM Studio, Qwen, MiniMax, Zhipu, Z.AI, Z.AI Global, Moonshot, Qianfan, Bedrock, and more. Intelligent routing with task complexity scoring, automatic fallback, cost tracking, and per-model pricing. diff --git a/crates/openfang-api/static/js/pages/wizard.js b/crates/openfang-api/static/js/pages/wizard.js index 26c28cd..8c5bb06 100644 --- a/crates/openfang-api/static/js/pages/wizard.js +++ b/crates/openfang-api/static/js/pages/wizard.js @@ -320,7 +320,7 @@ function wizardPage() { }, get popularProviders() { - var popular = ['anthropic', 'openai', 'gemini', 'groq', 'deepseek', 'openrouter']; + var popular = ['anthropic', 'openai', 'gemini', 'groq', 'deepseek', 'openrouter', 'zai', 'zai-global']; return this.providers.filter(function(p) { return popular.indexOf(p.id) >= 0; }).sort(function(a, b) { @@ -329,7 +329,7 @@ function wizardPage() { }, get otherProviders() { - var popular = ['anthropic', 'openai', 'gemini', 'groq', 'deepseek', 'openrouter']; + var popular = ['anthropic', 'openai', 'gemini', 'groq', 'deepseek', 'openrouter', 'zai', 'zai-global']; return this.providers.filter(function(p) { return popular.indexOf(p.id) < 0; }); @@ -350,6 +350,8 @@ function wizardPage() { groq: { url: 'https://console.groq.com/keys', text: 'Get your key from the Groq Console (free tier available)' }, deepseek: { url: 'https://platform.deepseek.com/api_keys', text: 'Get your key from the DeepSeek Platform (very affordable)' }, openrouter: { url: 'https://openrouter.ai/keys', text: 'Get your key from OpenRouter (access 100+ models with one key)' }, + zai: { url: 'https://docs.z.ai/api-reference/introduction', text: 'Get your key from Z.AI dashboard and use OpenAI-compatible API' }, + 'zai-global': { url: 'https://docs.z.ai/guides/coding-plan/overview', text: 'Use Z.AI Coding Global plan endpoint for coding-focused workloads' }, mistral: { url: 'https://console.mistral.ai/api-keys', text: 'Get your key from the Mistral Console' }, together: { url: 'https://api.together.xyz/settings/api-keys', text: 'Get your key from Together AI' }, fireworks: { url: 'https://fireworks.ai/account/api-keys', text: 'Get your key from Fireworks AI' }, @@ -468,6 +470,8 @@ function wizardPage() { gemini: 'gemini-2.5-flash', groq: 'llama-3.3-70b-versatile', deepseek: 'deepseek-chat', + zai: 'glm-5', + 'zai-global': 'glm-4.5-air', openrouter: 'openrouter/auto', mistral: 'mistral-large-latest', together: 'meta-llama/Llama-3-70b-chat-hf', diff --git a/crates/openfang-cli/src/main.rs b/crates/openfang-cli/src/main.rs index 5d908ff..f329749 100644 --- a/crates/openfang-cli/src/main.rs +++ b/crates/openfang-cli/src/main.rs @@ -1224,6 +1224,13 @@ fn provider_list() -> Vec<(&'static str, &'static str, &'static str, &'static st "openrouter/auto", "OpenRouter", ), + ("zai", "ZAI_API_KEY", "glm-5", "Z.AI"), + ( + "zai-global", + "ZAI_GLOBAL_API_KEY", + "glm-4.5-air", + "Z.AI Global", + ), ] } @@ -3712,6 +3719,8 @@ fn provider_to_env_var(provider: &str) -> String { "perplexity" => "PERPLEXITY_API_KEY".to_string(), "cohere" => "COHERE_API_KEY".to_string(), "xai" => "XAI_API_KEY".to_string(), + "zai" | "z.ai" => "ZAI_API_KEY".to_string(), + "zai-global" | "zai_global" | "z.ai-global" => "ZAI_GLOBAL_API_KEY".to_string(), "brave" => "BRAVE_API_KEY".to_string(), "tavily" => "TAVILY_API_KEY".to_string(), other => format!("{}_API_KEY", other.to_uppercase()), @@ -3763,6 +3772,14 @@ pub(crate) fn test_api_key(provider: &str, env_var: &str) -> bool { .get("https://openrouter.ai/api/v1/models") .bearer_auth(&key) .send(), + "zai" | "z.ai" => client + .get("https://api.z.ai/api/paas/v4/models") + .bearer_auth(&key) + .send(), + "zai-global" | "zai_global" | "z.ai-global" => client + .get("https://api.z.ai/api/coding/paas/v4/models") + .bearer_auth(&key) + .send(), _ => return true, // unknown provider — skip test }; diff --git a/crates/openfang-cli/src/tui/screens/init_wizard.rs b/crates/openfang-cli/src/tui/screens/init_wizard.rs index 3462ab7..612646b 100644 --- a/crates/openfang-cli/src/tui/screens/init_wizard.rs +++ b/crates/openfang-cli/src/tui/screens/init_wizard.rs @@ -100,6 +100,22 @@ const PROVIDERS: &[ProviderInfo] = &[ needs_key: true, hint: "", }, + ProviderInfo { + name: "zai", + display: "Z.AI", + env_var: "ZAI_API_KEY", + default_model: "glm-5", + needs_key: true, + hint: "", + }, + ProviderInfo { + name: "zai-global", + display: "Z.AI Global", + env_var: "ZAI_GLOBAL_API_KEY", + default_model: "glm-4.5-air", + needs_key: true, + hint: "", + }, ProviderInfo { name: "ollama", display: "Ollama", diff --git a/crates/openfang-cli/src/tui/screens/wizard.rs b/crates/openfang-cli/src/tui/screens/wizard.rs index d225fb4..f49206e 100644 --- a/crates/openfang-cli/src/tui/screens/wizard.rs +++ b/crates/openfang-cli/src/tui/screens/wizard.rs @@ -67,6 +67,18 @@ const PROVIDERS: &[ProviderInfo] = &[ default_model: "accounts/fireworks/models/llama-v3p3-70b-instruct", needs_key: true, }, + ProviderInfo { + name: "zai", + env_var: "ZAI_API_KEY", + default_model: "glm-5", + needs_key: true, + }, + ProviderInfo { + name: "zai-global", + env_var: "ZAI_GLOBAL_API_KEY", + default_model: "glm-4.5-air", + needs_key: true, + }, ProviderInfo { name: "ollama", env_var: "OLLAMA_API_KEY", diff --git a/crates/openfang-migrate/src/openclaw.rs b/crates/openfang-migrate/src/openclaw.rs index 5bd9a26..b31ebb6 100644 --- a/crates/openfang-migrate/src/openclaw.rs +++ b/crates/openfang-migrate/src/openclaw.rs @@ -646,6 +646,8 @@ fn map_provider(openclaw_provider: &str) -> String { "fireworks" => "fireworks".to_string(), "google" | "gemini" => "google".to_string(), "xai" | "grok" => "xai".to_string(), + "z.ai" | "zai" => "zai".to_string(), + "z.ai-global" | "zai-global" | "zai_global" => "zai-global".to_string(), "cerebras" => "cerebras".to_string(), "sambanova" => "sambanova".to_string(), other => other.to_string(), @@ -665,6 +667,8 @@ fn default_api_key_env(provider: &str) -> String { "fireworks" => "FIREWORKS_API_KEY".to_string(), "google" => "GOOGLE_API_KEY".to_string(), "xai" => "XAI_API_KEY".to_string(), + "zai" => "ZAI_API_KEY".to_string(), + "zai-global" => "ZAI_GLOBAL_API_KEY".to_string(), "cerebras" => "CEREBRAS_API_KEY".to_string(), "sambanova" => "SAMBANOVA_API_KEY".to_string(), "ollama" => String::new(), // Ollama doesn't need an API key diff --git a/crates/openfang-runtime/src/drivers/mod.rs b/crates/openfang-runtime/src/drivers/mod.rs index 56dc4f8..d685b77 100644 --- a/crates/openfang-runtime/src/drivers/mod.rs +++ b/crates/openfang-runtime/src/drivers/mod.rs @@ -15,9 +15,9 @@ use openfang_types::model_catalog::{ AI21_BASE_URL, ANTHROPIC_BASE_URL, CEREBRAS_BASE_URL, COHERE_BASE_URL, DEEPSEEK_BASE_URL, FIREWORKS_BASE_URL, GEMINI_BASE_URL, GROQ_BASE_URL, HUGGINGFACE_BASE_URL, LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL, - OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL, - REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VLLM_BASE_URL, XAI_BASE_URL, - ZHIPU_BASE_URL, + OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL, REPLICATE_BASE_URL, + SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VLLM_BASE_URL, XAI_BASE_URL, ZAI_BASE_URL, + ZAI_GLOBAL_BASE_URL, ZHIPU_BASE_URL, }; use std::sync::Arc; @@ -152,6 +152,16 @@ fn provider_defaults(provider: &str) -> Option { api_key_env: "ZHIPU_API_KEY", key_required: true, }), + "zai" | "z.ai" => Some(ProviderDefaults { + base_url: ZAI_BASE_URL, + api_key_env: "ZAI_API_KEY", + key_required: true, + }), + "zai-global" | "zai_global" | "z.ai-global" => Some(ProviderDefaults { + base_url: ZAI_GLOBAL_BASE_URL, + api_key_env: "ZAI_GLOBAL_API_KEY", + key_required: true, + }), "qianfan" | "baidu" => Some(ProviderDefaults { base_url: QIANFAN_BASE_URL, api_key_env: "QIANFAN_API_KEY", @@ -251,6 +261,13 @@ pub fn create_driver(config: &DriverConfig) -> Result, LlmErr .api_key .clone() .or_else(|| std::env::var(defaults.api_key_env).ok()) + .or_else(|| { + if provider == "zai-global" { + std::env::var("ZAI_API_KEY").ok() + } else { + None + } + }) .unwrap_or_default(); if defaults.key_required && api_key.is_empty() { @@ -282,7 +299,8 @@ pub fn create_driver(config: &DriverConfig) -> Result, LlmErr message: format!( "Unknown provider '{}'. Supported: anthropic, gemini, openai, groq, openrouter, \ deepseek, together, mistral, fireworks, ollama, vllm, lmstudio, perplexity, \ - cohere, ai21, cerebras, sambanova, huggingface, xai, replicate, github-copilot. \ + cohere, ai21, cerebras, sambanova, huggingface, xai, replicate, github-copilot, \ + moonshot, qwen, minimax, zhipu, qianfan, zai, zai-global. \ Or set base_url for a custom OpenAI-compatible endpoint.", provider ), @@ -317,6 +335,8 @@ pub fn known_providers() -> &'static [&'static str] { "qwen", "minimax", "zhipu", + "zai", + "zai-global", "qianfan", ] } @@ -409,8 +429,10 @@ mod tests { assert!(providers.contains(&"qwen")); assert!(providers.contains(&"minimax")); assert!(providers.contains(&"zhipu")); + assert!(providers.contains(&"zai")); + assert!(providers.contains(&"zai-global")); assert!(providers.contains(&"qianfan")); - assert_eq!(providers.len(), 26); + assert_eq!(providers.len(), 28); } #[test] @@ -450,4 +472,17 @@ mod tests { assert_eq!(d.api_key_env, "HF_API_KEY"); assert!(d.key_required); } + + #[test] + fn test_provider_defaults_zai_variants() { + let d = provider_defaults("zai").unwrap(); + assert_eq!(d.base_url, "https://api.z.ai/api/paas/v4"); + assert_eq!(d.api_key_env, "ZAI_API_KEY"); + assert!(d.key_required); + + let g = provider_defaults("zai-global").unwrap(); + assert_eq!(g.base_url, "https://api.z.ai/api/coding/paas/v4"); + assert_eq!(g.api_key_env, "ZAI_GLOBAL_API_KEY"); + assert!(g.key_required); + } } diff --git a/crates/openfang-runtime/src/model_catalog.rs b/crates/openfang-runtime/src/model_catalog.rs index 65bff00..1069bf3 100644 --- a/crates/openfang-runtime/src/model_catalog.rs +++ b/crates/openfang-runtime/src/model_catalog.rs @@ -1,6 +1,6 @@ //! Model catalog — registry of known models with metadata, pricing, and auth detection. //! -//! Provides a comprehensive catalog of 130+ builtin models across 27 providers, +//! Provides a comprehensive catalog of 130+ builtin models across 29 providers, //! with alias resolution, auth status detection, and pricing lookups. use openfang_types::model_catalog::{ @@ -10,7 +10,7 @@ use openfang_types::model_catalog::{ LMSTUDIO_BASE_URL, MINIMAX_BASE_URL, MISTRAL_BASE_URL, MOONSHOT_BASE_URL, OLLAMA_BASE_URL, OPENAI_BASE_URL, OPENROUTER_BASE_URL, PERPLEXITY_BASE_URL, QIANFAN_BASE_URL, QWEN_BASE_URL, REPLICATE_BASE_URL, SAMBANOVA_BASE_URL, TOGETHER_BASE_URL, VLLM_BASE_URL, XAI_BASE_URL, - ZHIPU_BASE_URL, + ZAI_BASE_URL, ZAI_GLOBAL_BASE_URL, ZHIPU_BASE_URL, }; use std::collections::HashMap; @@ -54,6 +54,9 @@ impl ModelCatalog { // Special case: Gemini also accepts GOOGLE_API_KEY if provider.id == "gemini" && std::env::var("GOOGLE_API_KEY").is_ok() { provider.auth_status = AuthStatus::Configured; + // Special case: Z.AI Global can reuse ZAI_API_KEY + } else if provider.id == "zai-global" && std::env::var("ZAI_API_KEY").is_ok() { + provider.auth_status = AuthStatus::Configured; } else { provider.auth_status = AuthStatus::Missing; } @@ -413,6 +416,24 @@ fn builtin_providers() -> Vec { auth_status: AuthStatus::Missing, model_count: 0, }, + ProviderInfo { + id: "zai".into(), + display_name: "Z.AI".into(), + api_key_env: "ZAI_API_KEY".into(), + base_url: ZAI_BASE_URL.into(), + key_required: true, + auth_status: AuthStatus::Missing, + model_count: 0, + }, + ProviderInfo { + id: "zai-global".into(), + display_name: "Z.AI Global (Coding)".into(), + api_key_env: "ZAI_GLOBAL_API_KEY".into(), + base_url: ZAI_GLOBAL_BASE_URL.into(), + key_required: true, + auth_status: AuthStatus::Missing, + model_count: 0, + }, ProviderInfo { id: "moonshot".into(), display_name: "Moonshot (Kimi)".into(), @@ -2115,8 +2136,53 @@ fn builtin_models() -> Vec { aliases: vec![], }, // ══════════════════════════════════════════════════════════════ - // Moonshot / Kimi (3) + // Z.AI (3) // ══════════════════════════════════════════════════════════════ + // Z.AI (official OpenAI-compatible endpoint) + ModelCatalogEntry { + id: "glm-5".into(), + display_name: "GLM-5".into(), + provider: "zai".into(), + tier: ModelTier::Frontier, + context_window: 131_072, + max_output_tokens: 8_192, + input_cost_per_m: 0.0, + output_cost_per_m: 0.0, + supports_tools: true, + supports_vision: true, + supports_streaming: true, + aliases: vec![], + }, + ModelCatalogEntry { + id: "glm-4.7".into(), + display_name: "GLM-4.7".into(), + provider: "zai".into(), + tier: ModelTier::Smart, + context_window: 128_000, + max_output_tokens: 8_192, + input_cost_per_m: 0.0, + output_cost_per_m: 0.0, + supports_tools: true, + supports_vision: true, + supports_streaming: true, + aliases: vec![], + }, + // Z.AI Global coding plan endpoint + ModelCatalogEntry { + id: "glm-4.5-air".into(), + display_name: "GLM-4.5 Air".into(), + provider: "zai-global".into(), + tier: ModelTier::Fast, + context_window: 128_000, + max_output_tokens: 8_192, + input_cost_per_m: 0.0, + output_cost_per_m: 0.0, + supports_tools: true, + supports_vision: true, + supports_streaming: true, + aliases: vec![], + }, + // Moonshot / Kimi (3) ModelCatalogEntry { id: "moonshot-v1-128k".into(), display_name: "Moonshot V1 128K".into(), @@ -2307,7 +2373,7 @@ mod tests { #[test] fn test_catalog_has_providers() { let catalog = ModelCatalog::new(); - assert_eq!(catalog.list_providers().len(), 27); + assert_eq!(catalog.list_providers().len(), 29); } #[test] @@ -2525,6 +2591,8 @@ mod tests { assert!(catalog.get_provider("qwen").is_some()); assert!(catalog.get_provider("minimax").is_some()); assert!(catalog.get_provider("zhipu").is_some()); + assert!(catalog.get_provider("zai").is_some()); + assert!(catalog.get_provider("zai-global").is_some()); assert!(catalog.get_provider("moonshot").is_some()); assert!(catalog.get_provider("qianfan").is_some()); assert!(catalog.get_provider("bedrock").is_some()); diff --git a/crates/openfang-types/src/model_catalog.rs b/crates/openfang-types/src/model_catalog.rs index 35cd828..4a42827 100644 --- a/crates/openfang-types/src/model_catalog.rs +++ b/crates/openfang-types/src/model_catalog.rs @@ -36,6 +36,8 @@ pub const GITHUB_COPILOT_BASE_URL: &str = "https://api.githubcopilot.com"; pub const QWEN_BASE_URL: &str = "https://dashscope.aliyuncs.com/compatible-mode/v1"; pub const MINIMAX_BASE_URL: &str = "https://api.minimax.chat/v1"; pub const ZHIPU_BASE_URL: &str = "https://open.bigmodel.cn/api/paas/v4"; +pub const ZAI_BASE_URL: &str = "https://api.z.ai/api/paas/v4"; +pub const ZAI_GLOBAL_BASE_URL: &str = "https://api.z.ai/api/coding/paas/v4"; pub const MOONSHOT_BASE_URL: &str = "https://api.moonshot.cn/v1"; pub const QIANFAN_BASE_URL: &str = "https://qianfan.baidubce.com/v2"; diff --git a/docs/cli-reference.md b/docs/cli-reference.md index e2a5f4f..addd6e9 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -896,7 +896,7 @@ openfang config set-key | Argument | Description | |---|---| -| `` | Provider name (e.g. `groq`, `anthropic`, `openai`, `gemini`, `deepseek`, `openrouter`, `together`, `mistral`, `fireworks`, `perplexity`, `cohere`, `xai`, `brave`, `tavily`). | +| `` | Provider name (e.g. `groq`, `anthropic`, `openai`, `gemini`, `deepseek`, `openrouter`, `zai`, `zai-global`, `together`, `mistral`, `fireworks`, `perplexity`, `cohere`, `xai`, `brave`, `tavily`). | **Behavior:** diff --git a/docs/configuration.md b/docs/configuration.md index 5e1195a..9389d95 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -267,7 +267,7 @@ api_key_env = "ANTHROPIC_API_KEY" | Field | Type | Default | Description | |-------|------|---------|-------------| -| `provider` | string | `"anthropic"` | Provider name. Supported: `anthropic`, `gemini`, `openai`, `groq`, `openrouter`, `deepseek`, `together`, `mistral`, `fireworks`, `ollama`, `vllm`, `lmstudio`, `perplexity`, `cohere`, `ai21`, `cerebras`, `sambanova`, `huggingface`, `xai`, `replicate`. | +| `provider` | string | `"anthropic"` | Provider name. Supported: `anthropic`, `gemini`, `openai`, `groq`, `openrouter`, `deepseek`, `together`, `mistral`, `fireworks`, `ollama`, `vllm`, `lmstudio`, `perplexity`, `cohere`, `ai21`, `cerebras`, `sambanova`, `huggingface`, `xai`, `replicate`, `zhipu`, `zai`, `zai-global`, `moonshot`, `qwen`, `minimax`, `qianfan`, `bedrock`. | | `model` | string | `"claude-sonnet-4-20250514"` | Model identifier. Aliases like `sonnet`, `haiku`, `gpt-4o`, `gemini-flash` are resolved by the model catalog. | | `api_key_env` | string | `"ANTHROPIC_API_KEY"` | Name of the environment variable holding the API key. The actual key is read from this env var at runtime, never stored in config. | | `base_url` | string or null | `null` | Override the API base URL. Useful for proxies or self-hosted endpoints. When `null`, the provider's default URL from the model catalog is used. |