mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -73,6 +73,7 @@ docs/.local/
|
||||
IDENTITY.md
|
||||
USER.md
|
||||
.tgz
|
||||
.idea
|
||||
|
||||
# local tooling
|
||||
.serena/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"];
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user