This commit is contained in:
Kyle Howells 2026-01-30 11:39:02 -05:00 committed by GitHub
commit 3db70acfe2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1246 additions and 128 deletions

View File

@ -4,18 +4,33 @@ read_when:
- You want GLM models in OpenClaw
- 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 OpenClaw, 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
openclaw onboard --auth-choice zai-api-key
# China users
openclaw onboard --auth-choice zhipu-api-key
```
## Config snippet
## Config Snippet
```json5
{
@ -24,8 +39,18 @@ openclaw 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)

View File

@ -41,9 +41,9 @@ See [Venice AI](/providers/venice).
- [Moonshot AI (Kimi + Kimi Code)](/providers/moonshot)
- [OpenCode Zen](/providers/opencode)
- [Amazon Bedrock](/bedrock)
- [Z.AI](/providers/zai)
- [Z.AI / Zhipu AI (GLM models)](/providers/zai) - International + China, pay-as-you-go + Coding Plan
- [GLM models](/providers/glm) - Model family overview
- [Xiaomi](/providers/xiaomi)
- [GLM models](/providers/glm)
- [MiniMax](/providers/minimax)
- [Venius (Venice AI, privacy-focused)](/providers/venice)
- [Ollama (local models)](/providers/ollama)

View File

@ -1,24 +1,81 @@
---
summary: "Use Z.AI (GLM models) with OpenClaw"
summary: "Use Z.AI / Zhipu AI (GLM models) with OpenClaw"
read_when:
- You want Z.AI / GLM models in OpenClaw
- 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. OpenClaw 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)
openclaw onboard --auth-choice zai-api-key
# or non-interactive
openclaw onboard --zai-api-key "$ZAI_API_KEY"
# International (Coding Plan)
openclaw onboard --auth-choice zai-coding-api-key
# China (pay-as-you-go)
openclaw onboard --auth-choice zhipu-api-key
# China (Coding Plan)
openclaw onboard --auth-choice zhipu-coding-api-key
```
## Config snippet
### Non-interactive
```bash
# International (pay-as-you-go)
openclaw onboard --non-interactive --auth-choice zai-api-key --zai-api-key "$ZAI_API_KEY"
# International (Coding Plan)
openclaw onboard --non-interactive --auth-choice zai-coding-api-key --zai-coding-api-key "$ZAI_API_KEY"
# China (pay-as-you-go)
openclaw onboard --non-interactive --auth-choice zhipu-api-key --zhipu-api-key "$ZHIPU_API_KEY"
# China (Coding Plan)
openclaw 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,47 @@ openclaw onboard --zai-api-key "$ZAI_API_KEY"
}
```
## Notes
### International (Coding Plan)
- GLM models are available as `zai/<model>` (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
- **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.
- **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.

View File

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

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

@ -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) {

View File

@ -52,7 +52,7 @@ export function registerOnboardCommand(program: Command) {
.option("--mode <mode>", "Wizard mode: local|remote")
.option(
"--auth-choice <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|xiaomi-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|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
)
.option(
"--token-provider <id>",
@ -72,6 +72,9 @@ export function registerOnboardCommand(program: Command) {
.option("--kimi-code-api-key <key>", "Kimi Code API key")
.option("--gemini-api-key <key>", "Gemini API key")
.option("--zai-api-key <key>", "Z.AI API key")
.option("--zai-coding-api-key <key>", "Z.AI Coding Plan API key")
.option("--zhipu-api-key <key>", "Zhipu AI API key")
.option("--zhipu-coding-api-key <key>", "Zhipu AI Coding Plan API key")
.option("--xiaomi-api-key <key>", "Xiaomi API key")
.option("--minimax-api-key <key>", "MiniMax API key")
.option("--synthetic-api-key <key>", "Synthetic API key")
@ -123,6 +126,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,
xiaomiApiKey: opts.xiaomiApiKey as string | undefined,
minimaxApiKey: opts.minimaxApiKey as string | undefined,
syntheticApiKey: opts.syntheticApiKey as string | undefined,

View File

@ -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 Xiaomi auth choice", () => {

View File

@ -16,6 +16,7 @@ export type AuthChoiceGroupId =
| "ai-gateway"
| "moonshot"
| "zai"
| "zhipu"
| "xiaomi"
| "opencode-zen"
| "minimax"
@ -104,9 +105,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: "xiaomi",
@ -170,7 +177,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: "xiaomi-api-key",
label: "Xiaomi API key",

View File

@ -29,7 +29,14 @@ import {
applyVercelAiGatewayProviderConfig,
applyXiaomiConfig,
applyXiaomiProviderConfig,
applyZaiCodingConfig,
applyZaiCodingProviderConfig,
applyZaiConfig,
applyZaiProviderConfig,
applyZhipuCodingConfig,
applyZhipuCodingProviderConfig,
applyZhipuConfig,
applyZhipuProviderConfig,
KIMI_CODE_MODEL_REF,
MOONSHOT_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
@ -47,7 +54,13 @@ import {
setVercelAiGatewayApiKey,
setXiaomiApiKey,
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";
@ -83,6 +96,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 === "xiaomi") {
authChoice = "xiaomi-api-key";
} else if (params.opts.tokenProvider === "synthetic") {
@ -184,15 +203,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) {
@ -232,15 +253,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) {
@ -287,15 +310,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) {
@ -335,15 +360,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) {
@ -382,15 +409,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) {
@ -411,22 +440,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,
@ -437,6 +451,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 === "xiaomi-api-key") {
let hasCredential = false;
@ -536,15 +700,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) {
@ -593,15 +759,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) {

View File

@ -16,11 +16,22 @@ import {
OPENROUTER_DEFAULT_MODEL_REF,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
XIAOMI_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,
@ -29,14 +40,33 @@ import {
MOONSHOT_DEFAULT_MODEL_REF,
} from "./onboard-auth.models.js";
export function applyZaiConfig(cfg: OpenClawConfig): OpenClawConfig {
export function applyZaiProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
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: {
@ -44,6 +74,24 @@ export function applyZaiConfig(cfg: OpenClawConfig): OpenClawConfig {
defaults: {
...cfg.agents?.defaults,
models,
},
},
models: {
mode: cfg.models?.mode ?? "merge",
providers,
},
};
}
export function applyZaiConfig(cfg: OpenClawConfig): OpenClawConfig {
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>)
? {
@ -57,6 +105,201 @@ export function applyZaiConfig(cfg: OpenClawConfig): OpenClawConfig {
};
}
export function applyZaiCodingProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
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: OpenClawConfig): OpenClawConfig {
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: OpenClawConfig): OpenClawConfig {
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: OpenClawConfig): OpenClawConfig {
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: OpenClawConfig): OpenClawConfig {
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: OpenClawConfig): OpenClawConfig {
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: OpenClawConfig): OpenClawConfig {
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 XIAOMI_DEFAULT_MODEL_REF = "xiaomi/mimo-v2-flash";
export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.5";
@ -130,6 +133,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 setXiaomiApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "xiaomi: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

@ -19,7 +19,14 @@ export {
applyVercelAiGatewayProviderConfig,
applyXiaomiConfig,
applyXiaomiProviderConfig,
applyZaiCodingConfig,
applyZaiCodingProviderConfig,
applyZaiConfig,
applyZaiProviderConfig,
applyZhipuCodingConfig,
applyZhipuCodingProviderConfig,
applyZhipuConfig,
applyZhipuProviderConfig,
} from "./onboard-auth.config-core.js";
export {
applyMinimaxApiConfig,
@ -48,10 +55,16 @@ export {
setVercelAiGatewayApiKey,
setXiaomiApiKey,
setZaiApiKey,
setZaiCodingApiKey,
setZhipuApiKey,
setZhipuCodingApiKey,
writeOAuthCredentials,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
XIAOMI_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

@ -18,7 +18,10 @@ import {
applyVeniceConfig,
applyVercelAiGatewayConfig,
applyXiaomiConfig,
applyZaiCodingConfig,
applyZaiConfig,
applyZhipuCodingConfig,
applyZhipuConfig,
setAnthropicApiKey,
setGeminiApiKey,
setKimiCodeApiKey,
@ -31,6 +34,9 @@ import {
setVercelAiGatewayApiKey,
setXiaomiApiKey,
setZaiApiKey,
setZaiCodingApiKey,
setZhipuApiKey,
setZhipuCodingApiKey,
} from "../../onboard-auth.js";
import type { AuthChoice, OnboardOptions } from "../../onboard-types.js";
import { resolveNonInteractiveApiKey } from "../api-keys.js";
@ -179,6 +185,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 === "xiaomi-api-key") {
const resolved = await resolveNonInteractiveApiKey({
provider: "xiaomi",
@ -380,7 +443,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);

View File

@ -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"
| "xiaomi-api-key"
| "minimax-cloud"
| "minimax"
@ -68,6 +71,9 @@ export type OnboardOptions = {
kimiCodeApiKey?: string;
geminiApiKey?: string;
zaiApiKey?: string;
zaiCodingApiKey?: string;
zhipuApiKey?: string;
zhipuCodingApiKey?: string;
xiaomiApiKey?: string;
minimaxApiKey?: string;
syntheticApiKey?: string;

View File

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

View File

@ -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<GlmUsageProviderId, string> = {
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<ProviderUsageSnapshot> {
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<ProviderUsageSnapshot> {
return fetchGlmUsage("zai", apiKey, timeoutMs, fetchFn);
}

View File

@ -5,8 +5,8 @@ import {
fetchCodexUsage,
fetchCopilotUsage,
fetchGeminiUsage,
fetchGlmUsage,
fetchMinimaxUsage,
fetchZaiUsage,
} from "./provider-usage.fetch.js";
import {
DEFAULT_TIMEOUT_MS,
@ -73,7 +73,10 @@ export async function loadProviderUsageSummary(
windows: [],
};
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,

View File

@ -11,7 +11,10 @@ export const PROVIDER_LABELS: Record<UsageProviderId, string> = {
minimax: "MiniMax",
"openai-codex": "Codex",
xiaomi: "Xiaomi",
zai: "z.ai",
zai: "Z.AI",
"zai-coding": "Z.AI Coding",
zhipu: "Zhipu AI",
"zhipu-coding": "Zhipu AI Coding",
};
export const usageProviders: UsageProviderId[] = [
@ -23,6 +26,9 @@ export const usageProviders: UsageProviderId[] = [
"openai-codex",
"xiaomi",
"zai",
"zai-coding",
"zhipu",
"zhipu-coding",
];
export function resolveUsageProviderId(provider?: string | null): UsageProviderId | undefined {

View File

@ -137,6 +137,206 @@ describe("provider usage loading", () => {
expect(mockFetch).toHaveBeenCalled();
});
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<Parameters<typeof fetch>, ReturnType<typeof fetch>>(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("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<Parameters<typeof fetch>, ReturnType<typeof fetch>>(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<Parameters<typeof fetch>, ReturnType<typeof fetch>>(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<Parameters<typeof fetch>, ReturnType<typeof fetch>>(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<Parameters<typeof fetch>, ReturnType<typeof fetch>>(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);

View File

@ -25,4 +25,7 @@ export type UsageProviderId =
| "minimax"
| "openai-codex"
| "xiaomi"
| "zai";
| "zai"
| "zai-coding"
| "zhipu"
| "zhipu-coding";