Skip to content

Commit c79c4ff

Browse files
authored
fix(zai): align explicit coding endpoint setup with detected model defaults (openclaw#45969)
* fix: align Z.AI coding onboarding with endpoint docs * fix: align Z.AI coding onboarding with endpoint docs (openclaw#45969)
1 parent 439c21e commit c79c4ff

9 files changed

Lines changed: 211 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Docs: https://docs.openclaw.ai
99
- Placeholder: replace with the first 2026.3.14 user-facing change.
1010
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
1111

12+
### Fixes
13+
14+
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969)
15+
1216
## 2026.3.13
1317

1418
### Changes

docs/providers/glm.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,17 @@ models are accessed via the `zai` provider and model IDs like `zai/glm-5`.
1414
## CLI setup
1515

1616
```bash
17-
openclaw onboard --auth-choice zai-api-key
17+
# Coding Plan Global, recommended for Coding Plan users
18+
openclaw onboard --auth-choice zai-coding-global
19+
20+
# Coding Plan CN (China region), recommended for Coding Plan users
21+
openclaw onboard --auth-choice zai-coding-cn
22+
23+
# General API
24+
openclaw onboard --auth-choice zai-global
25+
26+
# General API CN (China region)
27+
openclaw onboard --auth-choice zai-cn
1828
```
1929

2030
## Config snippet

docs/providers/zai.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,17 @@ with a Z.AI API key.
1515
## CLI setup
1616

1717
```bash
18-
openclaw onboard --auth-choice zai-api-key
19-
# or non-interactive
20-
openclaw onboard --zai-api-key "$ZAI_API_KEY"
18+
# Coding Plan Global, recommended for Coding Plan users
19+
openclaw onboard --auth-choice zai-coding-global
20+
21+
# Coding Plan CN (China region), recommended for Coding Plan users
22+
openclaw onboard --auth-choice zai-coding-cn
23+
24+
# General API
25+
openclaw onboard --auth-choice zai-global
26+
27+
# General API CN (China region)
28+
openclaw onboard --auth-choice zai-cn
2129
```
2230

2331
## Config snippet

src/commands/auth-choice.apply.api-providers.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,15 @@ export async function applyAuthChoiceApiProviders(
245245
setZaiApiKey(apiKey, params.agentDir, { secretInputMode: mode }),
246246
});
247247

248-
// zai-api-key: auto-detect endpoint + choose a working default model.
249248
let modelIdOverride: string | undefined;
250-
if (!endpoint) {
249+
if (endpoint) {
250+
const detected = await detectZaiEndpoint({ apiKey, endpoint });
251+
if (detected) {
252+
modelIdOverride = detected.modelId;
253+
await params.prompter.note(detected.note, "Z.AI endpoint");
254+
}
255+
} else {
256+
// zai-api-key: auto-detect endpoint + choose a working default model.
251257
const detected = await detectZaiEndpoint({ apiKey });
252258
if (detected) {
253259
endpoint = detected.endpoint;

src/commands/auth-choice.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ describe("applyAuthChoice", () => {
285285
expectedBaseUrl: string;
286286
expectedModel?: string;
287287
shouldPromptForEndpoint: boolean;
288-
shouldAssertDetectCall?: boolean;
288+
expectedDetectCall?: { apiKey: string; endpoint?: "coding-global" | "coding-cn" };
289289
}> = [
290290
{
291291
authChoice: "zai-api-key",
@@ -298,8 +298,16 @@ describe("applyAuthChoice", () => {
298298
{
299299
authChoice: "zai-coding-global",
300300
token: "zai-test-key",
301+
detectResult: {
302+
endpoint: "coding-global",
303+
modelId: "glm-4.7",
304+
baseUrl: ZAI_CODING_GLOBAL_BASE_URL,
305+
note: "Detected coding-global endpoint with GLM-4.7 fallback",
306+
},
301307
expectedBaseUrl: ZAI_CODING_GLOBAL_BASE_URL,
308+
expectedModel: "zai/glm-4.7",
302309
shouldPromptForEndpoint: false,
310+
expectedDetectCall: { apiKey: "zai-test-key", endpoint: "coding-global" },
303311
},
304312
{
305313
authChoice: "zai-api-key",
@@ -313,7 +321,7 @@ describe("applyAuthChoice", () => {
313321
expectedBaseUrl: ZAI_CODING_GLOBAL_BASE_URL,
314322
expectedModel: "zai/glm-4.5",
315323
shouldPromptForEndpoint: false,
316-
shouldAssertDetectCall: true,
324+
expectedDetectCall: { apiKey: "zai-detected-key" },
317325
},
318326
];
319327
for (const scenario of scenarios) {
@@ -344,8 +352,8 @@ describe("applyAuthChoice", () => {
344352
setDefaultModel: true,
345353
});
346354

347-
if (scenario.shouldAssertDetectCall) {
348-
expect(detectZaiEndpoint).toHaveBeenCalledWith({ apiKey: scenario.token });
355+
if (scenario.expectedDetectCall) {
356+
expect(detectZaiEndpoint).toHaveBeenCalledWith(scenario.expectedDetectCall);
349357
}
350358
if (scenario.shouldPromptForEndpoint) {
351359
expect(select).toHaveBeenCalledWith(

src/commands/onboard-non-interactive.provider-auth.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
33
import { setTimeout as delay } from "node:timers/promises";
4-
import { beforeAll, describe, expect, it, vi } from "vitest";
4+
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
55
import { makeTempWorkspace } from "../test-helpers/workspace.js";
66
import { withEnvAsync } from "../test-utils/env.js";
77
import { MINIMAX_API_BASE_URL, MINIMAX_CN_API_BASE_URL } from "./onboard-auth.js";
@@ -18,6 +18,8 @@ type OnboardEnv = {
1818
};
1919

2020
const ensureWorkspaceAndSessionsMock = vi.hoisted(() => vi.fn(async (..._args: unknown[]) => {}));
21+
type DetectZaiEndpoint = typeof import("./zai-endpoint-detect.js").detectZaiEndpoint;
22+
const detectZaiEndpoint = vi.hoisted(() => vi.fn<DetectZaiEndpoint>(async () => null));
2123

2224
vi.mock("./onboard-helpers.js", async (importOriginal) => {
2325
const actual = await importOriginal<typeof import("./onboard-helpers.js")>();
@@ -27,6 +29,10 @@ vi.mock("./onboard-helpers.js", async (importOriginal) => {
2729
};
2830
});
2931

32+
vi.mock("./zai-endpoint-detect.js", () => ({
33+
detectZaiEndpoint,
34+
}));
35+
3036
const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js");
3137

3238
const NON_INTERACTIVE_DEFAULT_OPTIONS = {
@@ -180,6 +186,11 @@ describe("onboard (non-interactive): provider auth", () => {
180186
({ ensureAuthProfileStore, upsertAuthProfile } = await import("../agents/auth-profiles.js"));
181187
});
182188

189+
beforeEach(() => {
190+
detectZaiEndpoint.mockReset();
191+
detectZaiEndpoint.mockResolvedValue(null);
192+
});
193+
183194
it("stores MiniMax API key and uses global baseUrl by default", async () => {
184195
await withOnboardEnv("openclaw-onboard-minimax-", async (env) => {
185196
const cfg = await runOnboardingAndReadConfig(env, {
@@ -220,6 +231,12 @@ describe("onboard (non-interactive): provider auth", () => {
220231

221232
it("stores Z.AI API key and uses global baseUrl by default", async () => {
222233
await withOnboardEnv("openclaw-onboard-zai-", async (env) => {
234+
detectZaiEndpoint.mockResolvedValueOnce({
235+
endpoint: "global",
236+
baseUrl: "https://api.z.ai/api/paas/v4",
237+
modelId: "glm-5",
238+
note: "Verified GLM-5 on global endpoint.",
239+
});
223240
const cfg = await runOnboardingAndReadConfig(env, {
224241
authChoice: "zai-api-key",
225242
zaiApiKey: "zai-test-key", // pragma: allowlist secret
@@ -235,6 +252,12 @@ describe("onboard (non-interactive): provider auth", () => {
235252

236253
it("supports Z.AI CN coding endpoint auth choice", async () => {
237254
await withOnboardEnv("openclaw-onboard-zai-cn-", async (env) => {
255+
detectZaiEndpoint.mockResolvedValueOnce({
256+
endpoint: "coding-cn",
257+
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
258+
modelId: "glm-4.7",
259+
note: "Coding Plan CN endpoint verified, but this key/plan does not expose GLM-5 there. Defaulting to GLM-4.7.",
260+
});
238261
const cfg = await runOnboardingAndReadConfig(env, {
239262
authChoice: "zai-coding-cn",
240263
zaiApiKey: "zai-test-key", // pragma: allowlist secret
@@ -243,6 +266,25 @@ describe("onboard (non-interactive): provider auth", () => {
243266
expect(cfg.models?.providers?.zai?.baseUrl).toBe(
244267
"https://open.bigmodel.cn/api/coding/paas/v4",
245268
);
269+
expect(cfg.agents?.defaults?.model?.primary).toBe("zai/glm-4.7");
270+
});
271+
});
272+
273+
it("supports Z.AI Coding Plan global endpoint with GLM-5 when available", async () => {
274+
await withOnboardEnv("openclaw-onboard-zai-coding-global-", async (env) => {
275+
detectZaiEndpoint.mockResolvedValueOnce({
276+
endpoint: "coding-global",
277+
baseUrl: "https://api.z.ai/api/coding/paas/v4",
278+
modelId: "glm-5",
279+
note: "Verified GLM-5 on coding-global endpoint.",
280+
});
281+
const cfg = await runOnboardingAndReadConfig(env, {
282+
authChoice: "zai-coding-global",
283+
zaiApiKey: "zai-test-key", // pragma: allowlist secret
284+
});
285+
286+
expect(cfg.models?.providers?.zai?.baseUrl).toBe("https://api.z.ai/api/coding/paas/v4");
287+
expect(cfg.agents?.defaults?.model?.primary).toBe("zai/glm-5");
246288
});
247289
});
248290

src/commands/onboard-non-interactive/local/auth-choice.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,13 @@ export async function applyNonInteractiveAuthChoice(params: {
291291
endpoint = "global";
292292
} else if (authChoice === "zai-cn") {
293293
endpoint = "cn";
294+
}
295+
296+
if (endpoint) {
297+
const detected = await detectZaiEndpoint({ apiKey: resolved.key, endpoint });
298+
if (detected) {
299+
modelIdOverride = detected.modelId;
300+
}
294301
} else {
295302
const detected = await detectZaiEndpoint({ apiKey: resolved.key });
296303
if (detected) {

src/commands/zai-endpoint-detect.test.ts

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { describe, expect, it } from "vitest";
22
import { detectZaiEndpoint } from "./zai-endpoint-detect.js";
33

4-
function makeFetch(map: Record<string, { status: number; body?: unknown }>) {
5-
return (async (url: string) => {
6-
const entry = map[url];
4+
type FetchResponse = { status: number; body?: unknown };
5+
6+
function makeFetch(map: Record<string, FetchResponse>) {
7+
return (async (url: string, init?: RequestInit) => {
8+
const rawBody = typeof init?.body === "string" ? JSON.parse(init.body) : null;
9+
const entry = map[`${url}::${rawBody?.model ?? ""}`] ?? map[url];
710
if (!entry) {
8-
throw new Error(`unexpected url: ${url}`);
11+
throw new Error(`unexpected url: ${url} model=${String(rawBody?.model ?? "")}`);
912
}
1013
const json = entry.body ?? {};
1114
return new Response(JSON.stringify(json), {
@@ -18,39 +21,71 @@ function makeFetch(map: Record<string, { status: number; body?: unknown }>) {
1821
describe("detectZaiEndpoint", () => {
1922
it("resolves preferred/fallback endpoints and null when probes fail", async () => {
2023
const scenarios: Array<{
24+
endpoint?: "global" | "cn" | "coding-global" | "coding-cn";
2125
responses: Record<string, { status: number; body?: unknown }>;
2226
expected: { endpoint: string; modelId: string } | null;
2327
}> = [
2428
{
2529
responses: {
26-
"https://api.z.ai/api/paas/v4/chat/completions": { status: 200 },
30+
"https://api.z.ai/api/paas/v4/chat/completions::glm-5": { status: 200 },
2731
},
2832
expected: { endpoint: "global", modelId: "glm-5" },
2933
},
3034
{
3135
responses: {
32-
"https://api.z.ai/api/paas/v4/chat/completions": {
36+
"https://api.z.ai/api/paas/v4/chat/completions::glm-5": {
3337
status: 404,
3438
body: { error: { message: "not found" } },
3539
},
36-
"https://open.bigmodel.cn/api/paas/v4/chat/completions": { status: 200 },
40+
"https://open.bigmodel.cn/api/paas/v4/chat/completions::glm-5": { status: 200 },
3741
},
3842
expected: { endpoint: "cn", modelId: "glm-5" },
3943
},
4044
{
4145
responses: {
42-
"https://api.z.ai/api/paas/v4/chat/completions": { status: 404 },
43-
"https://open.bigmodel.cn/api/paas/v4/chat/completions": { status: 404 },
44-
"https://api.z.ai/api/coding/paas/v4/chat/completions": { status: 200 },
46+
"https://api.z.ai/api/paas/v4/chat/completions::glm-5": { status: 404 },
47+
"https://open.bigmodel.cn/api/paas/v4/chat/completions::glm-5": { status: 404 },
48+
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5": { status: 200 },
49+
},
50+
expected: { endpoint: "coding-global", modelId: "glm-5" },
51+
},
52+
{
53+
endpoint: "coding-global",
54+
responses: {
55+
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5": {
56+
status: 404,
57+
body: { error: { message: "glm-5 unavailable" } },
58+
},
59+
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-4.7": { status: 200 },
4560
},
4661
expected: { endpoint: "coding-global", modelId: "glm-4.7" },
4762
},
63+
{
64+
endpoint: "coding-cn",
65+
responses: {
66+
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-5": { status: 200 },
67+
},
68+
expected: { endpoint: "coding-cn", modelId: "glm-5" },
69+
},
70+
{
71+
endpoint: "coding-cn",
72+
responses: {
73+
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-5": {
74+
status: 404,
75+
body: { error: { message: "glm-5 unavailable" } },
76+
},
77+
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-4.7": { status: 200 },
78+
},
79+
expected: { endpoint: "coding-cn", modelId: "glm-4.7" },
80+
},
4881
{
4982
responses: {
50-
"https://api.z.ai/api/paas/v4/chat/completions": { status: 401 },
51-
"https://open.bigmodel.cn/api/paas/v4/chat/completions": { status: 401 },
52-
"https://api.z.ai/api/coding/paas/v4/chat/completions": { status: 401 },
53-
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions": { status: 401 },
83+
"https://api.z.ai/api/paas/v4/chat/completions::glm-5": { status: 401 },
84+
"https://open.bigmodel.cn/api/paas/v4/chat/completions::glm-5": { status: 401 },
85+
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5": { status: 401 },
86+
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-4.7": { status: 401 },
87+
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-5": { status: 401 },
88+
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-4.7": { status: 401 },
5489
},
5590
expected: null,
5691
},
@@ -59,6 +94,7 @@ describe("detectZaiEndpoint", () => {
5994
for (const scenario of scenarios) {
6095
const detected = await detectZaiEndpoint({
6196
apiKey: "sk-test", // pragma: allowlist secret
97+
...(scenario.endpoint ? { endpoint: scenario.endpoint } : {}),
6298
fetchFn: makeFetch(scenario.responses),
6399
});
64100

0 commit comments

Comments
 (0)