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 <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
Kyle Howells 2026-01-29 16:18:51 +00:00
parent 718bc3f9c8
commit 719ab3b4c5
9 changed files with 362 additions and 6 deletions

View File

@ -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;

View File

@ -6,8 +6,15 @@ function isOpenAiCompletionsModel(model: Model<Api>): model is Model<"openai-com
export function normalizeModelCompat(model: Model<Api>): Model<Api> {
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;

View File

@ -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;

View File

@ -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<string, unknown>)
? {
@ -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<string, unknown>)
? {
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<string, unknown>)
? {
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<string, unknown>)
? {
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] = {

View File

@ -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",

View File

@ -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,
};
}

View File

@ -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,

View File

@ -10,7 +10,10 @@ export const PROVIDER_LABELS: Record<UsageProviderId, string> = {
"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 {

View File

@ -24,4 +24,7 @@ export type UsageProviderId =
| "google-antigravity"
| "minimax"
| "openai-codex"
| "zai";
| "zai"
| "zai-coding"
| "zhipu"
| "zhipu-coding";