From e63ff014cbaa5d1fa2db1022ccf53017d4fd1906 Mon Sep 17 00:00:00 2001 From: Duc Nguyen Date: Mon, 9 Mar 2026 22:28:18 +0700 Subject: [PATCH] feat: add Z.ai provider support (general API + coding plan) (#102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Z.ai provider support (general API + coding plan) Add Z.ai (GLM) as a new LLM provider with two variants: - `zai`: general API (api.z.ai/api/paas/v4) - `zai_coding`: coding plan (api.z.ai/api/coding/paas/v4) Reuses OpenAIProvider — Z.ai API is OpenAI-compatible with Bearer token auth, SSE streaming, and reasoning_content support. Includes: store constants, config struct fields, env var loading (GOCLAW_ZAI_API_KEY, GOCLAW_ZAI_CODING_API_KEY), secret masking, config + DB registration, onboard wizard, and UI provider types. Default model: glm-5 Closes #100 * docs: add Z.ai provider entries to providers documentation --- cmd/gateway_providers.go | 30 ++++++++++++++++++++++++++++++ cmd/onboard_managed.go | 2 ++ docs/02-providers.md | 2 ++ internal/config/config_channels.go | 4 ++++ internal/config/config_load.go | 2 ++ internal/config/config_secrets.go | 6 ++++++ internal/store/provider_store.go | 4 ++++ ui/web/src/constants/providers.ts | 2 ++ 8 files changed, 52 insertions(+) diff --git a/cmd/gateway_providers.go b/cmd/gateway_providers.go index 1261af07..a561ece9 100644 --- a/cmd/gateway_providers.go +++ b/cmd/gateway_providers.go @@ -98,6 +98,24 @@ func registerProviders(registry *providers.Registry, cfg *config.Config) { slog.Info("registered provider", "name", "bailian") } + if cfg.Providers.Zai.APIKey != "" { + base := cfg.Providers.Zai.APIBase + if base == "" { + base = "https://api.z.ai/api/paas/v4" + } + registry.Register(providers.NewOpenAIProvider("zai", cfg.Providers.Zai.APIKey, base, "glm-5")) + slog.Info("registered provider", "name", "zai") + } + + if cfg.Providers.ZaiCoding.APIKey != "" { + base := cfg.Providers.ZaiCoding.APIBase + if base == "" { + base = "https://api.z.ai/api/coding/paas/v4" + } + registry.Register(providers.NewOpenAIProvider("zai-coding", cfg.Providers.ZaiCoding.APIKey, base, "glm-5")) + slog.Info("registered provider", "name", "zai-coding") + } + // Claude CLI provider (subscription-based, no API key needed) if cfg.Providers.ClaudeCLI.CLIPath != "" { cliPath := cfg.Providers.ClaudeCLI.CLIPath @@ -243,6 +261,18 @@ func registerProvidersFromDB(registry *providers.Registry, provStore store.Provi base = "https://coding-intl.dashscope.aliyuncs.com/v1" } registry.Register(providers.NewOpenAIProvider(p.Name, p.APIKey, base, "qwen3.5-plus")) + case store.ProviderZai: + base := p.APIBase + if base == "" { + base = "https://api.z.ai/api/paas/v4" + } + registry.Register(providers.NewOpenAIProvider(p.Name, p.APIKey, base, "glm-5")) + case store.ProviderZaiCoding: + base := p.APIBase + if base == "" { + base = "https://api.z.ai/api/coding/paas/v4" + } + registry.Register(providers.NewOpenAIProvider(p.Name, p.APIKey, base, "glm-5")) case store.ProviderSuno: // Suno is a media-only provider (music gen). Register as OpenAI-compat // so credentialProvider interface works for API key/base extraction. diff --git a/cmd/onboard_managed.go b/cmd/onboard_managed.go index a7e8f35c..88ab8fd4 100644 --- a/cmd/onboard_managed.go +++ b/cmd/onboard_managed.go @@ -31,6 +31,8 @@ var defaultPlaceholderProviders = []store.LLMProviderData{ {Name: "synthetic", DisplayName: "Synthetic", ProviderType: store.ProviderOpenAICompat, APIBase: "https://api.synthetic.new/openai/v1", Enabled: false}, {Name: "alicloud-api", DisplayName: "AliCloud API", ProviderType: store.ProviderDashScope, APIBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", Enabled: false}, {Name: "alicloud-sub", DisplayName: "AliCloud Sub", ProviderType: store.ProviderBailian, APIBase: "https://coding-intl.dashscope.aliyuncs.com/v1", Enabled: false}, + {Name: "zai", DisplayName: "Z.ai API", ProviderType: store.ProviderZai, APIBase: "https://api.z.ai/api/paas/v4", Enabled: false}, + {Name: "zai-coding", DisplayName: "Z.ai Coding Plan", ProviderType: store.ProviderZaiCoding, APIBase: "https://api.z.ai/api/coding/paas/v4", Enabled: false}, } // seedOnboardPlaceholders opens a PG store and seeds disabled placeholder providers diff --git a/docs/02-providers.md b/docs/02-providers.md index 2ecb1746..72f08c89 100644 --- a/docs/02-providers.md +++ b/docs/02-providers.md @@ -45,6 +45,8 @@ The Anthropic provider uses `x-api-key` header authentication and the `anthropic | perplexity | OpenAI-compatible | `https://api.perplexity.ai` | `sonar-pro` | | dashscope | OpenAI-compatible | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `qwen3-max` | | bailian | OpenAI-compatible | `https://coding-intl.dashscope.aliyuncs.com/v1` | `qwen3.5-plus` | +| zai | OpenAI-compatible | `https://api.z.ai/api/paas/v4` | `glm-5` | +| zai_coding | OpenAI-compatible | `https://api.z.ai/api/coding/paas/v4` | `glm-5` | --- diff --git a/internal/config/config_channels.go b/internal/config/config_channels.go index 16ac4d85..064edc02 100644 --- a/internal/config/config_channels.go +++ b/internal/config/config_channels.go @@ -182,6 +182,8 @@ type ProvidersConfig struct { Perplexity ProviderConfig `json:"perplexity"` DashScope ProviderConfig `json:"dashscope"` Bailian ProviderConfig `json:"bailian"` + Zai ProviderConfig `json:"zai"` + ZaiCoding ProviderConfig `json:"zai_coding"` ClaudeCLI ClaudeCLIConfig `json:"claude_cli"` } @@ -214,6 +216,8 @@ func (c *Config) HasAnyProvider() bool { p.Perplexity.APIKey != "" || p.DashScope.APIKey != "" || p.Bailian.APIKey != "" || + p.Zai.APIKey != "" || + p.ZaiCoding.APIKey != "" || p.ClaudeCLI.CLIPath != "" } diff --git a/internal/config/config_load.go b/internal/config/config_load.go index 32b17236..ff81b4bb 100644 --- a/internal/config/config_load.go +++ b/internal/config/config_load.go @@ -110,6 +110,8 @@ func (c *Config) applyEnvOverrides() { envStr("GOCLAW_PERPLEXITY_API_KEY", &c.Providers.Perplexity.APIKey) envStr("GOCLAW_DASHSCOPE_API_KEY", &c.Providers.DashScope.APIKey) envStr("GOCLAW_BAILIAN_API_KEY", &c.Providers.Bailian.APIKey) + envStr("GOCLAW_ZAI_API_KEY", &c.Providers.Zai.APIKey) + envStr("GOCLAW_ZAI_CODING_API_KEY", &c.Providers.ZaiCoding.APIKey) envStr("GOCLAW_GATEWAY_TOKEN", &c.Gateway.Token) envStr("GOCLAW_TELEGRAM_TOKEN", &c.Channels.Telegram.Token) envStr("GOCLAW_DISCORD_TOKEN", &c.Channels.Discord.Token) diff --git a/internal/config/config_secrets.go b/internal/config/config_secrets.go index b49f3e24..e73b846d 100644 --- a/internal/config/config_secrets.go +++ b/internal/config/config_secrets.go @@ -34,6 +34,8 @@ func (c *Config) MaskedCopy() *Config { maskNonEmpty(&cp.Providers.Perplexity.APIKey) maskNonEmpty(&cp.Providers.DashScope.APIKey) maskNonEmpty(&cp.Providers.Bailian.APIKey) + maskNonEmpty(&cp.Providers.Zai.APIKey) + maskNonEmpty(&cp.Providers.ZaiCoding.APIKey) // Mask gateway token maskNonEmpty(&cp.Gateway.Token) @@ -81,6 +83,8 @@ func (c *Config) StripSecrets() { c.Providers.Perplexity.APIKey = "" c.Providers.DashScope.APIKey = "" c.Providers.Bailian.APIKey = "" + c.Providers.Zai.APIKey = "" + c.Providers.ZaiCoding.APIKey = "" // Gateway token c.Gateway.Token = "" @@ -133,6 +137,8 @@ func (c *Config) StripMaskedSecrets() { stripIfMasked(&c.Providers.Perplexity.APIKey) stripIfMasked(&c.Providers.DashScope.APIKey) stripIfMasked(&c.Providers.Bailian.APIKey) + stripIfMasked(&c.Providers.Zai.APIKey) + stripIfMasked(&c.Providers.ZaiCoding.APIKey) // Gateway token stripIfMasked(&c.Gateway.Token) diff --git a/internal/store/provider_store.go b/internal/store/provider_store.go index 9ac4ada2..3500fde1 100644 --- a/internal/store/provider_store.go +++ b/internal/store/provider_store.go @@ -26,6 +26,8 @@ const ( ProviderClaudeCLI = "claude_cli" ProviderSuno = "suno" ProviderYesScale = "yescale" + ProviderZai = "zai" + ProviderZaiCoding = "zai_coding" ) // ValidProviderTypes lists all accepted provider_type values. @@ -47,6 +49,8 @@ var ValidProviderTypes = map[string]bool{ ProviderClaudeCLI: true, ProviderSuno: true, ProviderYesScale: true, + ProviderZai: true, + ProviderZaiCoding: true, } // LLMProviderData represents an LLM provider configuration. diff --git a/ui/web/src/constants/providers.ts b/ui/web/src/constants/providers.ts index 3453a3fa..8d167264 100644 --- a/ui/web/src/constants/providers.ts +++ b/ui/web/src/constants/providers.ts @@ -21,5 +21,7 @@ export const PROVIDER_TYPES: ProviderTypeInfo[] = [ { value: "dashscope", label: "DashScope (Qwen)", apiBase: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", placeholder: "" }, { value: "bailian", label: "Bailian Coding", apiBase: "https://coding-intl.dashscope.aliyuncs.com/v1", placeholder: "" }, { value: "yescale", label: "YesScale", apiBase: "https://api.yescale.one/v1", placeholder: "" }, + { value: "zai", label: "Z.ai API", apiBase: "https://api.z.ai/api/paas/v4", placeholder: "" }, + { value: "zai_coding", label: "Z.ai Coding Plan", apiBase: "https://api.z.ai/api/coding/paas/v4", placeholder: "" }, { value: "claude_cli", label: "Claude CLI (Local)", apiBase: "", placeholder: "" }, ];