From 51df39b9b9246ecc0c914e7c4080815aa9a9e31c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=8E=E6=B0=B4?= Date: Tue, 2 Jun 2026 23:39:56 +0800 Subject: [PATCH] feat(ai): add ZAI Coding Plan China provider --- packages/ai/README.md | 2 + packages/ai/scripts/generate-models.ts | 61 ++++++------ packages/ai/src/env-api-keys.ts | 1 + packages/ai/src/models.generated.ts | 92 +++++++++++++++++++ .../ai/src/providers/openai-completions.ts | 6 +- packages/ai/src/types.ts | 1 + packages/ai/test/env-api-keys.test.ts | 14 +++ packages/coding-agent/README.md | 1 + packages/coding-agent/docs/providers.md | 1 + packages/coding-agent/src/cli/args.ts | 1 + .../coding-agent/src/core/model-resolver.ts | 1 + .../src/core/provider-display-names.ts | 1 + test.sh | 1 + 13 files changed, 155 insertions(+), 28 deletions(-) diff --git a/packages/ai/README.md b/packages/ai/README.md index 8f65bc727..6be702122 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -67,6 +67,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an - **xAI** - **OpenRouter** - **Vercel AI Gateway** +- **ZAI** (with separate Coding Plan China provider) - **MiniMax** - **Together AI** - **GitHub Copilot** (requires OAuth, see below) @@ -1119,6 +1120,7 @@ In Node.js environments, you can set environment variables to avoid passing API | OpenRouter | `OPENROUTER_API_KEY` | | Vercel AI Gateway | `AI_GATEWAY_API_KEY` | | zAI | `ZAI_API_KEY` | +| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` | | MiniMax | `MINIMAX_API_KEY` | | OpenCode Zen / OpenCode Go | `OPENCODE_API_KEY` | | Kimi For Coding | `KIMI_API_KEY` | diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index c98f868cd..a59911e54 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -788,34 +788,41 @@ async function loadModelsDevData(): Promise[]> { } // Process zAi models - if (data["zai-coding-plan"]?.models) { - for (const [modelId, model] of Object.entries(data["zai-coding-plan"].models)) { - const m = model as ModelsDevModel; - if (m.tool_call !== true) continue; - const supportsImage = m.modalities?.input?.includes("image"); + const zaiCodingPlanVariants = [ + { provider: "zai", baseUrl: "https://api.z.ai/api/coding/paas/v4" }, + { provider: "zai-coding-cn", baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4" }, + ] as const; - models.push({ - id: modelId, - name: m.name || modelId, - api: "openai-completions", - provider: "zai", - baseUrl: "https://api.z.ai/api/coding/paas/v4", - reasoning: m.reasoning === true, - input: supportsImage ? ["text", "image"] : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, - compat: { - supportsDeveloperRole: false, - thinkingFormat: "zai", - ...(!ZAI_TOOL_STREAM_UNSUPPORTED_MODELS.has(modelId) ? { zaiToolStream: true } : {}), - }, - contextWindow: m.limit?.context || 4096, - maxTokens: m.limit?.output || 4096, - }); + if (data["zai-coding-plan"]?.models) { + for (const { provider, baseUrl } of zaiCodingPlanVariants) { + for (const [modelId, model] of Object.entries(data["zai-coding-plan"].models)) { + const m = model as ModelsDevModel; + if (m.tool_call !== true) continue; + const supportsImage = m.modalities?.input?.includes("image"); + + models.push({ + id: modelId, + name: m.name || modelId, + api: "openai-completions", + provider, + baseUrl, + reasoning: m.reasoning === true, + input: supportsImage ? ["text", "image"] : ["text"], + cost: { + input: m.cost?.input || 0, + output: m.cost?.output || 0, + cacheRead: m.cost?.cache_read || 0, + cacheWrite: m.cost?.cache_write || 0, + }, + compat: { + supportsDeveloperRole: false, + thinkingFormat: "zai", + ...(!ZAI_TOOL_STREAM_UNSUPPORTED_MODELS.has(modelId) ? { zaiToolStream: true } : {}), + }, + contextWindow: m.limit?.context || 4096, + maxTokens: m.limit?.output || 4096, + }); + } } } diff --git a/packages/ai/src/env-api-keys.ts b/packages/ai/src/env-api-keys.ts index 510f3b387..291d2288f 100644 --- a/packages/ai/src/env-api-keys.ts +++ b/packages/ai/src/env-api-keys.ts @@ -112,6 +112,7 @@ function getApiKeyEnvVars(provider: string): readonly string[] | undefined { openrouter: "OPENROUTER_API_KEY", "vercel-ai-gateway": "AI_GATEWAY_API_KEY", zai: "ZAI_API_KEY", + "zai-coding-cn": "ZAI_CODING_CN_API_KEY", mistral: "MISTRAL_API_KEY", minimax: "MINIMAX_API_KEY", "minimax-cn": "MINIMAX_CN_API_KEY", diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 54acc9529..8dd246de2 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -16778,4 +16778,96 @@ export const MODELS = { maxTokens: 131072, } satisfies Model<"openai-completions">, }, + "zai-coding-cn": { + "glm-4.5-air": { + id: "glm-4.5-air", + name: "GLM-4.5-Air", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai"}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 131072, + maxTokens: 98304, + } satisfies Model<"openai-completions">, + "glm-4.7": { + id: "glm-4.7", + name: "GLM-4.7", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 204800, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5-turbo": { + id: "glm-5-turbo", + name: "GLM-5-Turbo", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5.1": { + id: "glm-5.1", + name: "GLM-5.1", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + "glm-5v-turbo": { + id: "glm-5v-turbo", + name: "GLM-5V-Turbo", + api: "openai-completions", + provider: "zai-coding-cn", + baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", + compat: {"supportsDeveloperRole":false,"thinkingFormat":"zai","zaiToolStream":true}, + reasoning: true, + input: ["text", "image"], + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + contextWindow: 200000, + maxTokens: 131072, + } satisfies Model<"openai-completions">, + }, } as const; diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 5093d3e88..0cfbc234c 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -1076,7 +1076,11 @@ function detectCompat(model: Model<"openai-completions">): ResolvedOpenAIComplet const provider = model.provider; const baseUrl = model.baseUrl; - const isZai = provider === "zai" || baseUrl.includes("api.z.ai"); + const isZai = + provider === "zai" || + provider === "zai-coding-cn" || + baseUrl.includes("api.z.ai") || + baseUrl.includes("open.bigmodel.cn"); const isTogether = provider === "together" || baseUrl.includes("api.together.ai") || baseUrl.includes("api.together.xyz"); const isMoonshot = provider === "moonshotai" || provider === "moonshotai-cn" || baseUrl.includes("api.moonshot."); diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index cbf136a63..a0564e616 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -38,6 +38,7 @@ export type KnownProvider = | "openrouter" | "vercel-ai-gateway" | "zai" + | "zai-coding-cn" | "mistral" | "minimax" | "minimax-cn" diff --git a/packages/ai/test/env-api-keys.test.ts b/packages/ai/test/env-api-keys.test.ts index 61fec9128..b7c864329 100644 --- a/packages/ai/test/env-api-keys.test.ts +++ b/packages/ai/test/env-api-keys.test.ts @@ -4,6 +4,7 @@ import { findEnvKeys, getEnvApiKey } from "../src/env-api-keys.ts"; const originalCopilotGitHubToken = process.env.COPILOT_GITHUB_TOKEN; const originalGhToken = process.env.GH_TOKEN; const originalGitHubToken = process.env.GITHUB_TOKEN; +const originalZaiCodingCnApiKey = process.env.ZAI_CODING_CN_API_KEY; afterEach(() => { if (originalCopilotGitHubToken === undefined) { @@ -23,6 +24,12 @@ afterEach(() => { } else { process.env.GITHUB_TOKEN = originalGitHubToken; } + + if (originalZaiCodingCnApiKey === undefined) { + delete process.env.ZAI_CODING_CN_API_KEY; + } else { + process.env.ZAI_CODING_CN_API_KEY = originalZaiCodingCnApiKey; + } }); describe("environment API keys", () => { @@ -43,4 +50,11 @@ describe("environment API keys", () => { expect(findEnvKeys("github-copilot")).toEqual(["COPILOT_GITHUB_TOKEN"]); expect(getEnvApiKey("github-copilot")).toBe("copilot-token"); }); + + it("resolves ZAI China Coding Plan credentials from ZAI_CODING_CN_API_KEY", () => { + process.env.ZAI_CODING_CN_API_KEY = "zai-coding-cn-token"; + + expect(findEnvKeys("zai-coding-cn")).toEqual(["ZAI_CODING_CN_API_KEY"]); + expect(getEnvApiKey("zai-coding-cn")).toBe("zai-coding-cn-token"); + }); }); diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index 8ef336c79..60e8da1a4 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -127,6 +127,7 @@ For each built-in provider, pi maintains a list of tool-capable models, updated - OpenRouter - Vercel AI Gateway - ZAI +- ZAI Coding Plan (China) - OpenCode Zen - OpenCode Go - Hugging Face diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index 0ecf439df..185405cd1 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -64,6 +64,7 @@ pi | OpenRouter | `OPENROUTER_API_KEY` | `openrouter` | | Vercel AI Gateway | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway` | | ZAI | `ZAI_API_KEY` | `zai` | +| ZAI Coding Plan (China) | `ZAI_CODING_CN_API_KEY` | `zai-coding-cn` | | OpenCode Zen | `OPENCODE_API_KEY` | `opencode` | | OpenCode Go | `OPENCODE_API_KEY` | `opencode-go` | | Hugging Face | `HF_TOKEN` | `huggingface` | diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 458bd43ee..959484daf 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -346,6 +346,7 @@ ${chalk.bold("Environment Variables:")} OPENROUTER_API_KEY - OpenRouter API key AI_GATEWAY_API_KEY - Vercel AI Gateway API key ZAI_API_KEY - ZAI API key + ZAI_CODING_CN_API_KEY - ZAI Coding Plan API key (China) MISTRAL_API_KEY - Mistral API key MINIMAX_API_KEY - MiniMax API key MOONSHOT_API_KEY - Moonshot AI API key diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index a00471c9c..53b8ad116 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -29,6 +29,7 @@ export const defaultModelPerProvider: Record = { groq: "openai/gpt-oss-120b", cerebras: "zai-glm-4.7", zai: "glm-5.1", + "zai-coding-cn": "glm-5.1", mistral: "devstral-medium-latest", minimax: "MiniMax-M2.7", "minimax-cn": "MiniMax-M2.7", diff --git a/packages/coding-agent/src/core/provider-display-names.ts b/packages/coding-agent/src/core/provider-display-names.ts index 3481e6aef..9b0371d25 100644 --- a/packages/coding-agent/src/core/provider-display-names.ts +++ b/packages/coding-agent/src/core/provider-display-names.ts @@ -27,6 +27,7 @@ export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record = { "vercel-ai-gateway": "Vercel AI Gateway", xai: "xAI", zai: "ZAI", + "zai-coding-cn": "ZAI Coding Plan (China)", xiaomi: "Xiaomi MiMo", "xiaomi-token-plan-cn": "Xiaomi MiMo Token Plan (China)", "xiaomi-token-plan-ams": "Xiaomi MiMo Token Plan (Amsterdam)", diff --git a/test.sh b/test.sh index 5f231845b..9a553f6fc 100755 --- a/test.sh +++ b/test.sh @@ -35,6 +35,7 @@ unset CEREBRAS_API_KEY unset XAI_API_KEY unset OPENROUTER_API_KEY unset ZAI_API_KEY +unset ZAI_CODING_CN_API_KEY unset MISTRAL_API_KEY unset MINIMAX_API_KEY unset MINIMAX_CN_API_KEY