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
View File
@@ -73,6 +73,7 @@ docs/.local/
IDENTITY.md IDENTITY.md
USER.md USER.md
.tgz .tgz
.idea
# local tooling # local tooling
.serena/ .serena/
+1
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. - 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. - 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. - 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: 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: 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. - Feishu: probe status uses the resolved account context for multi-account credential checks. (#11233) Thanks @onevcat.
+14
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`. `--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: Flow notes:
- `quickstart`: minimal prompts, auto-generates a gateway token. - `quickstart`: minimal prompts, auto-generates a gateway token.
+1 -1
View File
@@ -19,7 +19,7 @@ const CODEX_MODELS = [
"gpt-5.1-codex-max", "gpt-5.1-codex-max",
]; ];
const GOOGLE_PREFIXES = ["gemini-3"]; 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 MINIMAX_PREFIXES = ["minimax-m2.1"];
const XAI_PREFIXES = ["grok-4"]; const XAI_PREFIXES = ["grok-4"];
+22
View File
@@ -29,4 +29,26 @@ describeLive("zai live", () => {
.join(" "); .join(" ");
expect(text.length).toBeGreaterThan(0); expect(text.length).toBeGreaterThan(0);
}, 20000); }, 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);
}); });
+1 -1
View File
@@ -58,7 +58,7 @@ export function registerOnboardCommand(program: Command) {
.option("--mode <mode>", "Wizard mode: local|remote") .option("--mode <mode>", "Wizard mode: local|remote")
.option( .option(
"--auth-choice <choice>", "--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( .option(
"--token-provider <id>", "--token-provider <id>",
+24 -4
View File
@@ -92,9 +92,9 @@ const AUTH_CHOICE_GROUP_DEFS: {
}, },
{ {
value: "zai", value: "zai",
label: "Z.AI (GLM 4.7)", label: "Z.AI",
hint: "API key", hint: "GLM Coding Plan / Global / CN",
choices: ["zai-api-key"], choices: ["zai-coding-global", "zai-coding-cn", "zai-global", "zai-cn"],
}, },
{ {
value: "qianfan", value: "qianfan",
@@ -242,7 +242,27 @@ export function buildAuthChoiceOptions(params: {
label: "Google Gemini CLI OAuth", label: "Google Gemini CLI OAuth",
hint: "Uses the bundled Gemini CLI auth plugin", 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({ options.push({
value: "xiaomi-api-key", value: "xiaomi-api-key",
label: "Xiaomi API key", label: "Xiaomi API key",
+51 -18
View File
@@ -40,6 +40,7 @@ import {
applyXiaomiConfig, applyXiaomiConfig,
applyXiaomiProviderConfig, applyXiaomiProviderConfig,
applyZaiConfig, applyZaiConfig,
applyZaiProviderConfig,
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
LITELLM_DEFAULT_MODEL_REF, LITELLM_DEFAULT_MODEL_REF,
QIANFAN_DEFAULT_MODEL_REF, QIANFAN_DEFAULT_MODEL_REF,
@@ -619,7 +620,54 @@ export async function applyAuthChoiceApiProviders(
return { config: nextConfig, agentModelOverride }; 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; let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "zai") { if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "zai") {
@@ -655,23 +703,8 @@ export async function applyAuthChoiceApiProviders(
config: nextConfig, config: nextConfig,
setDefaultModel: params.setDefaultModel, setDefaultModel: params.setDefaultModel,
defaultModel: ZAI_DEFAULT_MODEL_REF, defaultModel: ZAI_DEFAULT_MODEL_REF,
applyDefaultConfig: applyZaiConfig, applyDefaultConfig: (config) => applyZaiConfig(config, { endpoint }),
applyProviderConfig: (config) => ({ applyProviderConfig: (config) => applyZaiProviderConfig(config, { endpoint }),
...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",
},
},
},
},
}),
noteDefault: ZAI_DEFAULT_MODEL_REF, noteDefault: ZAI_DEFAULT_MODEL_REF,
noteAgentModel, noteAgentModel,
prompter: params.prompter, prompter: params.prompter,
@@ -20,6 +20,10 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
"google-antigravity": "google-antigravity", "google-antigravity": "google-antigravity",
"google-gemini-cli": "google-gemini-cli", "google-gemini-cli": "google-gemini-cli",
"zai-api-key": "zai", "zai-api-key": "zai",
"zai-coding-global": "zai",
"zai-coding-cn": "zai",
"zai-global": "zai",
"zai-cn": "zai",
"xiaomi-api-key": "xiaomi", "xiaomi-api-key": "xiaomi",
"synthetic-api-key": "synthetic", "synthetic-api-key": "synthetic",
"venice-api-key": "venice", "venice-api-key": "venice",
+96
View File
@@ -6,6 +6,7 @@ import type { RuntimeEnv } from "../runtime.js";
import type { WizardPrompter } from "../wizard/prompts.js"; import type { WizardPrompter } from "../wizard/prompts.js";
import type { AuthChoice } from "./onboard-types.js"; import type { AuthChoice } from "./onboard-types.js";
import { applyAuthChoice, resolvePreferredProviderForAuthChoice } from "./auth-choice.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", () => ({ vi.mock("../providers/github-copilot-auth.js", () => ({
githubCopilotLoginCommand: vi.fn(async () => {}), githubCopilotLoginCommand: vi.fn(async () => {}),
@@ -199,6 +200,101 @@ describe("applyAuthChoice", () => {
expect(parsed.profiles?.["synthetic:default"]?.key).toBe("sk-synthetic-test"); 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 () => { 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-")); tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir; process.env.OPENCLAW_STATE_DIR = tempStateDir;
+78 -6
View File
@@ -38,6 +38,7 @@ import {
XAI_DEFAULT_MODEL_REF, XAI_DEFAULT_MODEL_REF,
} from "./onboard-auth.credentials.js"; } from "./onboard-auth.credentials.js";
import { import {
buildZaiModelDefinition,
buildMoonshotModelDefinition, buildMoonshotModelDefinition,
buildXaiModelDefinition, buildXaiModelDefinition,
QIANFAN_BASE_URL, QIANFAN_BASE_URL,
@@ -47,18 +48,65 @@ import {
MOONSHOT_CN_BASE_URL, MOONSHOT_CN_BASE_URL,
MOONSHOT_DEFAULT_MODEL_ID, MOONSHOT_DEFAULT_MODEL_ID,
MOONSHOT_DEFAULT_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF,
ZAI_DEFAULT_MODEL_ID,
resolveZaiBaseUrl,
XAI_BASE_URL, XAI_BASE_URL,
XAI_DEFAULT_MODEL_ID, XAI_DEFAULT_MODEL_ID,
} from "./onboard-auth.models.js"; } 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 }; const models = { ...cfg.agents?.defaults?.models };
models[ZAI_DEFAULT_MODEL_REF] = { models[modelRef] = {
...models[ZAI_DEFAULT_MODEL_REF], ...models[modelRef],
alias: models[ZAI_DEFAULT_MODEL_REF]?.alias ?? "GLM", 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 { return {
...cfg, ...cfg,
agents: { agents: {
@@ -66,13 +114,37 @@ export function applyZaiConfig(cfg: OpenClawConfig): OpenClawConfig {
defaults: { defaults: {
...cfg.agents?.defaults, ...cfg.agents?.defaults,
models, 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: { model: {
...(existingModel && "fallbacks" in (existingModel as Record<string, unknown>) ...(existingModel && "fallbacks" in (existingModel as Record<string, unknown>)
? { ? {
fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks,
} }
: undefined), : undefined),
primary: ZAI_DEFAULT_MODEL_REF, primary: modelRef,
}, },
}, },
}, },
+56
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 { QIANFAN_BASE_URL, QIANFAN_DEFAULT_MODEL_ID };
export const QIANFAN_DEFAULT_MODEL_REF = `qianfan/${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. // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
export const MINIMAX_API_COST = { export const MINIMAX_API_COST = {
input: 15, input: 15,
@@ -46,6 +66,13 @@ export const MOONSHOT_DEFAULT_COST = {
cacheWrite: 0, cacheWrite: 0,
}; };
export const ZAI_DEFAULT_COST = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
};
const MINIMAX_MODEL_CATALOG = { const MINIMAX_MODEL_CATALOG = {
"MiniMax-M2.1": { name: "MiniMax M2.1", reasoning: false }, "MiniMax-M2.1": { name: "MiniMax M2.1", reasoning: false },
"MiniMax-M2.1-lightning": { "MiniMax-M2.1-lightning": {
@@ -56,6 +83,15 @@ const MINIMAX_MODEL_CATALOG = {
type MinimaxCatalogId = keyof typeof 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: { export function buildMinimaxModelDefinition(params: {
id: string; id: string;
name?: 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_BASE_URL = "https://api.x.ai/v1";
export const XAI_DEFAULT_MODEL_ID = "grok-4"; export const XAI_DEFAULT_MODEL_ID = "grok-4";
export const XAI_DEFAULT_MODEL_REF = `xai/${XAI_DEFAULT_MODEL_ID}`; export const XAI_DEFAULT_MODEL_REF = `xai/${XAI_DEFAULT_MODEL_ID}`;
+45
View File
@@ -18,12 +18,16 @@ import {
applyXaiProviderConfig, applyXaiProviderConfig,
applyXiaomiConfig, applyXiaomiConfig,
applyXiaomiProviderConfig, applyXiaomiProviderConfig,
applyZaiConfig,
applyZaiProviderConfig,
OPENROUTER_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF,
SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_ID,
SYNTHETIC_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF,
XAI_DEFAULT_MODEL_REF, XAI_DEFAULT_MODEL_REF,
setMinimaxApiKey, setMinimaxApiKey,
writeOAuthCredentials, writeOAuthCredentials,
ZAI_CODING_CN_BASE_URL,
ZAI_CODING_GLOBAL_BASE_URL,
} from "./onboard-auth.js"; } from "./onboard-auth.js";
const authProfilePathFor = (agentDir: string) => path.join(agentDir, "auth-profiles.json"); 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", () => { describe("applySyntheticConfig", () => {
it("adds synthetic provider with correct settings", () => { it("adds synthetic provider with correct settings", () => {
const cfg = applySyntheticConfig({}); const cfg = applySyntheticConfig({});
+8
View File
@@ -32,6 +32,7 @@ export {
applyXiaomiConfig, applyXiaomiConfig,
applyXiaomiProviderConfig, applyXiaomiProviderConfig,
applyZaiConfig, applyZaiConfig,
applyZaiProviderConfig,
} from "./onboard-auth.config-core.js"; } from "./onboard-auth.config-core.js";
export { export {
applyMinimaxApiConfig, applyMinimaxApiConfig,
@@ -78,6 +79,7 @@ export {
buildMinimaxApiModelDefinition, buildMinimaxApiModelDefinition,
buildMinimaxModelDefinition, buildMinimaxModelDefinition,
buildMoonshotModelDefinition, buildMoonshotModelDefinition,
buildZaiModelDefinition,
DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_BASE_URL,
MOONSHOT_CN_BASE_URL, MOONSHOT_CN_BASE_URL,
QIANFAN_BASE_URL, QIANFAN_BASE_URL,
@@ -91,4 +93,10 @@ export {
MOONSHOT_BASE_URL, MOONSHOT_BASE_URL,
MOONSHOT_DEFAULT_MODEL_ID, MOONSHOT_DEFAULT_MODEL_ID,
MOONSHOT_DEFAULT_MODEL_REF, 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"; } from "./onboard-auth.models.js";
@@ -139,6 +139,60 @@ async function expectApiKeyProfile(params: {
} }
describe("onboard (non-interactive): provider auth", () => { 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 () => { it("stores xAI API key and sets default model", async () => {
await withOnboardEnv("openclaw-onboard-xai-", async ({ configPath, runtime }) => { await withOnboardEnv("openclaw-onboard-xai-", async ({ configPath, runtime }) => {
await runNonInteractive( await runNonInteractive(
@@ -187,7 +187,13 @@ export async function applyNonInteractiveAuthChoice(params: {
return applyGoogleGeminiModelDefault(nextConfig).next; 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({ const resolved = await resolveNonInteractiveApiKey({
provider: "zai", provider: "zai",
cfg: baseConfig, cfg: baseConfig,
@@ -207,7 +213,21 @@ export async function applyNonInteractiveAuthChoice(params: {
provider: "zai", provider: "zai",
mode: "api_key", 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") { if (authChoice === "xiaomi-api-key") {
+4
View File
@@ -27,6 +27,10 @@ export type AuthChoice =
| "google-antigravity" | "google-antigravity"
| "google-gemini-cli" | "google-gemini-cli"
| "zai-api-key" | "zai-api-key"
| "zai-coding-global"
| "zai-coding-cn"
| "zai-global"
| "zai-cn"
| "xiaomi-api-key" | "xiaomi-api-key"
| "minimax-cloud" | "minimax-cloud"
| "minimax" | "minimax"