diff --git a/docs/providers/glm.md b/docs/providers/glm.md index b180eda5d..9e57d11b9 100644 --- a/docs/providers/glm.md +++ b/docs/providers/glm.md @@ -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) diff --git a/docs/providers/index.md b/docs/providers/index.md index b2793ee22..1cb29eaed 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -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) diff --git a/docs/providers/zai.md b/docs/providers/zai.md index 056ae303c..f98bd9991 100644 --- a/docs/providers/zai.md +++ b/docs/providers/zai.md @@ -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/` (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. diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index d5ce43349..310a3333e 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/agents/model-auth.ts b/src/agents/model-auth.ts index 1445b53f7..b1dd40493 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 1278db072..dd27f0e3b 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/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/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 3f81a5ee8..ddff42686 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|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 ", @@ -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("--xiaomi-api-key ", "Xiaomi API key") .option("--minimax-api-key ", "MiniMax API key") .option("--synthetic-api-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, diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index c85cc0b4d..4670420ec 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 Xiaomi auth choice", () => { diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 5acddf4e3..174a3a0c9 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" | "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", diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index fa4fc77e7..240262ff6 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -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) { diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index c94eeb51b..fd90be02a 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -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) ? { @@ -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) + ? { + 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) + ? { + 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) + ? { + 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] = { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index fbf6dbfb9..5342306e0 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 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", 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 612b24865..1476aff0d 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -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, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 8719a1f1a..76ade9bb3 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -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); diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index f4154bc6d..dae16ba17 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" | "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; 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 5eb101d85..d7b93f030 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, @@ -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, diff --git a/src/infra/provider-usage.shared.ts b/src/infra/provider-usage.shared.ts index 55eca4757..268d14128 100644 --- a/src/infra/provider-usage.shared.ts +++ b/src/infra/provider-usage.shared.ts @@ -11,7 +11,10 @@ export const PROVIDER_LABELS: Record = { 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 { diff --git a/src/infra/provider-usage.test.ts b/src/infra/provider-usage.test.ts index 5bc6ba575..e7a099a79 100644 --- a/src/infra/provider-usage.test.ts +++ b/src/infra/provider-usage.test.ts @@ -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, 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("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); diff --git a/src/infra/provider-usage.types.ts b/src/infra/provider-usage.types.ts index 0a4637a7d..323e36144 100644 --- a/src/infra/provider-usage.types.ts +++ b/src/infra/provider-usage.types.ts @@ -25,4 +25,7 @@ export type UsageProviderId = | "minimax" | "openai-codex" | "xiaomi" - | "zai"; + | "zai" + | "zai-coding" + | "zhipu" + | "zhipu-coding";