From 719ab3b4c5fd4aceac9d299e28007c516de0e467 Mon Sep 17 00:00:00 2001 From: Kyle Howells Date: Thu, 29 Jan 2026 16:18:51 +0000 Subject: [PATCH 1/6] feat(providers): add Z.AI/Zhipu GLM multi-configuration support Add four distinct provider variants for GLM models: - zai: International pay-as-you-go (api.z.ai) - zai-coding: International Coding Plan (api.z.ai/coding) - zhipu: China pay-as-you-go (bigmodel.cn) - zhipu-coding: China Coding Plan (bigmodel.cn/coding) Includes: - Provider config functions with proper base URLs - GLM model definition (glm-4.7, 205K context) - Env var resolution with fallback chains - Provider ID normalization for aliases - Usage provider IDs and labels Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- src/agents/model-auth.ts | 14 ++ src/agents/model-compat.ts | 11 +- src/agents/model-selection.ts | 5 + src/commands/onboard-auth.config-core.ts | 247 ++++++++++++++++++++++- src/commands/onboard-auth.credentials.ts | 42 ++++ src/commands/onboard-auth.models.ts | 23 +++ src/commands/onboard-auth.ts | 13 ++ src/infra/provider-usage.shared.ts | 8 +- src/infra/provider-usage.types.ts | 5 +- 9 files changed, 362 insertions(+), 6 deletions(-) diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 96e4e4ae6..69b8ef298 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -255,6 +255,20 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { return pick("ZAI_API_KEY") ?? pick("Z_AI_API_KEY"); } + if (normalized === "zai-coding") { + // Coding plan can use same key as general, just different endpoint + return pick("ZAI_CODING_API_KEY") ?? pick("ZAI_API_KEY") ?? pick("Z_AI_API_KEY"); + } + + if (normalized === "zhipu") { + return pick("ZHIPU_API_KEY"); + } + + if (normalized === "zhipu-coding") { + // Coding plan can use same key as general, just different endpoint + return pick("ZHIPU_CODING_API_KEY") ?? pick("ZHIPU_API_KEY"); + } + if (normalized === "google-vertex") { const envKey = getEnvApiKey(normalized); if (!envKey) return null; diff --git a/src/agents/model-compat.ts b/src/agents/model-compat.ts index 741819073..b1fc1632d 100644 --- a/src/agents/model-compat.ts +++ b/src/agents/model-compat.ts @@ -6,8 +6,15 @@ function isOpenAiCompletionsModel(model: Model): model is Model<"openai-com export function normalizeModelCompat(model: Model): Model { const baseUrl = model.baseUrl ?? ""; - const isZai = model.provider === "zai" || baseUrl.includes("api.z.ai"); - if (!isZai || !isOpenAiCompletionsModel(model)) return model; + // All GLM providers (Z.AI international and Zhipu AI China) share the same compatibility + const isGlm = + model.provider === "zai" || + model.provider === "zai-coding" || + model.provider === "zhipu" || + model.provider === "zhipu-coding" || + baseUrl.includes("api.z.ai") || + baseUrl.includes("bigmodel.cn"); + if (!isGlm || !isOpenAiCompletionsModel(model)) return model; const openaiModel = model as Model<"openai-completions">; const compat = openaiModel.compat ?? undefined; diff --git a/src/agents/model-selection.ts b/src/agents/model-selection.ts index 8d6db36de..11ef6d746 100644 --- a/src/agents/model-selection.ts +++ b/src/agents/model-selection.ts @@ -26,7 +26,12 @@ export function modelKey(provider: string, model: string) { export function normalizeProviderId(provider: string): string { const normalized = provider.trim().toLowerCase(); + // Z.AI international variants if (normalized === "z.ai" || normalized === "z-ai") return "zai"; + if (normalized === "z.ai-coding" || normalized === "z-ai-coding") return "zai-coding"; + // Zhipu AI (China) variants + if (normalized === "zhipuai" || normalized === "zhipu-ai") return "zhipu"; + if (normalized === "zhipuai-coding" || normalized === "zhipu-ai-coding") return "zhipu-coding"; if (normalized === "opencode-zen") return "opencode"; if (normalized === "qwen") return "qwen-portal"; return normalized; diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 921ee01d1..79a435529 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -14,11 +14,22 @@ import type { MoltbotConfig } from "../config/config.js"; import { OPENROUTER_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, + ZAI_CODING_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, + ZHIPU_CODING_DEFAULT_MODEL_REF, + ZHIPU_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; + +// Z.AI / Zhipu AI base URLs +export const ZAI_BASE_URL = "https://api.z.ai/api/paas/v4"; +export const ZAI_CODING_BASE_URL = "https://api.z.ai/api/coding/paas/v4"; +export const ZHIPU_BASE_URL = "https://open.bigmodel.cn/api/paas/v4"; +export const ZHIPU_CODING_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4"; import { + buildGlmModelDefinition, buildKimiCodeModelDefinition, buildMoonshotModelDefinition, + GLM_DEFAULT_MODEL_ID, KIMI_CODE_BASE_URL, KIMI_CODE_MODEL_ID, KIMI_CODE_MODEL_REF, @@ -27,14 +38,33 @@ import { MOONSHOT_DEFAULT_MODEL_REF, } from "./onboard-auth.models.js"; -export function applyZaiConfig(cfg: MoltbotConfig): MoltbotConfig { +export function applyZaiProviderConfig(cfg: MoltbotConfig): MoltbotConfig { const models = { ...cfg.agents?.defaults?.models }; models[ZAI_DEFAULT_MODEL_REF] = { ...models[ZAI_DEFAULT_MODEL_REF], alias: models[ZAI_DEFAULT_MODEL_REF]?.alias ?? "GLM", }; - const existingModel = cfg.agents?.defaults?.model; + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.zai; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const defaultModel = buildGlmModelDefinition(); + const hasDefaultModel = existingModels.some((model) => model.id === GLM_DEFAULT_MODEL_ID); + const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.zai = { + ...existingProviderRest, + baseUrl: ZAI_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : [defaultModel], + }; + return { ...cfg, agents: { @@ -42,6 +72,24 @@ export function applyZaiConfig(cfg: MoltbotConfig): MoltbotConfig { defaults: { ...cfg.agents?.defaults, models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyZaiConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyZaiProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, model: { ...(existingModel && "fallbacks" in (existingModel as Record) ? { @@ -55,6 +103,201 @@ export function applyZaiConfig(cfg: MoltbotConfig): MoltbotConfig { }; } +export function applyZaiCodingProviderConfig(cfg: MoltbotConfig): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[ZAI_CODING_DEFAULT_MODEL_REF] = { + ...models[ZAI_CODING_DEFAULT_MODEL_REF], + alias: models[ZAI_CODING_DEFAULT_MODEL_REF]?.alias ?? "GLM Coding", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers["zai-coding"]; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const defaultModel = buildGlmModelDefinition(); + const hasDefaultModel = existingModels.some((model) => model.id === GLM_DEFAULT_MODEL_ID); + const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers["zai-coding"] = { + ...existingProviderRest, + baseUrl: ZAI_CODING_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : [defaultModel], + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyZaiCodingConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyZaiCodingProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: ZAI_CODING_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + +export function applyZhipuProviderConfig(cfg: MoltbotConfig): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[ZHIPU_DEFAULT_MODEL_REF] = { + ...models[ZHIPU_DEFAULT_MODEL_REF], + alias: models[ZHIPU_DEFAULT_MODEL_REF]?.alias ?? "GLM", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.zhipu; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const defaultModel = buildGlmModelDefinition(); + const hasDefaultModel = existingModels.some((model) => model.id === GLM_DEFAULT_MODEL_ID); + const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.zhipu = { + ...existingProviderRest, + baseUrl: ZHIPU_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : [defaultModel], + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyZhipuConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyZhipuProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: ZHIPU_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + +export function applyZhipuCodingProviderConfig(cfg: MoltbotConfig): MoltbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[ZHIPU_CODING_DEFAULT_MODEL_REF] = { + ...models[ZHIPU_CODING_DEFAULT_MODEL_REF], + alias: models[ZHIPU_CODING_DEFAULT_MODEL_REF]?.alias ?? "GLM Coding", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers["zhipu-coding"]; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const defaultModel = buildGlmModelDefinition(); + const hasDefaultModel = existingModels.some((model) => model.id === GLM_DEFAULT_MODEL_ID); + const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers["zhipu-coding"] = { + ...existingProviderRest, + baseUrl: ZHIPU_CODING_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : [defaultModel], + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyZhipuCodingConfig(cfg: MoltbotConfig): MoltbotConfig { + const next = applyZhipuCodingProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: ZHIPU_CODING_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyOpenrouterProviderConfig(cfg: MoltbotConfig): MoltbotConfig { const models = { ...cfg.agents?.defaults?.models }; models[OPENROUTER_DEFAULT_MODEL_REF] = { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index b2fb58542..d62f7776f 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -113,6 +113,9 @@ export async function setVeniceApiKey(key: string, agentDir?: string) { } export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; +export const ZAI_CODING_DEFAULT_MODEL_REF = "zai-coding/glm-4.7"; +export const ZHIPU_DEFAULT_MODEL_REF = "zhipu/glm-4.7"; +export const ZHIPU_CODING_DEFAULT_MODEL_REF = "zhipu-coding/glm-4.7"; export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.5"; @@ -129,6 +132,45 @@ export async function setZaiApiKey(key: string, agentDir?: string) { }); } +export async function setZaiCodingApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "zai-coding:default", + credential: { + type: "api_key", + provider: "zai-coding", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + +export async function setZhipuApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "zhipu:default", + credential: { + type: "api_key", + provider: "zhipu", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + +export async function setZhipuCodingApiKey(key: string, agentDir?: string) { + // Write to resolved agent dir so gateway finds credentials on startup. + upsertAuthProfile({ + profileId: "zhipu-coding:default", + credential: { + type: "api_key", + provider: "zhipu-coding", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + export async function setOpenrouterApiKey(key: string, agentDir?: string) { upsertAuthProfile({ profileId: "openrouter:default", diff --git a/src/commands/onboard-auth.models.ts b/src/commands/onboard-auth.models.ts index de5a4edaa..85c92c062 100644 --- a/src/commands/onboard-auth.models.ts +++ b/src/commands/onboard-auth.models.ts @@ -20,6 +20,11 @@ export const KIMI_CODE_MAX_TOKENS = 32768; export const KIMI_CODE_HEADERS = { "User-Agent": "KimiCLI/0.77" } as const; export const KIMI_CODE_COMPAT = { supportsDeveloperRole: false } as const; +// GLM (Z.AI / Zhipu AI) models +export const GLM_DEFAULT_MODEL_ID = "glm-4.7"; +export const GLM_DEFAULT_CONTEXT_WINDOW = 205000; +export const GLM_DEFAULT_MAX_TOKENS = 8192; + // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. export const MINIMAX_API_COST = { input: 15, @@ -51,6 +56,12 @@ export const KIMI_CODE_DEFAULT_COST = { cacheRead: 0, cacheWrite: 0, }; +export const GLM_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; const MINIMAX_MODEL_CATALOG = { "MiniMax-M2.1": { name: "MiniMax M2.1", reasoning: false }, @@ -116,3 +127,15 @@ export function buildKimiCodeModelDefinition(): ModelDefinitionConfig { compat: KIMI_CODE_COMPAT, }; } + +export function buildGlmModelDefinition(): ModelDefinitionConfig { + return { + id: GLM_DEFAULT_MODEL_ID, + name: "GLM 4.7", + reasoning: false, + input: ["text"], + cost: GLM_DEFAULT_COST, + contextWindow: GLM_DEFAULT_CONTEXT_WINDOW, + maxTokens: GLM_DEFAULT_MAX_TOKENS, + }; +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index b122d89cf..a0662fee7 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -17,7 +17,14 @@ export { applyVeniceProviderConfig, applyVercelAiGatewayConfig, applyVercelAiGatewayProviderConfig, + applyZaiCodingConfig, + applyZaiCodingProviderConfig, applyZaiConfig, + applyZaiProviderConfig, + applyZhipuCodingConfig, + applyZhipuCodingProviderConfig, + applyZhipuConfig, + applyZhipuProviderConfig, } from "./onboard-auth.config-core.js"; export { applyMinimaxApiConfig, @@ -45,9 +52,15 @@ export { setVeniceApiKey, setVercelAiGatewayApiKey, setZaiApiKey, + setZaiCodingApiKey, + setZhipuApiKey, + setZhipuCodingApiKey, writeOAuthCredentials, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, + ZAI_CODING_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, + ZHIPU_CODING_DEFAULT_MODEL_REF, + ZHIPU_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; export { buildKimiCodeModelDefinition, diff --git a/src/infra/provider-usage.shared.ts b/src/infra/provider-usage.shared.ts index 6c8c1d9bb..3a1b3c11e 100644 --- a/src/infra/provider-usage.shared.ts +++ b/src/infra/provider-usage.shared.ts @@ -10,7 +10,10 @@ export const PROVIDER_LABELS: Record = { "google-antigravity": "Antigravity", minimax: "MiniMax", "openai-codex": "Codex", - zai: "z.ai", + zai: "Z.AI", + "zai-coding": "Z.AI Coding", + zhipu: "Zhipu AI", + "zhipu-coding": "Zhipu AI Coding", }; export const usageProviders: UsageProviderId[] = [ @@ -21,6 +24,9 @@ export const usageProviders: UsageProviderId[] = [ "minimax", "openai-codex", "zai", + "zai-coding", + "zhipu", + "zhipu-coding", ]; export function resolveUsageProviderId(provider?: string | null): UsageProviderId | undefined { diff --git a/src/infra/provider-usage.types.ts b/src/infra/provider-usage.types.ts index cef446ceb..ab01b80a1 100644 --- a/src/infra/provider-usage.types.ts +++ b/src/infra/provider-usage.types.ts @@ -24,4 +24,7 @@ export type UsageProviderId = | "google-antigravity" | "minimax" | "openai-codex" - | "zai"; + | "zai" + | "zai-coding" + | "zhipu" + | "zhipu-coding"; From 33d8f6d6f1d8620fec658ea60326644ca5e8d9ad Mon Sep 17 00:00:00 2001 From: Kyle Howells Date: Thu, 29 Jan 2026 16:19:03 +0000 Subject: [PATCH 2/6] feat(onboard): add CLI flags and auth handlers for Z.AI/Zhipu variants Add onboarding support for all four GLM provider variants: - CLI flags: --zai-coding-api-key, --zhipu-api-key, --zhipu-coding-api-key - Auth choice menu options with regional grouping - Interactive and non-interactive auth handlers - hasCredential guards for proper token/env var precedence - OAuth rejection for non-API-key auth methods Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- src/cli/program/register.onboard.ts | 8 +- src/commands/auth-choice-options.ts | 26 +- .../auth-choice.apply.api-providers.ts | 326 +++++++++++++----- .../local/auth-choice.ts | 69 +++- src/commands/onboard-types.ts | 6 + 5 files changed, 350 insertions(+), 85 deletions(-) diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 8f31635f0..699c418c7 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|zai-coding-api-key|zhipu-api-key|zhipu-coding-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", ) .option( "--token-provider ", @@ -72,6 +72,9 @@ export function registerOnboardCommand(program: Command) { .option("--kimi-code-api-key ", "Kimi Code API key") .option("--gemini-api-key ", "Gemini API key") .option("--zai-api-key ", "Z.AI API key") + .option("--zai-coding-api-key ", "Z.AI Coding Plan API key") + .option("--zhipu-api-key ", "Zhipu AI API key") + .option("--zhipu-coding-api-key ", "Zhipu AI Coding Plan API key") .option("--minimax-api-key ", "MiniMax API key") .option("--synthetic-api-key ", "Synthetic API key") .option("--venice-api-key ", "Venice API key") @@ -122,6 +125,9 @@ export function registerOnboardCommand(program: Command) { kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined, geminiApiKey: opts.geminiApiKey as string | undefined, zaiApiKey: opts.zaiApiKey as string | undefined, + zaiCodingApiKey: opts.zaiCodingApiKey as string | undefined, + zhipuApiKey: opts.zhipuApiKey as string | undefined, + zhipuCodingApiKey: opts.zhipuCodingApiKey as string | undefined, minimaxApiKey: opts.minimaxApiKey as string | undefined, syntheticApiKey: opts.syntheticApiKey as string | undefined, veniceApiKey: opts.veniceApiKey as string | undefined, diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 6b49ff17b..8b8f5ee77 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -16,6 +16,7 @@ export type AuthChoiceGroupId = | "ai-gateway" | "moonshot" | "zai" + | "zhipu" | "opencode-zen" | "minimax" | "synthetic" @@ -103,9 +104,15 @@ const AUTH_CHOICE_GROUP_DEFS: { }, { value: "zai", - label: "Z.AI (GLM 4.7)", - hint: "API key", - choices: ["zai-api-key"], + label: "Z.AI (International)", + hint: "GLM models via api.z.ai", + choices: ["zai-api-key", "zai-coding-api-key"], + }, + { + value: "zhipu", + label: "Zhipu AI (China)", + hint: "GLM models via bigmodel.cn", + choices: ["zhipu-api-key", "zhipu-coding-api-key"], }, { value: "opencode-zen", @@ -163,7 +170,18 @@ export function buildAuthChoiceOptions(params: { label: "Google Gemini CLI OAuth", hint: "Uses the bundled Gemini CLI auth plugin", }); - options.push({ value: "zai-api-key", label: "Z.AI (GLM 4.7) API key" }); + options.push({ value: "zai-api-key", label: "Z.AI API key (pay-as-you-go)" }); + options.push({ + value: "zai-coding-api-key", + label: "Z.AI Coding Plan API key", + hint: "Subscription-based, optimized for coding tools", + }); + options.push({ value: "zhipu-api-key", label: "Zhipu AI API key (pay-as-you-go)" }); + options.push({ + value: "zhipu-coding-api-key", + label: "Zhipu AI Coding Plan API key", + hint: "China mainland, subscription-based", + }); options.push({ value: "qwen-portal", label: "Qwen OAuth" }); options.push({ value: "copilot-proxy", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 8be02008b..467838626 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -27,7 +27,14 @@ import { applyVeniceProviderConfig, applyVercelAiGatewayConfig, applyVercelAiGatewayProviderConfig, + applyZaiCodingConfig, + applyZaiCodingProviderConfig, applyZaiConfig, + applyZaiProviderConfig, + applyZhipuCodingConfig, + applyZhipuCodingProviderConfig, + applyZhipuConfig, + applyZhipuProviderConfig, KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, @@ -43,7 +50,13 @@ import { setVeniceApiKey, setVercelAiGatewayApiKey, setZaiApiKey, + setZaiCodingApiKey, + setZhipuApiKey, + setZhipuCodingApiKey, + ZAI_CODING_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, + ZHIPU_CODING_DEFAULT_MODEL_REF, + ZHIPU_DEFAULT_MODEL_REF, } from "./onboard-auth.js"; import { OPENCODE_ZEN_DEFAULT_MODEL } from "./opencode-zen-model-default.js"; @@ -79,6 +92,12 @@ export async function applyAuthChoiceApiProviders( authChoice = "gemini-api-key"; } else if (params.opts.tokenProvider === "zai") { authChoice = "zai-api-key"; + } else if (params.opts.tokenProvider === "zai-coding") { + authChoice = "zai-coding-api-key"; + } else if (params.opts.tokenProvider === "zhipu") { + authChoice = "zhipu-api-key"; + } else if (params.opts.tokenProvider === "zhipu-coding") { + authChoice = "zhipu-coding-api-key"; } else if (params.opts.tokenProvider === "synthetic") { authChoice = "synthetic-api-key"; } else if (params.opts.tokenProvider === "venice") { @@ -178,15 +197,17 @@ export async function applyAuthChoiceApiProviders( hasCredential = true; } - const envKey = resolveEnvApiKey("vercel-ai-gateway"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - await setVercelAiGatewayApiKey(envKey.apiKey, params.agentDir); - hasCredential = true; + if (!hasCredential) { + const envKey = resolveEnvApiKey("vercel-ai-gateway"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setVercelAiGatewayApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } } } if (!hasCredential) { @@ -226,15 +247,17 @@ export async function applyAuthChoiceApiProviders( hasCredential = true; } - const envKey = resolveEnvApiKey("moonshot"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing MOONSHOT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - await setMoonshotApiKey(envKey.apiKey, params.agentDir); - hasCredential = true; + if (!hasCredential) { + const envKey = resolveEnvApiKey("moonshot"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing MOONSHOT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setMoonshotApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } } } if (!hasCredential) { @@ -281,15 +304,17 @@ export async function applyAuthChoiceApiProviders( "Kimi Code", ); } - const envKey = resolveEnvApiKey("kimi-code"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing KIMICODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - await setKimiCodeApiKey(envKey.apiKey, params.agentDir); - hasCredential = true; + if (!hasCredential) { + const envKey = resolveEnvApiKey("kimi-code"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing KIMICODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setKimiCodeApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } } } if (!hasCredential) { @@ -329,15 +354,17 @@ export async function applyAuthChoiceApiProviders( hasCredential = true; } - const envKey = resolveEnvApiKey("google"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing GEMINI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - await setGeminiApiKey(envKey.apiKey, params.agentDir); - hasCredential = true; + if (!hasCredential) { + const envKey = resolveEnvApiKey("google"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing GEMINI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setGeminiApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } } } if (!hasCredential) { @@ -376,15 +403,17 @@ export async function applyAuthChoiceApiProviders( hasCredential = true; } - const envKey = resolveEnvApiKey("zai"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing ZAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - await setZaiApiKey(envKey.apiKey, params.agentDir); - hasCredential = true; + if (!hasCredential) { + const envKey = resolveEnvApiKey("zai"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing ZAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setZaiApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } } } if (!hasCredential) { @@ -405,22 +434,7 @@ export async function applyAuthChoiceApiProviders( setDefaultModel: params.setDefaultModel, defaultModel: ZAI_DEFAULT_MODEL_REF, applyDefaultConfig: applyZaiConfig, - applyProviderConfig: (config) => ({ - ...config, - agents: { - ...config.agents, - defaults: { - ...config.agents?.defaults, - models: { - ...config.agents?.defaults?.models, - [ZAI_DEFAULT_MODEL_REF]: { - ...config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF], - alias: config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF]?.alias ?? "GLM", - }, - }, - }, - }, - }), + applyProviderConfig: applyZaiProviderConfig, noteDefault: ZAI_DEFAULT_MODEL_REF, noteAgentModel, prompter: params.prompter, @@ -431,6 +445,156 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "zai-coding-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "zai-coding") { + await setZaiCodingApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + const envKey = resolveEnvApiKey("zai-coding"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing API key (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setZaiCodingApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Z.AI Coding Plan API key", + validate: validateApiKeyInput, + }); + await setZaiCodingApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "zai-coding:default", + provider: "zai-coding", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: ZAI_CODING_DEFAULT_MODEL_REF, + applyDefaultConfig: applyZaiCodingConfig, + applyProviderConfig: applyZaiCodingProviderConfig, + noteDefault: ZAI_CODING_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + + if (authChoice === "zhipu-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "zhipu") { + await setZhipuApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + const envKey = resolveEnvApiKey("zhipu"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing ZHIPU_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setZhipuApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Zhipu AI API key", + validate: validateApiKeyInput, + }); + await setZhipuApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "zhipu:default", + provider: "zhipu", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: ZHIPU_DEFAULT_MODEL_REF, + applyDefaultConfig: applyZhipuConfig, + applyProviderConfig: applyZhipuProviderConfig, + noteDefault: ZHIPU_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + + if (authChoice === "zhipu-coding-api-key") { + let hasCredential = false; + + if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "zhipu-coding") { + await setZhipuCodingApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + const envKey = resolveEnvApiKey("zhipu-coding"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing API key (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setZhipuCodingApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Zhipu AI Coding Plan API key", + validate: validateApiKeyInput, + }); + await setZhipuCodingApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "zhipu-coding:default", + provider: "zhipu-coding", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: ZHIPU_CODING_DEFAULT_MODEL_REF, + applyDefaultConfig: applyZhipuCodingConfig, + applyProviderConfig: applyZhipuCodingProviderConfig, + noteDefault: ZHIPU_CODING_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + if (authChoice === "synthetic-api-key") { if (params.opts?.token && params.opts?.tokenProvider === "synthetic") { await setSyntheticApiKey(String(params.opts.token).trim(), params.agentDir); @@ -482,15 +646,17 @@ export async function applyAuthChoiceApiProviders( ); } - const envKey = resolveEnvApiKey("venice"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing VENICE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - await setVeniceApiKey(envKey.apiKey, params.agentDir); - hasCredential = true; + if (!hasCredential) { + const envKey = resolveEnvApiKey("venice"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing VENICE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setVeniceApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } } } if (!hasCredential) { @@ -539,15 +705,17 @@ export async function applyAuthChoiceApiProviders( "OpenCode Zen", ); } - const envKey = resolveEnvApiKey("opencode"); - if (envKey) { - const useExisting = await params.prompter.confirm({ - message: `Use existing OPENCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, - initialValue: true, - }); - if (useExisting) { - await setOpencodeZenApiKey(envKey.apiKey, params.agentDir); - hasCredential = true; + if (!hasCredential) { + const envKey = resolveEnvApiKey("opencode"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing OPENCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setOpencodeZenApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } } } if (!hasCredential) { diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 7d952730c..ca3216774 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -17,7 +17,10 @@ import { applySyntheticConfig, applyVeniceConfig, applyVercelAiGatewayConfig, + applyZaiCodingConfig, applyZaiConfig, + applyZhipuCodingConfig, + applyZhipuConfig, setAnthropicApiKey, setGeminiApiKey, setKimiCodeApiKey, @@ -29,6 +32,9 @@ import { setVeniceApiKey, setVercelAiGatewayApiKey, setZaiApiKey, + setZaiCodingApiKey, + setZhipuApiKey, + setZhipuCodingApiKey, } from "../../onboard-auth.js"; import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; import { resolveNonInteractiveApiKey } from "../api-keys.js"; @@ -177,6 +183,63 @@ export async function applyNonInteractiveAuthChoice(params: { return applyZaiConfig(nextConfig); } + if (authChoice === "zai-coding-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "zai-coding", + cfg: baseConfig, + flagValue: opts.zaiCodingApiKey ?? opts.zaiApiKey, + flagName: "--zai-coding-api-key", + envVar: "ZAI_CODING_API_KEY (or ZAI_API_KEY)", + runtime, + }); + if (!resolved) return null; + if (resolved.source !== "profile") await setZaiCodingApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "zai-coding:default", + provider: "zai-coding", + mode: "api_key", + }); + return applyZaiCodingConfig(nextConfig); + } + + if (authChoice === "zhipu-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "zhipu", + cfg: baseConfig, + flagValue: opts.zhipuApiKey, + flagName: "--zhipu-api-key", + envVar: "ZHIPU_API_KEY", + runtime, + }); + if (!resolved) return null; + if (resolved.source !== "profile") await setZhipuApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "zhipu:default", + provider: "zhipu", + mode: "api_key", + }); + return applyZhipuConfig(nextConfig); + } + + if (authChoice === "zhipu-coding-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "zhipu-coding", + cfg: baseConfig, + flagValue: opts.zhipuCodingApiKey ?? opts.zhipuApiKey, + flagName: "--zhipu-coding-api-key", + envVar: "ZHIPU_CODING_API_KEY (or ZHIPU_API_KEY)", + runtime, + }); + if (!resolved) return null; + if (resolved.source !== "profile") await setZhipuCodingApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "zhipu-coding:default", + provider: "zhipu-coding", + mode: "api_key", + }); + return applyZhipuCodingConfig(nextConfig); + } + if (authChoice === "openai-api-key") { const resolved = await resolveNonInteractiveApiKey({ provider: "openai", @@ -359,7 +422,11 @@ export async function applyNonInteractiveAuthChoice(params: { authChoice === "oauth" || authChoice === "chutes" || authChoice === "openai-codex" || - authChoice === "qwen-portal" + authChoice === "qwen-portal" || + authChoice === "github-copilot" || + authChoice === "google-gemini-cli" || + authChoice === "google-antigravity" || + authChoice === "copilot-proxy" ) { runtime.error("OAuth requires interactive mode."); runtime.exit(1); diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index aa1d9afe0..83fa8dc9d 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -23,6 +23,9 @@ export type AuthChoice = | "google-antigravity" | "google-gemini-cli" | "zai-api-key" + | "zai-coding-api-key" + | "zhipu-api-key" + | "zhipu-coding-api-key" | "minimax-cloud" | "minimax" | "minimax-api" @@ -67,6 +70,9 @@ export type OnboardOptions = { kimiCodeApiKey?: string; geminiApiKey?: string; zaiApiKey?: string; + zaiCodingApiKey?: string; + zhipuApiKey?: string; + zhipuCodingApiKey?: string; minimaxApiKey?: string; syntheticApiKey?: string; veniceApiKey?: string; From 61d9798b6b8ec277d563397d0a49ece6a8096624 Mon Sep 17 00:00:00 2001 From: Kyle Howells Date: Thu, 29 Jan 2026 16:19:15 +0000 Subject: [PATCH 3/6] docs(providers): document Z.AI/Zhipu GLM provider variants Update provider documentation with: - Four provider variants table (zai, zai-coding, zhipu, zhipu-coding) - Region and plan type guidance - CLI setup commands for all variants - Environment variable fallback chains - Config snippets for each variant - Important notes on key interchangeability and billing Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- docs/providers/glm.md | 41 +++++++++++--- docs/providers/index.md | 4 +- docs/providers/zai.md | 123 +++++++++++++++++++++++++++++++++++----- 3 files changed, 144 insertions(+), 24 deletions(-) diff --git a/docs/providers/glm.md b/docs/providers/glm.md index 60358a40d..979440b01 100644 --- a/docs/providers/glm.md +++ b/docs/providers/glm.md @@ -4,18 +4,33 @@ read_when: - You want GLM models in Moltbot - You need the model naming convention and setup --- -# GLM models +# GLM Models -GLM is a **model family** (not a company) available through the Z.AI platform. In Moltbot, GLM -models are accessed via the `zai` provider and model IDs like `zai/glm-4.7`. +GLM is a **model family** developed by Zhipu AI. GLM models are available through two platforms: -## CLI setup +- **Z.AI** (api.z.ai) - International platform +- **Zhipu AI** (bigmodel.cn) - China mainland platform + +## Provider Options + +| Provider | Platform | Use Case | +|----------|----------|----------| +| `zai` | Z.AI (International) | Pay-as-you-go | +| `zai-coding` | Z.AI (International) | Coding Plan subscription | +| `zhipu` | Zhipu AI (China) | Pay-as-you-go | +| `zhipu-coding` | Zhipu AI (China) | Coding Plan subscription | + +## CLI Setup ```bash +# International users moltbot onboard --auth-choice zai-api-key + +# China users +moltbot onboard --auth-choice zhipu-api-key ``` -## Config snippet +## Config Snippet ```json5 { @@ -24,8 +39,18 @@ moltbot onboard --auth-choice zai-api-key } ``` +## Available Models + +- `glm-4.7` - Latest flagship model (205K context) +- `glm-4.6` - Previous generation (205K context) +- `glm-4.6v` - Vision model (128K context) +- `glm-4.5` - Balanced performance (131K context) +- `glm-4.5-air` - Lighter variant (131K context) +- `glm-4.5-flash` - Faster variant (131K context) + +Model availability may vary by region; check the platform docs for the latest. + ## Notes -- GLM versions and availability can change; check Z.AI's docs for the latest. -- Example model IDs include `glm-4.7` and `glm-4.6`. -- For provider details, see [/providers/zai](/providers/zai). +- Model IDs follow the pattern `{provider}/glm-{version}` (e.g., `zai/glm-4.7`, `zhipu/glm-4.7`) +- For detailed provider setup, see [/providers/zai](/providers/zai) diff --git a/docs/providers/index.md b/docs/providers/index.md index c18ad70fb..8d4ff63a3 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -41,8 +41,8 @@ See [Venice AI](/providers/venice). - [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot) - [OpenCode Zen](/providers/opencode) - [Amazon Bedrock](/bedrock) -- [Z.AI](/providers/zai) -- [GLM models](/providers/glm) +- [Z.AI / Zhipu AI (GLM models)](/providers/zai) - International + China, pay-as-you-go + Coding Plan +- [GLM models](/providers/glm) - Model family overview - [MiniMax](/providers/minimax) - [Venius (Venice AI, privacy-focused)](/providers/venice) - [Ollama (local models)](/providers/ollama) diff --git a/docs/providers/zai.md b/docs/providers/zai.md index 2ec6c1cc7..674e6e493 100644 --- a/docs/providers/zai.md +++ b/docs/providers/zai.md @@ -1,24 +1,81 @@ --- -summary: "Use Z.AI (GLM models) with Moltbot" +summary: "Use Z.AI / Zhipu AI (GLM models) with Moltbot" read_when: - You want Z.AI / GLM models in Moltbot - - You need a simple ZAI_API_KEY setup + - You need to choose between international and China endpoints + - You have a Coding Plan subscription --- -# Z.AI +# Z.AI / Zhipu AI (GLM Models) -Z.AI is the API platform for **GLM** models. It provides REST APIs for GLM and uses API keys -for authentication. Create your API key in the Z.AI console. Moltbot uses the `zai` provider -with a Z.AI API key. +Z.AI and Zhipu AI provide access to **GLM** models. There are four provider configurations +depending on your region and subscription type. -## CLI setup +## Provider Variants + +| Provider | Region | Plan Type | Base URL | +|----------|--------|-----------|----------| +| `zai` | International | Pay-as-you-go | api.z.ai | +| `zai-coding` | International | Coding Plan | api.z.ai (coding endpoint) | +| `zhipu` | China | Pay-as-you-go | bigmodel.cn | +| `zhipu-coding` | China | Coding Plan | bigmodel.cn (coding endpoint) | + +## Which should I use? + +- **International users with pay-as-you-go**: Use `zai` +- **International users with Coding Plan subscription ($3-15/mo)**: Use `zai-coding` +- **China mainland users with pay-as-you-go**: Use `zhipu` +- **China mainland users with Coding Plan**: Use `zhipu-coding` + +The Coding Plan endpoints are optimized for coding tools and have better tool-calling +performance. They use subscription-based billing rather than per-token billing. + +## CLI Setup ```bash +# International (pay-as-you-go) moltbot onboard --auth-choice zai-api-key -# or non-interactive -moltbot onboard --zai-api-key "$ZAI_API_KEY" + +# International (Coding Plan) +moltbot onboard --auth-choice zai-coding-api-key + +# China (pay-as-you-go) +moltbot onboard --auth-choice zhipu-api-key + +# China (Coding Plan) +moltbot onboard --auth-choice zhipu-coding-api-key ``` -## Config snippet +### Non-interactive + +```bash +# International (pay-as-you-go) +moltbot onboard --non-interactive --auth-choice zai-api-key --zai-api-key "$ZAI_API_KEY" + +# International (Coding Plan) +moltbot onboard --non-interactive --auth-choice zai-coding-api-key --zai-coding-api-key "$ZAI_API_KEY" + +# China (pay-as-you-go) +moltbot onboard --non-interactive --auth-choice zhipu-api-key --zhipu-api-key "$ZHIPU_API_KEY" + +# China (Coding Plan) +moltbot onboard --non-interactive --auth-choice zhipu-coding-api-key --zhipu-coding-api-key "$ZHIPU_API_KEY" +``` + +## Environment Variables + +| Provider | Primary Env Var | Fallback Chain | +|----------|-----------------|----------------| +| `zai` | `ZAI_API_KEY` | `Z_AI_API_KEY` | +| `zai-coding` | `ZAI_CODING_API_KEY` | `ZAI_API_KEY` → `Z_AI_API_KEY` | +| `zhipu` | `ZHIPU_API_KEY` | (none) | +| `zhipu-coding` | `ZHIPU_CODING_API_KEY` | `ZHIPU_API_KEY` | + +The coding providers fall back to their respective general provider's env var, so you can +use a single API key for both if desired. + +## Config Snippets + +### International (pay-as-you-go) ```json5 { @@ -27,8 +84,46 @@ moltbot onboard --zai-api-key "$ZAI_API_KEY" } ``` -## Notes +### International (Coding Plan) -- GLM models are available as `zai/` (example: `zai/glm-4.7`). -- See [/providers/glm](/providers/glm) for the model family overview. -- Z.AI uses Bearer auth with your API key. +```json5 +{ + env: { ZAI_API_KEY: "sk-..." }, + agents: { defaults: { model: { primary: "zai-coding/glm-4.7" } } } +} +``` + +### China (pay-as-you-go) + +```json5 +{ + env: { ZHIPU_API_KEY: "sk-..." }, + agents: { defaults: { model: { primary: "zhipu/glm-4.7" } } } +} +``` + +### China (Coding Plan) + +```json5 +{ + env: { ZHIPU_API_KEY: "sk-..." }, + agents: { defaults: { model: { primary: "zhipu-coding/glm-4.7" } } } +} +``` + +## Important Notes + +- **Same API key, different endpoints**: Your API key works on both general and coding + endpoints, but the billing is different. Using a Coding Plan key on the general + endpoint may return error 1113 ("Insufficient balance"). + +- **Regional keys are not interchangeable**: Keys from z.ai don't work on bigmodel.cn + and vice versa. Create your key on the platform for your region. + +- **Coding endpoint optimized for tools**: The coding endpoints have better + tool-calling performance and are recommended for use with coding assistants. + +- **GLM models**: Models are available as `{provider}/glm-4.7`, `{provider}/glm-4.6`, etc. + See [/providers/glm](/providers/glm) for the model family overview. + +- **Authentication**: All variants use Bearer token auth with your API key. From 17a4345004a3a72818acd0dceb1cb74524cbcf1f Mon Sep 17 00:00:00 2001 From: Kyle Howells Date: Thu, 29 Jan 2026 16:31:36 +0000 Subject: [PATCH 4/6] test(providers): add tests for Z.AI/Zhipu multi-configuration Update existing test files to cover all four GLM provider variants: - auth-choice-options.test.ts: Group test for zai, zai-coding, zhipu, zhipu-coding auth choice menu presence (follows MiniMax/Moonshot pattern) - program.smoke.test.ts: Add CLI flag wiring tests for --zai-coding-api-key, --zhipu-api-key, --zhipu-coding-api-key - model-auth.test.ts: Add env var resolution tests for: - zai-coding with fallback to ZAI_API_KEY - zhipu with ZHIPU_API_KEY - zhipu-coding with fallback to ZHIPU_API_KEY - provider-usage.test.ts: Add test for zhipu bigmodel.cn endpoint (will fail until fetchZhipuUsage is implemented) Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- src/agents/model-auth.test.ts | 126 +++++++++++++++++++++++ src/cli/program.smoke.test.ts | 18 ++++ src/commands/auth-choice-options.test.ts | 7 +- src/infra/provider-usage.test.ts | 43 ++++++++ 4 files changed, 193 insertions(+), 1 deletion(-) diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index 7219e128d..d8445d546 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -233,6 +233,132 @@ describe("getApiKeyForModel", () => { } }); + it("resolves zai-coding API key with fallback to ZAI_API_KEY", async () => { + const previous = { + coding: process.env.ZAI_CODING_API_KEY, + zai: process.env.ZAI_API_KEY, + legacy: process.env.Z_AI_API_KEY, + }; + + try { + delete process.env.ZAI_CODING_API_KEY; + process.env.ZAI_API_KEY = "zai-fallback-key"; + delete process.env.Z_AI_API_KEY; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + const resolved = await resolveApiKeyForProvider({ + provider: "zai-coding", + store: { version: 1, profiles: {} }, + }); + expect(resolved.apiKey).toBe("zai-fallback-key"); + expect(resolved.source).toContain("ZAI_API_KEY"); + } finally { + if (previous.coding === undefined) { + delete process.env.ZAI_CODING_API_KEY; + } else { + process.env.ZAI_CODING_API_KEY = previous.coding; + } + if (previous.zai === undefined) { + delete process.env.ZAI_API_KEY; + } else { + process.env.ZAI_API_KEY = previous.zai; + } + if (previous.legacy === undefined) { + delete process.env.Z_AI_API_KEY; + } else { + process.env.Z_AI_API_KEY = previous.legacy; + } + } + }); + + it("throws when zhipu API key is missing", async () => { + const previous = process.env.ZHIPU_API_KEY; + + try { + delete process.env.ZHIPU_API_KEY; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + let error: unknown = null; + try { + await resolveApiKeyForProvider({ + provider: "zhipu", + store: { version: 1, profiles: {} }, + }); + } catch (err) { + error = err; + } + + expect(String(error)).toContain('No API key found for provider "zhipu".'); + } finally { + if (previous === undefined) { + delete process.env.ZHIPU_API_KEY; + } else { + process.env.ZHIPU_API_KEY = previous; + } + } + }); + + it("resolves zhipu API key from ZHIPU_API_KEY", async () => { + const previous = process.env.ZHIPU_API_KEY; + + try { + process.env.ZHIPU_API_KEY = "zhipu-test-key"; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + const resolved = await resolveApiKeyForProvider({ + provider: "zhipu", + store: { version: 1, profiles: {} }, + }); + expect(resolved.apiKey).toBe("zhipu-test-key"); + expect(resolved.source).toContain("ZHIPU_API_KEY"); + } finally { + if (previous === undefined) { + delete process.env.ZHIPU_API_KEY; + } else { + process.env.ZHIPU_API_KEY = previous; + } + } + }); + + it("resolves zhipu-coding API key with fallback to ZHIPU_API_KEY", async () => { + const previous = { + coding: process.env.ZHIPU_CODING_API_KEY, + zhipu: process.env.ZHIPU_API_KEY, + }; + + try { + delete process.env.ZHIPU_CODING_API_KEY; + process.env.ZHIPU_API_KEY = "zhipu-fallback-key"; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + const resolved = await resolveApiKeyForProvider({ + provider: "zhipu-coding", + store: { version: 1, profiles: {} }, + }); + expect(resolved.apiKey).toBe("zhipu-fallback-key"); + expect(resolved.source).toContain("ZHIPU_API_KEY"); + } finally { + if (previous.coding === undefined) { + delete process.env.ZHIPU_CODING_API_KEY; + } else { + process.env.ZHIPU_CODING_API_KEY = previous.coding; + } + if (previous.zhipu === undefined) { + delete process.env.ZHIPU_API_KEY; + } else { + process.env.ZHIPU_API_KEY = previous.zhipu; + } + } + }); + it("resolves Synthetic API key from env", async () => { const previousSynthetic = process.env.SYNTHETIC_API_KEY; diff --git a/src/cli/program.smoke.test.ts b/src/cli/program.smoke.test.ts index f6b155554..35e76644d 100644 --- a/src/cli/program.smoke.test.ts +++ b/src/cli/program.smoke.test.ts @@ -182,6 +182,24 @@ describe("cli program (smoke)", () => { key: "sk-zai-test", field: "zaiApiKey", }, + { + authChoice: "zai-coding-api-key", + flag: "--zai-coding-api-key", + key: "sk-zai-coding-test", + field: "zaiCodingApiKey", + }, + { + authChoice: "zhipu-api-key", + flag: "--zhipu-api-key", + key: "sk-zhipu-test", + field: "zhipuApiKey", + }, + { + authChoice: "zhipu-coding-api-key", + flag: "--zhipu-coding-api-key", + key: "sk-zhipu-coding-test", + field: "zhipuCodingApiKey", + }, ] as const; for (const entry of cases) { diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index 7bf917a27..3f216b220 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -23,14 +23,19 @@ describe("buildAuthChoiceOptions", () => { expect(options.some((opt) => opt.value === "token")).toBe(true); }); - it("includes Z.AI (GLM) auth choice", () => { + it("includes Z.AI / Zhipu (GLM) auth choices", () => { const store: AuthProfileStore = { version: 1, profiles: {} }; const options = buildAuthChoiceOptions({ store, includeSkip: false, }); + // International variants (api.z.ai) expect(options.some((opt) => opt.value === "zai-api-key")).toBe(true); + expect(options.some((opt) => opt.value === "zai-coding-api-key")).toBe(true); + // China variants (bigmodel.cn) + expect(options.some((opt) => opt.value === "zhipu-api-key")).toBe(true); + expect(options.some((opt) => opt.value === "zhipu-coding-api-key")).toBe(true); }); it("includes MiniMax auth choice", () => { diff --git a/src/infra/provider-usage.test.ts b/src/infra/provider-usage.test.ts index 077e70918..f5f95749a 100644 --- a/src/infra/provider-usage.test.ts +++ b/src/infra/provider-usage.test.ts @@ -137,6 +137,49 @@ describe("provider usage loading", () => { expect(mockFetch).toHaveBeenCalled(); }); + // TODO: Implement fetchZhipuUsage in provider-usage.load.ts + it("loads zhipu usage from bigmodel.cn endpoint", async () => { + const makeResponse = (status: number, body: unknown): Response => { + const payload = typeof body === "string" ? body : JSON.stringify(body); + const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; + return new Response(payload, { status, headers }); + }; + + const mockFetch = vi.fn, ReturnType>(async (input) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("open.bigmodel.cn")) { + return makeResponse(200, { + success: true, + code: 200, + data: { + planName: "Basic", + limits: [ + { + type: "TOKENS_LIMIT", + percentage: 30, + unit: 3, + number: 6, + nextResetTime: "2026-01-07T06:00:00Z", + }, + ], + }, + }); + } + return makeResponse(404, "not found"); + }); + + const summary = await loadProviderUsageSummary({ + now: Date.UTC(2026, 0, 7, 0, 0, 0), + auth: [{ provider: "zhipu", token: "token-1" }], + fetch: mockFetch, + }); + + const zhipu = summary.providers.find((p) => p.provider === "zhipu"); + expect(zhipu?.plan).toBe("Basic"); + expect(zhipu?.windows[0]?.usedPercent).toBe(30); + }); + it("handles nested MiniMax usage payloads", async () => { const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body); From 38464465da76c1192408f86f8cf38619317fa74c Mon Sep 17 00:00:00 2001 From: Kyle Howells Date: Thu, 29 Jan 2026 18:50:28 +0000 Subject: [PATCH 5/6] feat(providers): implement GLM usage fetching for all variants - Refactor fetchZaiUsage to fetchGlmUsage supporting all 4 GLM variants - Add URL routing for api.z.ai (zai, zai-coding) and bigmodel.cn (zhipu, zhipu-coding) - Update provider-usage.load.ts to route all GLM providers to fetchGlmUsage - Add focused tests for GLM-specific behaviors: - HTTP error handling - API error (non-200 code) handling - TIME_LIMIT window format (Monthly label) - Combined TOKENS_LIMIT + TIME_LIMIT response - Keep deprecated fetchZaiUsage as compatibility wrapper Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- src/infra/provider-usage.fetch.ts | 2 +- src/infra/provider-usage.fetch.zai.ts | 42 +++++-- src/infra/provider-usage.load.ts | 7 +- src/infra/provider-usage.test.ts | 159 +++++++++++++++++++++++++- 4 files changed, 197 insertions(+), 13 deletions(-) diff --git a/src/infra/provider-usage.fetch.ts b/src/infra/provider-usage.fetch.ts index 070396554..c9f904f2f 100644 --- a/src/infra/provider-usage.fetch.ts +++ b/src/infra/provider-usage.fetch.ts @@ -4,4 +4,4 @@ export { fetchCodexUsage } from "./provider-usage.fetch.codex.js"; export { fetchCopilotUsage } from "./provider-usage.fetch.copilot.js"; export { fetchGeminiUsage } from "./provider-usage.fetch.gemini.js"; export { fetchMinimaxUsage } from "./provider-usage.fetch.minimax.js"; -export { fetchZaiUsage } from "./provider-usage.fetch.zai.js"; +export { fetchGlmUsage, fetchZaiUsage } from "./provider-usage.fetch.zai.js"; diff --git a/src/infra/provider-usage.fetch.zai.ts b/src/infra/provider-usage.fetch.zai.ts index 03237f279..30b417e4d 100644 --- a/src/infra/provider-usage.fetch.zai.ts +++ b/src/infra/provider-usage.fetch.zai.ts @@ -1,6 +1,10 @@ import { fetchJson } from "./provider-usage.fetch.shared.js"; import { clampPercent, PROVIDER_LABELS } from "./provider-usage.shared.js"; -import type { ProviderUsageSnapshot, UsageWindow } from "./provider-usage.types.js"; +import type { + ProviderUsageSnapshot, + UsageProviderId, + UsageWindow, +} from "./provider-usage.types.js"; type ZaiUsageResponse = { success?: boolean; @@ -19,13 +23,24 @@ type ZaiUsageResponse = { }; }; -export async function fetchZaiUsage( +type GlmUsageProviderId = "zai" | "zai-coding" | "zhipu" | "zhipu-coding"; + +const GLM_USAGE_URLS: Record = { + zai: "https://api.z.ai/api/monitor/usage/quota/limit", + "zai-coding": "https://api.z.ai/api/monitor/usage/quota/limit", + zhipu: "https://open.bigmodel.cn/api/monitor/usage/quota/limit", + "zhipu-coding": "https://open.bigmodel.cn/api/monitor/usage/quota/limit", +}; + +export async function fetchGlmUsage( + provider: GlmUsageProviderId, apiKey: string, timeoutMs: number, fetchFn: typeof fetch, ): Promise { + const url = GLM_USAGE_URLS[provider]; const res = await fetchJson( - "https://api.z.ai/api/monitor/usage/quota/limit", + url, { method: "GET", headers: { @@ -39,8 +54,8 @@ export async function fetchZaiUsage( if (!res.ok) { return { - provider: "zai", - displayName: PROVIDER_LABELS.zai, + provider: provider as UsageProviderId, + displayName: PROVIDER_LABELS[provider], windows: [], error: `HTTP ${res.status}`, }; @@ -49,8 +64,8 @@ export async function fetchZaiUsage( const data = (await res.json()) as ZaiUsageResponse; if (!data.success || data.code !== 200) { return { - provider: "zai", - displayName: PROVIDER_LABELS.zai, + provider: provider as UsageProviderId, + displayName: PROVIDER_LABELS[provider], windows: [], error: data.msg || "API error", }; @@ -84,9 +99,18 @@ export async function fetchZaiUsage( const planName = data.data?.planName || data.data?.plan || undefined; return { - provider: "zai", - displayName: PROVIDER_LABELS.zai, + provider: provider as UsageProviderId, + displayName: PROVIDER_LABELS[provider], windows, plan: planName, }; } + +/** @deprecated Use fetchGlmUsage instead */ +export async function fetchZaiUsage( + apiKey: string, + timeoutMs: number, + fetchFn: typeof fetch, +): Promise { + return fetchGlmUsage("zai", apiKey, timeoutMs, fetchFn); +} diff --git a/src/infra/provider-usage.load.ts b/src/infra/provider-usage.load.ts index 39a97a86c..c026df46b 100644 --- a/src/infra/provider-usage.load.ts +++ b/src/infra/provider-usage.load.ts @@ -5,8 +5,8 @@ import { fetchCodexUsage, fetchCopilotUsage, fetchGeminiUsage, + fetchGlmUsage, fetchMinimaxUsage, - fetchZaiUsage, } from "./provider-usage.fetch.js"; import { DEFAULT_TIMEOUT_MS, @@ -67,7 +67,10 @@ export async function loadProviderUsageSummary( case "minimax": return await fetchMinimaxUsage(auth.token, timeoutMs, fetchFn); case "zai": - return await fetchZaiUsage(auth.token, timeoutMs, fetchFn); + case "zai-coding": + case "zhipu": + case "zhipu-coding": + return await fetchGlmUsage(auth.provider, auth.token, timeoutMs, fetchFn); default: return { provider: auth.provider, diff --git a/src/infra/provider-usage.test.ts b/src/infra/provider-usage.test.ts index f5f95749a..a39e81ddc 100644 --- a/src/infra/provider-usage.test.ts +++ b/src/infra/provider-usage.test.ts @@ -137,7 +137,6 @@ describe("provider usage loading", () => { expect(mockFetch).toHaveBeenCalled(); }); - // TODO: Implement fetchZhipuUsage in provider-usage.load.ts it("loads zhipu usage from bigmodel.cn endpoint", async () => { const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body); @@ -180,6 +179,164 @@ describe("provider usage loading", () => { expect(zhipu?.windows[0]?.usedPercent).toBe(30); }); + it("returns error snapshot when GLM API returns HTTP error", async () => { + const makeResponse = (status: number, body: unknown): Response => { + const payload = typeof body === "string" ? body : JSON.stringify(body); + const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; + return new Response(payload, { status, headers }); + }; + + const mockFetch = vi.fn, ReturnType>(async (input) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("api.z.ai")) { + return makeResponse(500, "Internal Server Error"); + } + return makeResponse(404, "not found"); + }); + + const summary = await loadProviderUsageSummary({ + now: Date.UTC(2026, 0, 7, 0, 0, 0), + auth: [{ provider: "zai", token: "token-1" }], + fetch: mockFetch, + }); + + const zai = summary.providers.find((p) => p.provider === "zai"); + expect(zai?.error).toBe("HTTP 500"); + expect(zai?.windows).toHaveLength(0); + }); + + it("returns error snapshot when GLM API returns non-200 code", async () => { + const makeResponse = (status: number, body: unknown): Response => { + const payload = typeof body === "string" ? body : JSON.stringify(body); + const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; + return new Response(payload, { status, headers }); + }; + + const mockFetch = vi.fn, ReturnType>(async (input) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("api.z.ai")) { + return makeResponse(200, { + success: false, + code: 401, + msg: "Invalid API key", + }); + } + return makeResponse(404, "not found"); + }); + + const summary = await loadProviderUsageSummary({ + now: Date.UTC(2026, 0, 7, 0, 0, 0), + auth: [{ provider: "zai", token: "invalid-token" }], + fetch: mockFetch, + }); + + const zai = summary.providers.find((p) => p.provider === "zai"); + expect(zai?.error).toBe("Invalid API key"); + expect(zai?.windows).toHaveLength(0); + }); + + it("handles GLM TIME_LIMIT as Monthly window", async () => { + const makeResponse = (status: number, body: unknown): Response => { + const payload = typeof body === "string" ? body : JSON.stringify(body); + const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; + return new Response(payload, { status, headers }); + }; + + const mockFetch = vi.fn, ReturnType>(async (input) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("api.z.ai")) { + return makeResponse(200, { + success: true, + code: 200, + data: { + planName: "Enterprise", + limits: [ + { + type: "TIME_LIMIT", + percentage: 45, + unit: 1, + number: 30, + nextResetTime: "2026-02-01T00:00:00Z", + }, + ], + }, + }); + } + return makeResponse(404, "not found"); + }); + + const summary = await loadProviderUsageSummary({ + now: Date.UTC(2026, 0, 7, 0, 0, 0), + auth: [{ provider: "zai", token: "token-1" }], + fetch: mockFetch, + }); + + const zai = summary.providers.find((p) => p.provider === "zai"); + expect(zai?.plan).toBe("Enterprise"); + expect(zai?.windows).toHaveLength(1); + expect(zai?.windows[0]?.label).toBe("Monthly"); + expect(zai?.windows[0]?.usedPercent).toBe(45); + }); + + it("handles GLM response with both TOKENS_LIMIT and TIME_LIMIT", async () => { + const makeResponse = (status: number, body: unknown): Response => { + const payload = typeof body === "string" ? body : JSON.stringify(body); + const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; + return new Response(payload, { status, headers }); + }; + + const mockFetch = vi.fn, ReturnType>(async (input) => { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("api.z.ai")) { + return makeResponse(200, { + success: true, + code: 200, + data: { + planName: "Pro", + limits: [ + { + type: "TOKENS_LIMIT", + percentage: 25, + unit: 3, + number: 24, + nextResetTime: "2026-01-08T00:00:00Z", + }, + { + type: "TIME_LIMIT", + percentage: 10, + unit: 1, + number: 30, + nextResetTime: "2026-02-01T00:00:00Z", + }, + ], + }, + }); + } + return makeResponse(404, "not found"); + }); + + const summary = await loadProviderUsageSummary({ + now: Date.UTC(2026, 0, 7, 0, 0, 0), + auth: [{ provider: "zai", token: "token-1" }], + fetch: mockFetch, + }); + + const zai = summary.providers.find((p) => p.provider === "zai"); + expect(zai?.plan).toBe("Pro"); + expect(zai?.windows).toHaveLength(2); + + const tokensWindow = zai?.windows.find((w) => w.label.startsWith("Tokens")); + expect(tokensWindow?.label).toBe("Tokens (24h)"); + expect(tokensWindow?.usedPercent).toBe(25); + + const monthlyWindow = zai?.windows.find((w) => w.label === "Monthly"); + expect(monthlyWindow?.usedPercent).toBe(10); + }); + it("handles nested MiniMax usage payloads", async () => { const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body); From 531b31efe54a23505e6b071d08a239b74e5a40f1 Mon Sep 17 00:00:00 2001 From: Kyle Howells Date: Thu, 29 Jan 2026 19:30:43 +0000 Subject: [PATCH 6/6] docs(zai): correct misleading API key interoperability claim The previous documentation suggested API keys work on both general and coding endpoints with just different billing. Research confirmed this is misleading - keys are subscription-gated and will return error 1113 ("Insufficient balance") when used on the wrong endpoint type. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- docs/providers/zai.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/providers/zai.md b/docs/providers/zai.md index 674e6e493..ea4ff007d 100644 --- a/docs/providers/zai.md +++ b/docs/providers/zai.md @@ -113,9 +113,10 @@ use a single API key for both if desired. ## Important Notes -- **Same API key, different endpoints**: Your API key works on both general and coding - endpoints, but the billing is different. Using a Coding Plan key on the general - endpoint may return error 1113 ("Insufficient balance"). +- **Keys are subscription-gated**: Your API key only works on the endpoint matching + your active subscription. A Coding Plan key will return error 1113 ("Insufficient + balance") on the general pay-as-you-go endpoint, and vice versa. Choose the provider + variant that matches your subscription type. - **Regional keys are not interchangeable**: Keys from z.ai don't work on bigmodel.cn and vice versa. Create your key on the platform for your region.