From 719ab3b4c5fd4aceac9d299e28007c516de0e467 Mon Sep 17 00:00:00 2001 From: Kyle Howells Date: Thu, 29 Jan 2026 16:18:51 +0000 Subject: [PATCH] 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";