Merge 60c26c4604 into 09be5d45d5
This commit is contained in:
commit
3db70acfe2
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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] = {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -25,4 +25,7 @@ export type UsageProviderId =
|
||||
| "minimax"
|
||||
| "openai-codex"
|
||||
| "xiaomi"
|
||||
| "zai";
|
||||
| "zai"
|
||||
| "zai-coding"
|
||||
| "zhipu"
|
||||
| "zhipu-coding";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user