feat(provider): Z.AI endpoints + model catalog (#13456) (thanks @tomsun28) (#13456)

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Tomsun28
2026-02-12 21:01:48 +08:00
committed by GitHub
parent b094491cf5
commit 540996f10f
17 changed files with 482 additions and 32 deletions

1
.gitignore vendored
View File

@@ -73,6 +73,7 @@ docs/.local/
IDENTITY.md
USER.md
.tgz
.idea
# local tooling
.serena/

View File

@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
- Slack: detect control commands when channel messages start with bot mention prefixes (for example, `@Bot /new`). (#14142) Thanks @beefiker.
- Discord tests: use a partial @buape/carbon mock in slash command coverage. (#13262) Thanks @arosstale.
- CLI/Wizard: exit with code 1 when `configure`, `agents add`, or interactive `onboard` wizards are canceled, so `set -e` automation stops correctly. (#14156) Thanks @0xRaini.
- Onboarding/Providers: add Z.AI endpoint-specific auth choices (`zai-coding-global`, `zai-coding-cn`, `zai-global`, `zai-cn`) and expand default Z.AI model wiring. (#13456) Thanks @tomsun28.
- Feishu: pass `Buffer` directly to the Feishu SDK upload APIs instead of `Readable.from(...)` to avoid form-data upload failures. (#10345) Thanks @youngerstyle.
- Feishu: trigger mention-gated group handling only when the bot itself is mentioned (not just any mention). (#11088) Thanks @openperf.
- Feishu: probe status uses the resolved account context for multi-account credential checks. (#11233) Thanks @onevcat.

View File

@@ -39,6 +39,20 @@ openclaw onboard --non-interactive \
`--custom-api-key` is optional in non-interactive mode. If omitted, onboarding checks `CUSTOM_API_KEY`.
Non-interactive Z.AI endpoint choices:
```bash
# Promptless endpoint selection
openclaw onboard --non-interactive \
--auth-choice zai-coding-global \
--zai-api-key "$ZAI_API_KEY"
# Other Z.AI endpoint choices:
# --auth-choice zai-coding-cn
# --auth-choice zai-global
# --auth-choice zai-cn
```
Flow notes:
- `quickstart`: minimal prompts, auto-generates a gateway token.

View File

@@ -19,7 +19,7 @@ const CODEX_MODELS = [
"gpt-5.1-codex-max",
];
const GOOGLE_PREFIXES = ["gemini-3"];
const ZAI_PREFIXES = ["glm-4.7"];
const ZAI_PREFIXES = ["glm-5", "glm-4.7", "glm-4.7-flash", "glm-4.7-flashx"];
const MINIMAX_PREFIXES = ["minimax-m2.1"];
const XAI_PREFIXES = ["grok-4"];

View File

@@ -29,4 +29,26 @@ describeLive("zai live", () => {
.join(" ");
expect(text.length).toBeGreaterThan(0);
}, 20000);
it("glm-4.7-flashx returns assistant text", async () => {
const model = getModel("zai", "glm-4.7-flashx" as "glm-4.7");
const res = await completeSimple(
model,
{
messages: [
{
role: "user",
content: "Reply with the word ok.",
timestamp: Date.now(),
},
],
},
{ apiKey: ZAI_KEY, maxTokens: 64 },
);
const text = res.content
.filter((block) => block.type === "text")
.map((block) => block.text.trim())
.join(" ");
expect(text.length).toBeGreaterThan(0);
}, 20000);
});

View File

@@ -58,7 +58,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|xai-api-key|qianfan-api-key|openrouter-api-key|litellm-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|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|custom-api-key|skip|together-api-key",
"Auth: setup-token|token|chutes|openai-codex|openai-api-key|xai-api-key|qianfan-api-key|openrouter-api-key|litellm-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|zai-coding-global|zai-coding-cn|zai-global|zai-cn|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|custom-api-key|skip|together-api-key",
)
.option(
"--token-provider <id>",

View File

@@ -92,9 +92,9 @@ const AUTH_CHOICE_GROUP_DEFS: {
},
{
value: "zai",
label: "Z.AI (GLM 4.7)",
hint: "API key",
choices: ["zai-api-key"],
label: "Z.AI",
hint: "GLM Coding Plan / Global / CN",
choices: ["zai-coding-global", "zai-coding-cn", "zai-global", "zai-cn"],
},
{
value: "qianfan",
@@ -242,7 +242,27 @@ 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" });
options.push({
value: "zai-coding-global",
label: "Coding-Plan-Global",
hint: "GLM Coding Plan Global (api.z.ai)",
});
options.push({
value: "zai-coding-cn",
label: "Coding-Plan-CN",
hint: "GLM Coding Plan CN (open.bigmodel.cn)",
});
options.push({
value: "zai-global",
label: "Global",
hint: "Z.AI Global (api.z.ai)",
});
options.push({
value: "zai-cn",
label: "CN",
hint: "Z.AI CN (open.bigmodel.cn)",
});
options.push({
value: "xiaomi-api-key",
label: "Xiaomi API key",

View File

@@ -40,6 +40,7 @@ import {
applyXiaomiConfig,
applyXiaomiProviderConfig,
applyZaiConfig,
applyZaiProviderConfig,
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
LITELLM_DEFAULT_MODEL_REF,
QIANFAN_DEFAULT_MODEL_REF,
@@ -619,7 +620,54 @@ export async function applyAuthChoiceApiProviders(
return { config: nextConfig, agentModelOverride };
}
if (authChoice === "zai-api-key") {
if (
authChoice === "zai-api-key" ||
authChoice === "zai-coding-global" ||
authChoice === "zai-coding-cn" ||
authChoice === "zai-global" ||
authChoice === "zai-cn"
) {
// Determine endpoint from authChoice or prompt
let endpoint: string;
if (authChoice === "zai-coding-global") {
endpoint = "coding-global";
} else if (authChoice === "zai-coding-cn") {
endpoint = "coding-cn";
} else if (authChoice === "zai-global") {
endpoint = "global";
} else if (authChoice === "zai-cn") {
endpoint = "cn";
} else {
// zai-api-key: prompt for endpoint selection
endpoint = await params.prompter.select({
message: "Select Z.AI endpoint",
options: [
{
value: "coding-global",
label: "Coding-Plan-Global",
hint: "GLM Coding Plan Global (api.z.ai)",
},
{
value: "coding-cn",
label: "Coding-Plan-CN",
hint: "GLM Coding Plan CN (open.bigmodel.cn)",
},
{
value: "global",
label: "Global",
hint: "Z.AI Global (api.z.ai)",
},
{
value: "cn",
label: "CN",
hint: "Z.AI CN (open.bigmodel.cn)",
},
],
initialValue: "coding-global",
});
}
// Input API key
let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "zai") {
@@ -655,23 +703,8 @@ export async function applyAuthChoiceApiProviders(
config: nextConfig,
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",
},
},
},
},
}),
applyDefaultConfig: (config) => applyZaiConfig(config, { endpoint }),
applyProviderConfig: (config) => applyZaiProviderConfig(config, { endpoint }),
noteDefault: ZAI_DEFAULT_MODEL_REF,
noteAgentModel,
prompter: params.prompter,

View File

@@ -20,6 +20,10 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"google-antigravity": "google-antigravity",
"google-gemini-cli": "google-gemini-cli",
"zai-api-key": "zai",
"zai-coding-global": "zai",
"zai-coding-cn": "zai",
"zai-global": "zai",
"zai-cn": "zai",
"xiaomi-api-key": "xiaomi",
"synthetic-api-key": "synthetic",
"venice-api-key": "venice",

View File

@@ -6,6 +6,7 @@ import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js";
import type { AuthChoice } from "./onboard-types.js";
import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.js";
import { ZAI_CODING_CN_BASE_URL, ZAI_CODING_GLOBAL_BASE_URL } from "./onboard-auth.js";
vi.mock("../providers/github-copilot-auth.js", () => ({
githubCopilotLoginCommand: vi.fn(async () => {}),
@@ -199,6 +200,101 @@ describe("applyAuthChoice", () => {
expect(parsed.profiles?.["synthetic:default"]?.key).toBe("sk-synthetic-test");
});
it("prompts for Z.AI endpoint when selecting zai-api-key", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir;
process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent");
process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR;
const text = vi.fn().mockResolvedValue("zai-test-key");
const select = vi.fn(async (params: { message: string }) => {
if (params.message === "Select Z.AI endpoint") {
return "coding-cn";
}
return "default";
});
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
const prompter: WizardPrompter = {
intro: vi.fn(noopAsync),
outro: vi.fn(noopAsync),
note: vi.fn(noopAsync),
select: select as WizardPrompter["select"],
multiselect,
text,
confirm: vi.fn(async () => false),
progress: vi.fn(() => ({ update: noop, stop: noop })),
};
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn((code: number) => {
throw new Error(`exit:${code}`);
}),
};
const result = await applyAuthChoice({
authChoice: "zai-api-key",
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(select).toHaveBeenCalledWith(
expect.objectContaining({ message: "Select Z.AI endpoint", initialValue: "coding-global" }),
);
expect(result.config.models?.providers?.zai?.baseUrl).toBe(ZAI_CODING_CN_BASE_URL);
expect(result.config.agents?.defaults?.model?.primary).toBe("zai/glm-4.7");
const authProfilePath = authProfilePathFor(requireAgentDir());
const raw = await fs.readFile(authProfilePath, "utf8");
const parsed = JSON.parse(raw) as {
profiles?: Record<string, { key?: string }>;
};
expect(parsed.profiles?.["zai:default"]?.key).toBe("zai-test-key");
});
it("uses endpoint-specific auth choice without prompting for Z.AI endpoint", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir;
process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent");
process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR;
const text = vi.fn().mockResolvedValue("zai-test-key");
const select = vi.fn(async () => "default");
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
const prompter: WizardPrompter = {
intro: vi.fn(noopAsync),
outro: vi.fn(noopAsync),
note: vi.fn(noopAsync),
select: select as WizardPrompter["select"],
multiselect,
text,
confirm: vi.fn(async () => false),
progress: vi.fn(() => ({ update: noop, stop: noop })),
};
const runtime: RuntimeEnv = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn((code: number) => {
throw new Error(`exit:${code}`);
}),
};
const result = await applyAuthChoice({
authChoice: "zai-coding-global",
config: {},
prompter,
runtime,
setDefaultModel: true,
});
expect(select).not.toHaveBeenCalledWith(
expect.objectContaining({ message: "Select Z.AI endpoint" }),
);
expect(result.config.models?.providers?.zai?.baseUrl).toBe(ZAI_CODING_GLOBAL_BASE_URL);
});
it("does not override the global default model when selecting xai-api-key without setDefaultModel", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir;

View File

@@ -38,6 +38,7 @@ import {
XAI_DEFAULT_MODEL_REF,
} from "./onboard-auth.credentials.js";
import {
buildZaiModelDefinition,
buildMoonshotModelDefinition,
buildXaiModelDefinition,
QIANFAN_BASE_URL,
@@ -47,18 +48,65 @@ import {
MOONSHOT_CN_BASE_URL,
MOONSHOT_DEFAULT_MODEL_ID,
MOONSHOT_DEFAULT_MODEL_REF,
ZAI_DEFAULT_MODEL_ID,
resolveZaiBaseUrl,
XAI_BASE_URL,
XAI_DEFAULT_MODEL_ID,
} from "./onboard-auth.models.js";
export function applyZaiConfig(cfg: OpenClawConfig): OpenClawConfig {
export function applyZaiProviderConfig(
cfg: OpenClawConfig,
params?: { endpoint?: string; modelId?: string },
): OpenClawConfig {
const modelId = params?.modelId?.trim() || ZAI_DEFAULT_MODEL_ID;
const modelRef = `zai/${modelId}`;
const models = { ...cfg.agents?.defaults?.models };
models[ZAI_DEFAULT_MODEL_REF] = {
...models[ZAI_DEFAULT_MODEL_REF],
alias: models[ZAI_DEFAULT_MODEL_REF]?.alias ?? "GLM",
models[modelRef] = {
...models[modelRef],
alias: models[modelRef]?.alias ?? "GLM",
};
const providers = { ...cfg.models?.providers };
const existingProvider = providers.zai;
const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
const defaultModels = [
buildZaiModelDefinition({ id: "glm-5" }),
buildZaiModelDefinition({ id: "glm-4.7" }),
buildZaiModelDefinition({ id: "glm-4.7-flash" }),
buildZaiModelDefinition({ id: "glm-4.7-flashx" }),
];
const mergedModels = [...existingModels];
const seen = new Set(existingModels.map((m) => m.id));
for (const model of defaultModels) {
if (!seen.has(model.id)) {
mergedModels.push(model);
seen.add(model.id);
}
}
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
string,
unknown
> as { apiKey?: string };
const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
const normalizedApiKey = resolvedApiKey?.trim();
const baseUrl = params?.endpoint
? resolveZaiBaseUrl(params.endpoint)
: (typeof existingProvider?.baseUrl === "string" ? existingProvider.baseUrl : "") ||
resolveZaiBaseUrl();
providers.zai = {
...existingProviderRest,
baseUrl,
api: "openai-completions",
...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}),
models: mergedModels.length > 0 ? mergedModels : defaultModels,
};
const existingModel = cfg.agents?.defaults?.model;
return {
...cfg,
agents: {
@@ -66,13 +114,37 @@ export function applyZaiConfig(cfg: OpenClawConfig): OpenClawConfig {
defaults: {
...cfg.agents?.defaults,
models,
},
},
models: {
mode: cfg.models?.mode ?? "merge",
providers,
},
};
}
export function applyZaiConfig(
cfg: OpenClawConfig,
params?: { endpoint?: string; modelId?: string },
): OpenClawConfig {
const modelId = params?.modelId?.trim() || ZAI_DEFAULT_MODEL_ID;
const modelRef = modelId === ZAI_DEFAULT_MODEL_ID ? ZAI_DEFAULT_MODEL_REF : `zai/${modelId}`;
const next = applyZaiProviderConfig(cfg, params);
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_DEFAULT_MODEL_REF,
primary: modelRef,
},
},
},

View File

@@ -20,6 +20,26 @@ export const KIMI_CODING_MODEL_REF = `kimi-coding/${KIMI_CODING_MODEL_ID}`;
export { QIANFAN_BASE_URL, QIANFAN_DEFAULT_MODEL_ID };
export const QIANFAN_DEFAULT_MODEL_REF = `qianfan/${QIANFAN_DEFAULT_MODEL_ID}`;
export const ZAI_CODING_GLOBAL_BASE_URL = "https://api.z.ai/api/coding/paas/v4";
export const ZAI_CODING_CN_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4";
export const ZAI_GLOBAL_BASE_URL = "https://api.z.ai/api/paas/v4";
export const ZAI_CN_BASE_URL = "https://open.bigmodel.cn/api/paas/v4";
export const ZAI_DEFAULT_MODEL_ID = "glm-4.7";
export function resolveZaiBaseUrl(endpoint?: string): string {
switch (endpoint) {
case "coding-cn":
return ZAI_CODING_CN_BASE_URL;
case "global":
return ZAI_GLOBAL_BASE_URL;
case "cn":
return ZAI_CN_BASE_URL;
case "coding-global":
default:
return ZAI_CODING_GLOBAL_BASE_URL;
}
}
// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
export const MINIMAX_API_COST = {
input: 15,
@@ -46,6 +66,13 @@ export const MOONSHOT_DEFAULT_COST = {
cacheWrite: 0,
};
export const ZAI_DEFAULT_COST = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
};
const MINIMAX_MODEL_CATALOG = {
"MiniMax-M2.1": { name: "MiniMax M2.1", reasoning: false },
"MiniMax-M2.1-lightning": {
@@ -56,6 +83,15 @@ const MINIMAX_MODEL_CATALOG = {
type MinimaxCatalogId = keyof typeof MINIMAX_MODEL_CATALOG;
const ZAI_MODEL_CATALOG = {
"glm-5": { name: "GLM-5", reasoning: true },
"glm-4.7": { name: "GLM-4.7", reasoning: true },
"glm-4.7-flash": { name: "GLM-4.7 Flash", reasoning: true },
"glm-4.7-flashx": { name: "GLM-4.7 FlashX", reasoning: true },
} as const;
type ZaiCatalogId = keyof typeof ZAI_MODEL_CATALOG;
export function buildMinimaxModelDefinition(params: {
id: string;
name?: string;
@@ -97,6 +133,26 @@ export function buildMoonshotModelDefinition(): ModelDefinitionConfig {
};
}
export function buildZaiModelDefinition(params: {
id: string;
name?: string;
reasoning?: boolean;
cost?: ModelDefinitionConfig["cost"];
contextWindow?: number;
maxTokens?: number;
}): ModelDefinitionConfig {
const catalog = ZAI_MODEL_CATALOG[params.id as ZaiCatalogId];
return {
id: params.id,
name: params.name ?? catalog?.name ?? `GLM ${params.id}`,
reasoning: params.reasoning ?? catalog?.reasoning ?? true,
input: ["text"],
cost: params.cost ?? ZAI_DEFAULT_COST,
contextWindow: params.contextWindow ?? 204800,
maxTokens: params.maxTokens ?? 131072,
};
}
export const XAI_BASE_URL = "https://api.x.ai/v1";
export const XAI_DEFAULT_MODEL_ID = "grok-4";
export const XAI_DEFAULT_MODEL_REF = `xai/${XAI_DEFAULT_MODEL_ID}`;

View File

@@ -18,12 +18,16 @@ import {
applyXaiProviderConfig,
applyXiaomiConfig,
applyXiaomiProviderConfig,
applyZaiConfig,
applyZaiProviderConfig,
OPENROUTER_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_ID,
SYNTHETIC_DEFAULT_MODEL_REF,
XAI_DEFAULT_MODEL_REF,
setMinimaxApiKey,
writeOAuthCredentials,
ZAI_CODING_CN_BASE_URL,
ZAI_CODING_GLOBAL_BASE_URL,
} from "./onboard-auth.js";
const authProfilePathFor = (agentDir: string) => path.join(agentDir, "auth-profiles.json");
@@ -303,6 +307,47 @@ describe("applyMinimaxApiProviderConfig", () => {
});
});
describe("applyZaiConfig", () => {
it("adds zai provider with correct settings", () => {
const cfg = applyZaiConfig({});
expect(cfg.models?.providers?.zai).toMatchObject({
baseUrl: ZAI_CODING_GLOBAL_BASE_URL,
api: "openai-completions",
});
const ids = cfg.models?.providers?.zai?.models?.map((m) => m.id);
expect(ids).toContain("glm-5");
expect(ids).toContain("glm-4.7");
expect(ids).toContain("glm-4.7-flash");
expect(ids).toContain("glm-4.7-flashx");
});
it("sets correct primary model", () => {
const cfg = applyZaiConfig({}, { modelId: "glm-5" });
expect(cfg.agents?.defaults?.model?.primary).toBe("zai/glm-5");
});
it("supports CN endpoint", () => {
const cfg = applyZaiConfig({}, { endpoint: "coding-cn", modelId: "glm-4.7-flash" });
expect(cfg.models?.providers?.zai?.baseUrl).toBe(ZAI_CODING_CN_BASE_URL);
expect(cfg.agents?.defaults?.model?.primary).toBe("zai/glm-4.7-flash");
});
it("supports CN endpoint with glm-4.7-flashx", () => {
const cfg = applyZaiConfig({}, { endpoint: "coding-cn", modelId: "glm-4.7-flashx" });
expect(cfg.models?.providers?.zai?.baseUrl).toBe(ZAI_CODING_CN_BASE_URL);
expect(cfg.agents?.defaults?.model?.primary).toBe("zai/glm-4.7-flashx");
});
});
describe("applyZaiProviderConfig", () => {
it("does not overwrite existing primary model", () => {
const cfg = applyZaiProviderConfig({
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } },
});
expect(cfg.agents?.defaults?.model?.primary).toBe("anthropic/claude-opus-4-5");
});
});
describe("applySyntheticConfig", () => {
it("adds synthetic provider with correct settings", () => {
const cfg = applySyntheticConfig({});

View File

@@ -32,6 +32,7 @@ export {
applyXiaomiConfig,
applyXiaomiProviderConfig,
applyZaiConfig,
applyZaiProviderConfig,
} from "./onboard-auth.config-core.js";
export {
applyMinimaxApiConfig,
@@ -78,6 +79,7 @@ export {
buildMinimaxApiModelDefinition,
buildMinimaxModelDefinition,
buildMoonshotModelDefinition,
buildZaiModelDefinition,
DEFAULT_MINIMAX_BASE_URL,
MOONSHOT_CN_BASE_URL,
QIANFAN_BASE_URL,
@@ -91,4 +93,10 @@ export {
MOONSHOT_BASE_URL,
MOONSHOT_DEFAULT_MODEL_ID,
MOONSHOT_DEFAULT_MODEL_REF,
resolveZaiBaseUrl,
ZAI_CODING_CN_BASE_URL,
ZAI_DEFAULT_MODEL_ID,
ZAI_CODING_GLOBAL_BASE_URL,
ZAI_CN_BASE_URL,
ZAI_GLOBAL_BASE_URL,
} from "./onboard-auth.models.js";

View File

@@ -139,6 +139,60 @@ async function expectApiKeyProfile(params: {
}
describe("onboard (non-interactive): provider auth", () => {
it("stores Z.AI API key and uses coding-global baseUrl by default", async () => {
await withOnboardEnv("openclaw-onboard-zai-", async ({ configPath, runtime }) => {
await runNonInteractive(
{
nonInteractive: true,
authChoice: "zai-api-key",
zaiApiKey: "zai-test-key",
skipHealth: true,
skipChannels: true,
skipSkills: true,
json: true,
},
runtime,
);
const cfg = await readJsonFile<{
auth?: { profiles?: Record<string, { provider?: string; mode?: string }> };
agents?: { defaults?: { model?: { primary?: string } } };
models?: { providers?: Record<string, { baseUrl?: string }> };
}>(configPath);
expect(cfg.auth?.profiles?.["zai:default"]?.provider).toBe("zai");
expect(cfg.auth?.profiles?.["zai:default"]?.mode).toBe("api_key");
expect(cfg.models?.providers?.zai?.baseUrl).toBe("https://api.z.ai/api/coding/paas/v4");
expect(cfg.agents?.defaults?.model?.primary).toBe("zai/glm-4.7");
await expectApiKeyProfile({ profileId: "zai:default", provider: "zai", key: "zai-test-key" });
});
}, 60_000);
it("supports Z.AI CN coding endpoint auth choice", async () => {
await withOnboardEnv("openclaw-onboard-zai-cn-", async ({ configPath, runtime }) => {
await runNonInteractive(
{
nonInteractive: true,
authChoice: "zai-coding-cn",
zaiApiKey: "zai-test-key",
skipHealth: true,
skipChannels: true,
skipSkills: true,
json: true,
},
runtime,
);
const cfg = await readJsonFile<{
models?: { providers?: Record<string, { baseUrl?: string }> };
}>(configPath);
expect(cfg.models?.providers?.zai?.baseUrl).toBe(
"https://open.bigmodel.cn/api/coding/paas/v4",
);
});
}, 60_000);
it("stores xAI API key and sets default model", async () => {
await withOnboardEnv("openclaw-onboard-xai-", async ({ configPath, runtime }) => {
await runNonInteractive(

View File

@@ -187,7 +187,13 @@ export async function applyNonInteractiveAuthChoice(params: {
return applyGoogleGeminiModelDefault(nextConfig).next;
}
if (authChoice === "zai-api-key") {
if (
authChoice === "zai-api-key" ||
authChoice === "zai-coding-global" ||
authChoice === "zai-coding-cn" ||
authChoice === "zai-global" ||
authChoice === "zai-cn"
) {
const resolved = await resolveNonInteractiveApiKey({
provider: "zai",
cfg: baseConfig,
@@ -207,7 +213,21 @@ export async function applyNonInteractiveAuthChoice(params: {
provider: "zai",
mode: "api_key",
});
return applyZaiConfig(nextConfig);
// Determine endpoint from authChoice or opts
let endpoint: "global" | "cn" | "coding-global" | "coding-cn" | undefined;
if (authChoice === "zai-coding-global") {
endpoint = "coding-global";
} else if (authChoice === "zai-coding-cn") {
endpoint = "coding-cn";
} else if (authChoice === "zai-global") {
endpoint = "global";
} else if (authChoice === "zai-cn") {
endpoint = "cn";
} else {
endpoint = "coding-global";
}
return applyZaiConfig(nextConfig, { endpoint });
}
if (authChoice === "xiaomi-api-key") {

View File

@@ -27,6 +27,10 @@ export type AuthChoice =
| "google-antigravity"
| "google-gemini-cli"
| "zai-api-key"
| "zai-coding-global"
| "zai-coding-cn"
| "zai-global"
| "zai-cn"
| "xiaomi-api-key"
| "minimax-cloud"
| "minimax"