Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled

Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
2026-07-05 09:36:54 +00:00
parent 3216769225
commit bedb527145
21108 changed files with 6010766 additions and 0 deletions

13
extensions/zai/README.md Normal file
View File

@@ -0,0 +1,13 @@
# Z.AI OpenClaw provider
Official OpenClaw provider plugin for Z.AI.
## Install
```sh
openclaw plugins install @openclaw/zai-provider
```
## Docs
See `docs/providers/zai.md` in the OpenClaw repository, or the published docs at `https://docs.openclaw.ai/providers/zai`.

13
extensions/zai/api.ts Normal file
View File

@@ -0,0 +1,13 @@
// Zai API module exposes the plugin public contract.
export { applyZaiConfig, applyZaiProviderConfig, ZAI_DEFAULT_MODEL_REF } from "./onboard.js";
export { detectZaiEndpoint, type ZaiDetectedEndpoint, type ZaiEndpointId } from "./detect.js";
export {
buildZaiModelDefinition,
resolveZaiBaseUrl,
ZAI_CN_BASE_URL,
ZAI_CODING_CN_BASE_URL,
ZAI_CODING_GLOBAL_BASE_URL,
ZAI_DEFAULT_COST,
ZAI_DEFAULT_MODEL_ID,
ZAI_GLOBAL_BASE_URL,
} from "./model-definitions.js";

View File

@@ -0,0 +1,12 @@
// Narrow config barrel for core/test consumers.
// Keep this separate from provider registration/runtime imports.
export { applyZaiConfig, applyZaiProviderConfig, ZAI_DEFAULT_MODEL_REF } from "./onboard.js";
export {
ZAI_CN_BASE_URL,
ZAI_CODING_CN_BASE_URL,
ZAI_CODING_GLOBAL_BASE_URL,
ZAI_DEFAULT_COST,
ZAI_DEFAULT_MODEL_ID,
ZAI_GLOBAL_BASE_URL,
} from "./model-definitions.js";

View File

@@ -0,0 +1,338 @@
// Zai tests cover detect plugin behavior.
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
import { afterEach, describe, expect, it, vi } from "vitest";
import { detectZaiEndpoint } from "./detect.js";
type FetchResponse = { status: number; body?: unknown };
const ZAI_DETECT_ERROR_BODY_MAX_BYTES = 16 * 1024 * 1024;
/**
* Builds a streaming error Response whose body is far larger than the 16 MiB cap.
* Tracks how many bytes were actually pulled and whether the consumer cancelled
* the stream, so tests can prove the read is bounded (fail-closed) rather than
* draining the whole untrusted body into memory.
*/
function makeOversizedStreamFetch(params: {
url: string;
status: number;
chunkBytes?: number;
hardCeilingBytes?: number;
}) {
const chunkBytes = params.chunkBytes ?? 1024 * 1024;
const hardCeilingBytes = params.hardCeilingBytes ?? 64 * 1024 * 1024;
const state = { enqueuedBytes: 0, cancelled: false };
const fetchFn = (async (url: string) => {
if (url !== params.url) {
throw new Error(`unexpected url: ${url}`);
}
const body = new ReadableStream<Uint8Array>({
pull(controller) {
if (state.enqueuedBytes >= hardCeilingBytes) {
// Safety stop: with an unbounded reader this point would be reached
// (and the test would fail on the bounded-bytes assertion below).
controller.close();
return;
}
state.enqueuedBytes += chunkBytes;
controller.enqueue(new Uint8Array(chunkBytes));
},
cancel() {
state.cancelled = true;
},
});
return new Response(body, {
status: params.status,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;
return { fetchFn, state };
}
/**
* Builds a fetch returning a single raw (possibly non-JSON) error body, keyed by
* `${url}::${model}`. Used to drive the new bounded decode path with small,
* well-formed, empty, and malformed sub-cap bodies that must behave exactly as
* the previous `res.json()` path did.
*/
function makeRawBodyFetch(map: Record<string, { status: number; raw: string }>) {
return (async (url: string, init?: RequestInit) => {
const rawBody = typeof init?.body === "string" ? JSON.parse(init.body) : null;
const entry = map[`${url}::${rawBody?.model ?? ""}`] ?? map[url];
if (!entry) {
throw new Error(`unexpected url: ${url} model=${String(rawBody?.model ?? "")}`);
}
return new Response(entry.raw, {
status: entry.status,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;
}
function makeFetch(map: Record<string, FetchResponse>) {
return (async (url: string, init?: RequestInit) => {
const rawBody = typeof init?.body === "string" ? JSON.parse(init.body) : null;
const entry = map[`${url}::${rawBody?.model ?? ""}`] ?? map[url];
if (!entry) {
throw new Error(`unexpected url: ${url} model=${String(rawBody?.model ?? "")}`);
}
const json = entry.body ?? {};
return new Response(JSON.stringify(json), {
status: entry.status,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;
}
describe("detectZaiEndpoint", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("resolves preferred/fallback endpoints and null when probes fail", async () => {
const scenarios: Array<{
endpoint?: "global" | "cn" | "coding-global" | "coding-cn";
responses: Record<string, { status: number; body?: unknown }>;
expected: { endpoint: string; modelId: string } | null;
}> = [
{
responses: {
"https://api.z.ai/api/paas/v4/chat/completions::glm-5.1": { status: 200 },
},
expected: { endpoint: "global", modelId: "glm-5.1" },
},
{
responses: {
"https://api.z.ai/api/paas/v4/chat/completions::glm-5.1": { status: 404 },
"https://open.bigmodel.cn/api/paas/v4/chat/completions::glm-5.1": { status: 200 },
},
expected: { endpoint: "cn", modelId: "glm-5.1" },
},
{
responses: {
"https://api.z.ai/api/paas/v4/chat/completions::glm-5.1": { status: 404 },
"https://open.bigmodel.cn/api/paas/v4/chat/completions::glm-5.1": { status: 404 },
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5.2": { status: 200 },
},
expected: { endpoint: "coding-global", modelId: "glm-5.2" },
},
{
endpoint: "coding-global",
responses: {
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5.2": {
status: 404,
body: { error: { message: "glm-5.2 unavailable" } },
},
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5.1": {
status: 404,
body: { error: { message: "glm-5.1 unavailable" } },
},
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-4.7": { status: 200 },
},
expected: { endpoint: "coding-global", modelId: "glm-4.7" },
},
{
endpoint: "coding-global",
responses: {
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5.2": {
status: 400,
body: { code: 1311, msg: "model not included in the current plan" },
},
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5.1": {
status: 400,
body: { code: 1211, msg: "model does not exist" },
},
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-4.7": { status: 200 },
},
expected: { endpoint: "coding-global", modelId: "glm-4.7" },
},
{
endpoint: "coding-global",
responses: {
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5.2": {
status: 429,
body: { error: { message: "rate limited" } },
},
},
expected: null,
},
{
endpoint: "coding-cn",
responses: {
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-5.2": {
status: 200,
},
},
expected: { endpoint: "coding-cn", modelId: "glm-5.2" },
},
{
endpoint: "coding-cn",
responses: {
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-5.2": {
status: 404,
},
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-5.1": {
status: 200,
},
},
expected: { endpoint: "coding-cn", modelId: "glm-5.1" },
},
{
endpoint: "coding-cn",
responses: {
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-5.2": {
status: 404,
body: { error: { message: "glm-5.2 unavailable" } },
},
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-5.1": {
status: 404,
body: { error: { message: "glm-5.1 unavailable" } },
},
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-4.7": {
status: 200,
},
},
expected: { endpoint: "coding-cn", modelId: "glm-4.7" },
},
{
responses: {
"https://api.z.ai/api/paas/v4/chat/completions::glm-5.1": { status: 401 },
"https://open.bigmodel.cn/api/paas/v4/chat/completions::glm-5.1": { status: 401 },
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5.2": { status: 401 },
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-5.1": { status: 401 },
"https://api.z.ai/api/coding/paas/v4/chat/completions::glm-4.7": { status: 401 },
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-5.2": {
status: 401,
},
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-5.1": {
status: 401,
},
"https://open.bigmodel.cn/api/coding/paas/v4/chat/completions::glm-4.7": {
status: 401,
},
},
expected: null,
},
];
for (const scenario of scenarios) {
const detected = await detectZaiEndpoint({
apiKey: "sk-test", // pragma: allowlist secret
...(scenario.endpoint ? { endpoint: scenario.endpoint } : {}),
fetchFn: makeFetch(scenario.responses),
});
if (scenario.expected === null) {
expect(detected).toBeNull();
} else {
expect(detected?.endpoint).toBe(scenario.expected.endpoint);
expect(detected?.modelId).toBe(scenario.expected.modelId);
}
}
});
it("caps oversized probe timeouts before scheduling", async () => {
const timeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockReturnValue(1 as unknown as ReturnType<typeof setTimeout>);
vi.spyOn(globalThis, "clearTimeout").mockImplementation(() => undefined);
const fetchFn = makeFetch({
"https://api.z.ai/api/paas/v4/chat/completions::glm-5.1": { status: 200 },
});
await detectZaiEndpoint({
apiKey: "sk-test", // pragma: allowlist secret
fetchFn,
timeoutMs: MAX_TIMER_TIMEOUT_MS + 1_000_000,
});
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
});
it("still parses well-formed sub-cap error bodies to drive endpoint classification", async () => {
// Happy path: model-not-found errors must still be decoded from the bounded
// body so the probe classifies them as unsupported and walks to the GLM-4.7
// fallback. The error message that drives classification lives only inside
// the body, so a passing fallback proves the new bounded reader decoded it.
const codingGlobal = "https://api.z.ai/api/coding/paas/v4/chat/completions";
const detected = await detectZaiEndpoint({
apiKey: "sk-test", // pragma: allowlist secret
endpoint: "coding-global",
fetchFn: makeRawBodyFetch({
[`${codingGlobal}::glm-5.2`]: {
status: 400,
raw: JSON.stringify({ error: { message: "model not found for this plan" } }),
},
[`${codingGlobal}::glm-5.1`]: {
status: 400,
raw: JSON.stringify({ code: 1211, msg: "model does not exist" }),
},
[`${codingGlobal}::glm-4.7`]: { status: 200, raw: "{}" },
}),
});
expect(detected?.endpoint).toBe("coding-global");
expect(detected?.modelId).toBe("glm-4.7");
});
it("swallows malformed and empty sub-cap error bodies and falls back on status", async () => {
// Regression: a non-JSON or empty error body must not throw out of the
// probe. JSON.parse fails, the existing try/catch swallows it, and the
// probe degrades to status-only classification (404 => unsupported model),
// so the GLM-4.7 fallback still resolves exactly as before.
const codingGlobal = "https://api.z.ai/api/coding/paas/v4/chat/completions";
const detected = await detectZaiEndpoint({
apiKey: "sk-test", // pragma: allowlist secret
endpoint: "coding-global",
fetchFn: makeRawBodyFetch({
[`${codingGlobal}::glm-5.2`]: { status: 404, raw: "<html>gateway error</html>" },
[`${codingGlobal}::glm-5.1`]: { status: 404, raw: "" },
[`${codingGlobal}::glm-4.7`]: { status: 200, raw: "{}" },
}),
});
expect(detected?.endpoint).toBe("coding-global");
expect(detected?.modelId).toBe("glm-4.7");
});
it("fails closed on oversized probe error bodies without buffering unbounded", async () => {
const { fetchFn, state } = makeOversizedStreamFetch({
url: "https://api.z.ai/api/paas/v4/chat/completions",
status: 400,
});
const detected = await detectZaiEndpoint({
apiKey: "sk-test", // pragma: allowlist secret
endpoint: "global",
fetchFn,
});
// Probe swallows the bounded-read overflow and falls back to status-only,
// so the oversized error body cannot promote this endpoint.
expect(detected).toBeNull();
// The stream was cancelled (fail-closed) instead of being drained to the
// 64 MiB safety ceiling, proving the read stops near the 16 MiB cap.
expect(state.cancelled).toBe(true);
expect(state.enqueuedBytes).toBeLessThanOrEqual(
ZAI_DETECT_ERROR_BODY_MAX_BYTES + 2 * 1024 * 1024,
);
});
it("rejects oversized bodies via the shared bounded reader the probe uses", async () => {
const { fetchFn } = makeOversizedStreamFetch({
url: "https://api.z.ai/api/paas/v4/chat/completions",
status: 400,
});
const res = await fetchFn("https://api.z.ai/api/paas/v4/chat/completions");
await expect(
readResponseWithLimit(res, ZAI_DETECT_ERROR_BODY_MAX_BYTES, {
onOverflow: ({ maxBytes }) =>
new Error(`Z.AI probe error body exceeded size limit (${maxBytes} bytes)`),
}),
).rejects.toThrow(/exceeded size limit/);
});
});

263
extensions/zai/detect.ts Normal file
View File

@@ -0,0 +1,263 @@
// Zai plugin module implements detect behavior.
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
import {
ZAI_CN_BASE_URL,
ZAI_CODING_CN_BASE_URL,
ZAI_CODING_DEFAULT_MODEL_ID,
ZAI_CODING_GLOBAL_BASE_URL,
ZAI_DEFAULT_MODEL_ID,
ZAI_GLOBAL_BASE_URL,
} from "./model-definitions.js";
export type ZaiEndpointId = "global" | "cn" | "coding-global" | "coding-cn";
export type ZaiDetectedEndpoint = {
endpoint: ZaiEndpointId;
/** Provider baseUrl to store in config. */
baseUrl: string;
/** Recommended default model id for that endpoint. */
modelId: string;
/** Human-readable note explaining the choice. */
note: string;
};
type ProbeResult =
| { ok: true }
| {
ok: false;
status?: number;
errorCode?: string;
errorMessage?: string;
};
type ProbeCandidate = ZaiDetectedEndpoint & {
fallback?: boolean;
};
const UNSUPPORTED_MODEL_ERROR_CODES = new Set(["1211", "1311"]);
/** Cap for the Z.AI probe error body; bounds untrusted error responses to avoid unbounded buffering/OOM. */
const ZAI_DETECT_ERROR_BODY_MAX_BYTES = 16 * 1024 * 1024;
function isUnsupportedModelResult(result: ProbeResult): boolean {
if (result.ok) {
return false;
}
if (result.status === 404) {
return true;
}
if (result.errorCode && UNSUPPORTED_MODEL_ERROR_CODES.has(result.errorCode)) {
return true;
}
if (result.status !== 400) {
return false;
}
const detail = `${result.errorCode ?? ""} ${result.errorMessage ?? ""}`.toLowerCase();
return (
/\bmodel\b.*\b(not found|unavailable|unsupported|does not exist)\b/.test(detail) ||
/模型.*(不存在|不支持|不可用)/.test(detail)
);
}
async function fetchWithTimeoutLocal(
fetchFn: typeof fetch,
url: string,
init: RequestInit,
timeoutMs: number,
): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetchFn(url, { ...init, signal: controller.signal });
} finally {
clearTimeout(timeout);
}
}
async function probeZaiChatCompletions(params: {
baseUrl: string;
apiKey: string;
modelId: string;
timeoutMs: number;
fetchFn?: typeof fetch;
}): Promise<ProbeResult> {
try {
const fetchFn = params.fetchFn ?? globalThis.fetch;
const res = await fetchWithTimeoutLocal(
fetchFn,
`${params.baseUrl}/chat/completions`,
{
method: "POST",
headers: {
authorization: `Bearer ${params.apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify({
model: params.modelId,
stream: false,
max_tokens: 1,
messages: [{ role: "user", content: "ping" }],
}),
},
params.timeoutMs,
);
if (res.ok) {
return { ok: true };
}
let errorCode: string | undefined;
let errorMessage: string | undefined;
try {
const bytes = await readResponseWithLimit(res, ZAI_DETECT_ERROR_BODY_MAX_BYTES, {
onOverflow: ({ maxBytes }) =>
new Error(`Z.AI probe error body exceeded size limit (${maxBytes} bytes)`),
});
const json = JSON.parse(new TextDecoder().decode(bytes)) as {
error?: { code?: unknown; message?: unknown };
code?: unknown;
msg?: unknown;
message?: unknown;
};
const code = json?.error?.code ?? json?.code;
const msg = json?.error?.message ?? json?.msg ?? json?.message;
if (typeof code === "string") {
errorCode = code;
} else if (typeof code === "number") {
errorCode = String(code);
}
if (typeof msg === "string") {
errorMessage = msg;
}
} catch {
// ignore malformed error bodies
}
return { ok: false, status: res.status, errorCode, errorMessage };
} catch {
return { ok: false };
}
}
export async function detectZaiEndpoint(params: {
apiKey: string;
endpoint?: ZaiEndpointId;
timeoutMs?: number;
fetchFn?: typeof fetch;
}): Promise<ZaiDetectedEndpoint | null> {
// Never auto-probe in vitest; it would create flaky network behavior.
if (process.env.VITEST && !params.fetchFn) {
return null;
}
const timeoutMs = resolveTimerTimeoutMs(params.timeoutMs, 5_000);
const probeCandidates = (() => {
const general: ProbeCandidate[] = [
{
endpoint: "global" as const,
baseUrl: ZAI_GLOBAL_BASE_URL,
modelId: ZAI_DEFAULT_MODEL_ID,
note: "Verified GLM-5.1 on global endpoint.",
},
{
endpoint: "cn" as const,
baseUrl: ZAI_CN_BASE_URL,
modelId: ZAI_DEFAULT_MODEL_ID,
note: "Verified GLM-5.1 on cn endpoint.",
},
];
const codingModels: ProbeCandidate[] = [
{
endpoint: "coding-global" as const,
baseUrl: ZAI_CODING_GLOBAL_BASE_URL,
modelId: ZAI_CODING_DEFAULT_MODEL_ID,
note: "Verified GLM-5.2 on coding-global endpoint.",
},
{
endpoint: "coding-global" as const,
baseUrl: ZAI_CODING_GLOBAL_BASE_URL,
modelId: "glm-5.1",
note: "Verified GLM-5.1 on coding-global endpoint; GLM-5.2 is unavailable.",
fallback: true,
},
{
endpoint: "coding-cn" as const,
baseUrl: ZAI_CODING_CN_BASE_URL,
modelId: ZAI_CODING_DEFAULT_MODEL_ID,
note: "Verified GLM-5.2 on coding-cn endpoint.",
},
{
endpoint: "coding-cn" as const,
baseUrl: ZAI_CODING_CN_BASE_URL,
modelId: "glm-5.1",
note: "Verified GLM-5.1 on coding-cn endpoint; GLM-5.2 is unavailable.",
fallback: true,
},
];
const codingFallback: ProbeCandidate[] = [
{
endpoint: "coding-global" as const,
baseUrl: ZAI_CODING_GLOBAL_BASE_URL,
modelId: "glm-4.7",
note: "Coding Plan endpoint verified, but this key/plan does not expose GLM-5.2 or GLM-5.1 there. Defaulting to GLM-4.7.",
fallback: true,
},
{
endpoint: "coding-cn" as const,
baseUrl: ZAI_CODING_CN_BASE_URL,
modelId: "glm-4.7",
note: "Coding Plan CN endpoint verified, but this key/plan does not expose GLM-5.2 or GLM-5.1 there. Defaulting to GLM-4.7.",
fallback: true,
},
];
switch (params.endpoint) {
case "global":
return general.filter((candidate) => candidate.endpoint === "global");
case "cn":
return general.filter((candidate) => candidate.endpoint === "cn");
case "coding-global":
return [
...codingModels.filter((candidate) => candidate.endpoint === "coding-global"),
...codingFallback.filter((candidate) => candidate.endpoint === "coding-global"),
];
case "coding-cn":
return [
...codingModels.filter((candidate) => candidate.endpoint === "coding-cn"),
...codingFallback.filter((candidate) => candidate.endpoint === "coding-cn"),
];
default:
return [...general, ...codingModels, ...codingFallback];
}
})();
const resultsByEndpoint = new Map<ZaiEndpointId, ProbeResult[]>();
for (const candidate of probeCandidates) {
const priorResults = resultsByEndpoint.get(candidate.endpoint) ?? [];
if (
candidate.fallback &&
(priorResults.length === 0 || !priorResults.every(isUnsupportedModelResult))
) {
continue;
}
const result = await probeZaiChatCompletions({
baseUrl: candidate.baseUrl,
apiKey: params.apiKey,
modelId: candidate.modelId,
timeoutMs,
fetchFn: params.fetchFn,
});
if (result.ok) {
return {
endpoint: candidate.endpoint,
baseUrl: candidate.baseUrl,
modelId: candidate.modelId,
note: candidate.note,
};
}
resultsByEndpoint.set(candidate.endpoint, [...priorResults, result]);
}
return null;
}

View File

@@ -0,0 +1,587 @@
// Zai tests cover index plugin behavior.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { Context, Model } from "openclaw/plugin-sdk/llm";
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime";
import { describe, expect, it } from "vitest";
import plugin from "./index.js";
function createGlm47Template() {
return {
id: "glm-4.7",
name: "GLM-4.7",
provider: "zai",
api: "openai-completions",
baseUrl: "https://api.z.ai/api/paas/v4",
reasoning: true,
input: ["text"],
cost: { input: 0.6, output: 2.2, cacheRead: 0.11, cacheWrite: 0 },
contextWindow: 204800,
maxTokens: 131072,
};
}
function expectReplayPolicyFields(
policy: Record<string, unknown> | undefined,
fields: Record<string, unknown>,
): void {
if (!policy) {
throw new Error("Expected replay policy");
}
for (const [key, value] of Object.entries(fields)) {
expect(policy[key]).toEqual(value);
}
}
function expectModelFields(
model: Record<string, unknown> | undefined,
fields: Record<string, unknown>,
): void {
if (!model) {
throw new Error("Expected provider model");
}
for (const [key, value] of Object.entries(fields)) {
expect(model[key]).toEqual(value);
}
}
describe("zai provider plugin", () => {
it("owns replay policy for OpenAI-compatible Z.ai transports", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expectReplayPolicyFields(
provider.buildReplayPolicy?.({
provider: "zai",
modelApi: "openai-completions",
modelId: "glm-5.1",
} as never) as Record<string, unknown> | undefined,
{
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
},
);
expectReplayPolicyFields(
provider.buildReplayPolicy?.({
provider: "zai",
modelApi: "openai-responses",
modelId: "glm-5.1",
} as never) as Record<string, unknown> | undefined,
{
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
},
);
});
it("resolves persisted GLM-5 family models with provider-owned metadata", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const template = createGlm47Template();
const cases = [
{
modelId: "glm-5.2",
providerBaseUrl: "https://api.z.ai/api/coding/paas/v4",
expected: {
baseUrl: "https://api.z.ai/api/coding/paas/v4",
input: ["text"],
reasoning: true,
contextWindow: 1_000_000,
maxTokens: 131_072,
},
},
{
modelId: "glm-5.1",
providerBaseUrl: "https://api.z.ai/api/paas/v4",
expected: {
baseUrl: "https://api.z.ai/api/paas/v4",
input: ["text"],
reasoning: true,
contextWindow: 202800,
maxTokens: 131100,
},
},
{
modelId: "glm-5v-turbo",
providerBaseUrl: "https://api.z.ai/api/paas/v4",
expected: {
baseUrl: "https://api.z.ai/api/paas/v4",
input: ["text", "image"],
reasoning: true,
contextWindow: 202800,
maxTokens: 131100,
},
},
] as const;
for (const testCase of cases) {
const resolved = provider.resolveDynamicModel?.({
provider: "zai",
modelId: testCase.modelId,
modelRegistry: {
find: (_provider: string, modelId: string) => (modelId === "glm-4.7" ? template : null),
},
providerConfig: { baseUrl: testCase.providerBaseUrl },
} as never) as Record<string, unknown> | undefined;
expectModelFields(resolved, {
provider: "zai",
api: "openai-completions",
id: testCase.modelId,
...testCase.expected,
});
}
});
it("keeps selected Coding Plan and proxy endpoints for dynamic GLM-5 models", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const template = createGlm47Template();
for (const baseUrl of [
"https://open.bigmodel.cn/api/coding/paas/v4",
"https://proxy.example.test/zai",
]) {
const resolved = provider.resolveDynamicModel?.({
provider: "zai",
modelId: "glm-5.2",
modelRegistry: {
find: (_provider: string, modelId: string) => (modelId === "glm-4.7" ? template : null),
},
providerConfig: { baseUrl },
} as never) as Record<string, unknown> | undefined;
expect(resolved?.baseUrl).toBe(baseUrl);
}
});
it("returns an already-registered GLM-5 variant as-is", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const registered = {
id: "glm-5-turbo",
name: "GLM-5-Turbo",
provider: "zai",
api: "openai-completions",
baseUrl: "https://api.z.ai/api/paas/v4",
reasoning: false,
input: ["text"],
cost: { input: 0.1, output: 0.2, cacheRead: 0, cacheWrite: 0 },
contextWindow: 123456,
maxTokens: 54321,
};
const template = createGlm47Template();
expect(
provider.resolveDynamicModel?.({
provider: "zai",
modelId: "glm-5-turbo",
modelRegistry: {
find: (_provider: string, modelId: string) =>
modelId === "glm-5-turbo" ? registered : modelId === "glm-4.7" ? template : null,
},
} as never),
).toEqual(registered);
});
it("falls back to manifest baseUrl when both providerConfig and template model are unavailable", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const resolved = provider.resolveDynamicModel?.({
provider: "zai",
modelId: "glm-5-turbo",
modelRegistry: {
find: () => null,
},
} as never) as Record<string, unknown> | undefined;
expectModelFields(resolved, {
id: "glm-5-turbo",
provider: "zai",
api: "openai-completions",
baseUrl: "https://api.z.ai/api/paas/v4",
reasoning: true,
input: ["text"],
});
});
it("still synthesizes unknown GLM-5 variants from the GLM-4.7 template", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const template = createGlm47Template();
const resolved = provider.resolveDynamicModel?.({
provider: "zai",
modelId: "glm-5-turbo",
modelRegistry: {
find: (_provider: string, modelId: string) => (modelId === "glm-4.7" ? template : null),
},
} as never) as Record<string, unknown> | undefined;
expectModelFields(resolved, {
id: "glm-5-turbo",
name: "GLM-5 Turbo",
provider: "zai",
api: "openai-completions",
baseUrl: "https://api.z.ai/api/paas/v4",
reasoning: true,
input: ["text"],
});
});
it("wires tool-stream defaults through the shared stream family hook", async () => {
const provider = await registerSingleProviderPlugin(plugin);
let capturedPayload: Record<string, unknown> | undefined;
const baseStreamFn: StreamFn = (model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload as never, model as never);
capturedPayload = payload;
return {} as ReturnType<StreamFn>;
};
const defaultWrapped = provider.wrapStreamFn?.({
provider: "zai",
modelId: "glm-5.1",
extraParams: {},
streamFn: baseStreamFn,
} as never);
void defaultWrapped?.(
{
api: "openai-completions",
provider: "zai",
id: "glm-5.1",
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
);
expect(capturedPayload?.tool_stream).toBe(true);
const disabledWrapped = provider.wrapStreamFn?.({
provider: "zai",
modelId: "glm-5.1",
extraParams: { tool_stream: false },
streamFn: baseStreamFn,
} as never);
void disabledWrapped?.(
{
api: "openai-completions",
provider: "zai",
id: "glm-5.1",
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
);
expect(capturedPayload).not.toHaveProperty("tool_stream");
});
it("exposes full GLM-5.2 thinking levels while keeping older GLM models binary", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(
provider.resolveThinkingProfile?.({
provider: "zai",
modelId: "glm-5.2",
reasoning: true,
} as never),
).toEqual({
levels: [
{ id: "off", label: "off" },
{ id: "low", label: "low" },
{ id: "high", label: "high" },
{ id: "max", label: "max" },
],
defaultLevel: "off",
});
expect(
provider.resolveThinkingProfile?.({
provider: "zai",
modelId: "glm-5.1",
reasoning: true,
} as never),
).toEqual({
levels: [
{ id: "off", label: "off" },
{ id: "low", label: "on" },
],
defaultLevel: "off",
});
});
it("maps thinking off to Z.AI thinking disabled", async () => {
const provider = await registerSingleProviderPlugin(plugin);
let capturedPayload: Record<string, unknown> | undefined;
const baseStreamFn: StreamFn = (model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload as never, model as never);
capturedPayload = payload;
return {} as ReturnType<StreamFn>;
};
const wrapped = provider.wrapStreamFn?.({
provider: "zai",
modelId: "glm-5.1",
extraParams: {},
thinkingLevel: "off",
streamFn: baseStreamFn,
} as never);
void wrapped?.(
{
api: "openai-completions",
provider: "zai",
id: "glm-5.1",
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
);
expect(capturedPayload?.tool_stream).toBe(true);
expect(capturedPayload?.thinking).toEqual({ type: "disabled" });
});
it("keeps minimal thinking enabled for binary GLM models", async () => {
const provider = await registerSingleProviderPlugin(plugin);
let capturedPayload: Record<string, unknown> | undefined;
const baseStreamFn: StreamFn = (model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload as never, model as never);
capturedPayload = payload;
return {} as ReturnType<StreamFn>;
};
const wrapped = provider.wrapStreamFn?.({
provider: "zai",
modelId: "glm-5.1",
extraParams: {},
thinkingLevel: "minimal",
streamFn: baseStreamFn,
} as never);
void wrapped?.(
{
api: "openai-completions",
provider: "zai",
id: "glm-5.1",
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
);
expect(capturedPayload).not.toHaveProperty("thinking");
});
it("maps GLM-5.2 thinking levels to Z.AI reasoning effort", async () => {
const provider = await registerSingleProviderPlugin(plugin);
const baseStreamFn: StreamFn = (model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload as never, model as never);
return { payload } as never;
};
for (const [thinkingLevel, expectedEffort] of [
["low", "high"],
["high", "high"],
["max", "max"],
] as const) {
const wrapped = provider.wrapStreamFn?.({
provider: "zai",
modelId: "glm-5.2",
extraParams: {},
thinkingLevel,
streamFn: baseStreamFn,
} as never);
const result = wrapped?.(
{
api: "openai-completions",
provider: "zai",
id: "glm-5.2",
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
) as unknown as { payload: Record<string, unknown> };
expect(result.payload.reasoning_effort).toBe(expectedEffort);
expect(result.payload).not.toHaveProperty("thinking");
expect(result.payload.tool_stream).toBe(true);
}
});
it("enables Z.AI preserved thinking only when requested", async () => {
const provider = await registerSingleProviderPlugin(plugin);
let capturedPayload: Record<string, unknown> | undefined;
const baseStreamFn: StreamFn = (model, _context, options) => {
const payload: Record<string, unknown> = {};
options?.onPayload?.(payload as never, model as never);
capturedPayload = payload;
return {} as ReturnType<StreamFn>;
};
const wrappedWithoutPreserve = provider.wrapStreamFn?.({
provider: "zai",
modelId: "glm-5.1",
extraParams: {},
thinkingLevel: "low",
streamFn: baseStreamFn,
} as never);
void wrappedWithoutPreserve?.(
{
api: "openai-completions",
provider: "zai",
id: "glm-5.1",
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
);
expect(capturedPayload?.tool_stream).toBe(true);
expect(capturedPayload).not.toHaveProperty("thinking");
const wrappedWithPreserve = provider.wrapStreamFn?.({
provider: "zai",
modelId: "glm-5.1",
extraParams: { preserveThinking: true },
thinkingLevel: "low",
streamFn: baseStreamFn,
} as never);
void wrappedWithPreserve?.(
{
api: "openai-completions",
provider: "zai",
id: "glm-5.1",
} as Model<"openai-completions">,
{ messages: [] } as Context,
{},
);
expect(capturedPayload?.tool_stream).toBe(true);
expect(capturedPayload?.thinking).toEqual({ type: "enabled", clear_thinking: false });
});
it("preserves replayed reasoning_content for Z.AI preserved thinking", async () => {
const provider = await registerSingleProviderPlugin(plugin);
let capturedPayload: Record<string, unknown> | undefined;
const model = {
provider: "zai",
id: "glm-5.1",
name: "GLM 5.1",
api: "openai-completions",
baseUrl: "https://api.z.ai/api/paas/v4",
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200_000,
maxTokens: 131_072,
} as Model<"openai-completions">;
const context = {
messages: [
{ role: "user", content: "hi", timestamp: 1 },
{
role: "assistant",
api: "openai-completions",
provider: "zai",
model: "glm-5.1",
content: [
{
type: "thinking",
thinking: "prior reasoning",
thinkingSignature: "reasoning_content",
},
{ type: "text", text: "visible reply" },
],
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: "stop",
timestamp: 2,
},
{ role: "user", content: "continue", timestamp: 3 },
],
} as Context;
const baseStreamFn: StreamFn = (streamModel, streamContext, options) => {
const payload = buildOpenAICompletionsParams(streamModel as never, streamContext, {
reasoning: "high",
} as never);
options?.onPayload?.(payload as never, streamModel as never);
capturedPayload = payload;
return {} as ReturnType<StreamFn>;
};
const wrapped = provider.wrapStreamFn?.({
provider: "zai",
modelId: "glm-5.1",
extraParams: { preserve_thinking: true },
thinkingLevel: "low",
streamFn: baseStreamFn,
} as never);
void wrapped?.(model, context, {});
expect(capturedPayload?.thinking).toEqual({ type: "enabled", clear_thinking: false });
const assistantMessage = (capturedPayload!.messages as Array<Record<string, unknown>>)[1];
expect(assistantMessage?.role).toBe("assistant");
expect(assistantMessage?.content).toBe("visible reply");
expect(assistantMessage?.reasoning_content).toBe("prior reasoning");
});
it("defaults tool_stream extra params but preserves explicit values", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(
provider.prepareExtraParams?.({
provider: "zai",
modelId: "glm-4.7",
extraParams: { endpoint: "global" },
} as never),
).toEqual({
endpoint: "global",
tool_stream: true,
});
const explicit = { endpoint: "global", tool_stream: false };
expect(
provider.prepareExtraParams?.({
provider: "zai",
modelId: "glm-4.7",
extraParams: explicit,
} as never),
).toBe(explicit);
});
it("uses deprecated pi agent auth.json for usage auth when modern sources are empty", async () => {
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-zai-legacy-auth-"));
try {
const authDir = path.join(home, ".pi", "agent");
await fs.mkdir(authDir, { recursive: true });
await fs.writeFile(
path.join(authDir, "auth.json"),
`${JSON.stringify({ "z-ai": { access: "legacy-zai-token" } }, null, 2)}\n`,
"utf-8",
);
const provider = await registerSingleProviderPlugin(plugin);
await expect(
provider.resolveUsageAuth?.({
env: { HOME: home },
resolveApiKeyFromConfigAndStore: () => undefined,
} as never),
).resolves.toEqual({ token: "legacy-zai-token" });
} finally {
await fs.rm(home, { recursive: true, force: true });
}
});
});

440
extensions/zai/index.ts Normal file
View File

@@ -0,0 +1,440 @@
// Zai plugin entrypoint registers its OpenClaw integration.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
definePluginEntry,
type ProviderAuthContext,
type ProviderAuthMethod,
type ProviderAuthMethodNonInteractiveContext,
type ProviderResolveDynamicModelContext,
type ProviderRuntimeModel,
type ProviderWrapStreamFnContext,
} from "openclaw/plugin-sdk/plugin-entry";
import {
applyAuthProfileConfig,
buildApiKeyCredential,
ensureApiKeyFromOptionEnvOrPrompt,
normalizeApiKeyInput,
normalizeOptionalSecretInput,
type SecretInput,
upsertAuthProfileWithLock,
validateApiKeyInput,
} from "openclaw/plugin-sdk/provider-auth-api-key";
import {
buildProviderReplayFamilyHooks,
normalizeModelCompat,
} from "openclaw/plugin-sdk/provider-model-shared";
import {
createPayloadPatchStreamWrapper,
createToolStreamWrapper,
defaultToolStreamExtraParams,
} from "openclaw/plugin-sdk/provider-stream-shared";
import { fetchZaiUsage } from "openclaw/plugin-sdk/provider-usage";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { detectZaiEndpoint, type ZaiEndpointId } from "./detect.js";
import { zaiMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { buildZaiModelDefinition, resolveZaiBaseUrl } from "./model-definitions.js";
import { applyZaiConfig, applyZaiProviderConfig, resolveZaiModelId } from "./onboard.js";
const PROVIDER_ID = "zai";
const GLM5_TEMPLATE_MODEL_ID = "glm-4.7";
const PROFILE_ID = "zai:default";
type UpsertAuthProfileParams = Parameters<typeof upsertAuthProfileWithLock>[0];
function resolveDeprecatedPiAgentAuthPath(env: NodeJS.ProcessEnv): string {
const home = env.HOME?.trim() || env.USERPROFILE?.trim() || os.homedir();
return path.join(home, ".pi", "agent", "auth.json");
}
function resolveDeprecatedPiAgentAccessToken(
env: NodeJS.ProcessEnv,
providerIds: readonly string[],
): string | undefined {
try {
const authPath = resolveDeprecatedPiAgentAuthPath(env);
if (!fs.existsSync(authPath)) {
return undefined;
}
const parsed = JSON.parse(fs.readFileSync(authPath, "utf-8")) as Record<
string,
{ access?: unknown }
>;
for (const providerId of providerIds) {
const token = parsed[providerId]?.access;
if (typeof token === "string" && token.trim()) {
return token;
}
}
} catch {}
return undefined;
}
async function upsertAuthProfileWithLockOrThrow(params: UpsertAuthProfileParams): Promise<void> {
const updated = await upsertAuthProfileWithLock(params);
if (!updated) {
throw new Error(
"Failed to update auth profile store; the auth store lock may be busy. Wait a moment and retry.",
);
}
}
function resolveGlm5ForwardCompatModel(
ctx: ProviderResolveDynamicModelContext,
): ProviderRuntimeModel | undefined {
const trimmedModelId = ctx.modelId.trim();
if (!normalizeLowercaseStringOrEmpty(trimmedModelId).startsWith("glm-5")) {
return undefined;
}
const existing = ctx.modelRegistry.find(
PROVIDER_ID,
trimmedModelId,
) as ProviderRuntimeModel | null;
if (existing) {
return existing;
}
const def = buildZaiModelDefinition({ id: trimmedModelId });
const template = ctx.modelRegistry.find(
PROVIDER_ID,
GLM5_TEMPLATE_MODEL_ID,
) as ProviderRuntimeModel | null;
return normalizeModelCompat({
...template,
id: def.id,
name: def.name,
// Native models must never fall through to the OpenAI SDK's default host.
baseUrl: ctx.providerConfig?.baseUrl ?? template?.baseUrl ?? resolveZaiBaseUrl(),
api: "openai-completions",
provider: PROVIDER_ID,
reasoning: def.reasoning,
input: def.input,
cost: def.cost,
contextWindow: def.contextWindow,
maxTokens: def.maxTokens,
} as ProviderRuntimeModel);
}
function isTrueParam(value: unknown): boolean {
return value === true;
}
function shouldPreserveZaiThinking(extraParams?: Record<string, unknown>): boolean {
return isTrueParam(extraParams?.preserveThinking) || isTrueParam(extraParams?.preserve_thinking);
}
function isDisabledThinkingLevel(thinkingLevel: ProviderWrapStreamFnContext["thinkingLevel"]) {
return thinkingLevel === "off";
}
function isGlm52ModelId(modelId?: string | null): boolean {
return normalizeLowercaseStringOrEmpty(modelId).startsWith("glm-5.2");
}
function mapThinkingLevelToZaiReasoningEffort(
thinkingLevel: ProviderWrapStreamFnContext["thinkingLevel"],
): "high" | "max" | undefined {
switch (thinkingLevel) {
case "low":
case "medium":
case "high":
case "adaptive":
return "high";
case "xhigh":
case "max":
return "max";
default:
return undefined;
}
}
function wrapZaiStreamFn(ctx: ProviderWrapStreamFnContext) {
let streamFn = createToolStreamWrapper(ctx.streamFn, ctx.extraParams?.tool_stream !== false);
const preserveThinking = shouldPreserveZaiThinking(ctx.extraParams);
const reasoningEffort = isGlm52ModelId(ctx.modelId)
? mapThinkingLevelToZaiReasoningEffort(ctx.thinkingLevel)
: undefined;
if (!isDisabledThinkingLevel(ctx.thinkingLevel) && !preserveThinking && !reasoningEffort) {
return streamFn;
}
streamFn = createPayloadPatchStreamWrapper(streamFn, ({ payload, model }) => {
if (model.api !== "openai-completions" || model.provider !== PROVIDER_ID) {
return;
}
if (isDisabledThinkingLevel(ctx.thinkingLevel)) {
payload.thinking = { type: "disabled" };
return;
}
if (reasoningEffort) {
payload.reasoning_effort = reasoningEffort;
}
if (preserveThinking) {
payload.thinking = { type: "enabled", clear_thinking: false };
}
});
return streamFn;
}
async function promptForZaiEndpoint(ctx: ProviderAuthContext): Promise<ZaiEndpointId> {
return await ctx.prompter.select<ZaiEndpointId>({
message: "Select Z.AI endpoint",
initialValue: "global",
options: [
{ value: "global", label: "Global", hint: "Z.AI Global (api.z.ai)" },
{ value: "cn", label: "CN", hint: "Z.AI CN (open.bigmodel.cn)" },
{
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)",
},
],
});
}
async function runZaiApiKeyAuth(
ctx: ProviderAuthContext,
endpoint?: ZaiEndpointId,
): Promise<{
profiles: Array<{ profileId: string; credential: ReturnType<typeof buildApiKeyCredential> }>;
configPatch: ReturnType<typeof applyZaiProviderConfig>;
defaultModel: string;
notes?: string[];
}> {
let capturedSecretInput: SecretInput | undefined;
let capturedCredential = false;
let capturedMode: "plaintext" | "ref" | undefined;
const apiKey = await ensureApiKeyFromOptionEnvOrPrompt({
token:
normalizeOptionalSecretInput(ctx.opts?.zaiApiKey) ??
normalizeOptionalSecretInput(ctx.opts?.token),
tokenProvider: normalizeOptionalSecretInput(ctx.opts?.zaiApiKey)
? PROVIDER_ID
: normalizeOptionalSecretInput(ctx.opts?.tokenProvider),
secretInputMode:
ctx.allowSecretRefPrompt === false
? (ctx.secretInputMode ?? "plaintext")
: ctx.secretInputMode,
config: ctx.config,
expectedProviders: [PROVIDER_ID, "z-ai"],
provider: PROVIDER_ID,
envLabel: "ZAI_API_KEY",
promptMessage: "Enter Z.AI API key",
normalize: normalizeApiKeyInput,
validate: validateApiKeyInput,
prompter: ctx.prompter,
setCredential: async (key, mode) => {
capturedSecretInput = key;
capturedCredential = true;
capturedMode = mode;
},
});
if (!capturedCredential) {
throw new Error("Missing Z.AI API key.");
}
const credentialInput = capturedSecretInput ?? "";
const detected = await detectZaiEndpoint({ apiKey, ...(endpoint ? { endpoint } : {}) });
const modelIdOverride = detected?.modelId;
const nextEndpoint = detected?.endpoint ?? endpoint ?? (await promptForZaiEndpoint(ctx));
const preset = {
...(nextEndpoint ? { endpoint: nextEndpoint } : {}),
...(modelIdOverride ? { modelId: modelIdOverride } : {}),
};
return {
profiles: [
{
profileId: PROFILE_ID,
credential: buildApiKeyCredential(
PROVIDER_ID,
credentialInput,
undefined,
capturedMode ? { secretInputMode: capturedMode } : undefined,
),
},
],
configPatch: applyZaiProviderConfig(ctx.config, preset),
defaultModel: `zai/${resolveZaiModelId(preset)}`,
...(detected?.note ? { notes: [detected.note] } : {}),
};
}
async function runZaiApiKeyAuthNonInteractive(
ctx: ProviderAuthMethodNonInteractiveContext,
endpoint?: ZaiEndpointId,
) {
const resolved = await ctx.resolveApiKey({
provider: PROVIDER_ID,
flagValue: normalizeOptionalSecretInput(ctx.opts.zaiApiKey),
flagName: "--zai-api-key",
envVar: "ZAI_API_KEY",
});
if (!resolved) {
return null;
}
const detected = await detectZaiEndpoint({
apiKey: resolved.key,
...(endpoint ? { endpoint } : {}),
});
const modelIdOverride = detected?.modelId;
const nextEndpoint = detected?.endpoint ?? endpoint;
if (resolved.source !== "profile") {
const credential = ctx.toApiKeyCredential({
provider: PROVIDER_ID,
resolved,
});
if (!credential) {
return null;
}
await upsertAuthProfileWithLockOrThrow({
profileId: PROFILE_ID,
credential,
agentDir: ctx.agentDir,
});
}
const next = applyAuthProfileConfig(ctx.config, {
profileId: PROFILE_ID,
provider: PROVIDER_ID,
mode: "api_key",
});
return applyZaiConfig(next, {
...(nextEndpoint ? { endpoint: nextEndpoint } : {}),
...(modelIdOverride ? { modelId: modelIdOverride } : {}),
});
}
function buildZaiApiKeyMethod(params: {
id: string;
choiceId: string;
choiceLabel: string;
choiceHint?: string;
endpoint?: ZaiEndpointId;
}): ProviderAuthMethod {
return {
id: params.id,
label: params.choiceLabel,
hint: params.choiceHint,
kind: "api_key",
wizard: {
choiceId: params.choiceId,
choiceLabel: params.choiceLabel,
...(params.choiceHint ? { choiceHint: params.choiceHint } : {}),
groupId: "zai",
groupLabel: "Z.AI",
groupHint: "GLM Coding Plan / Global / CN",
},
run: async (ctx) => await runZaiApiKeyAuth(ctx, params.endpoint),
runNonInteractive: async (ctx) => await runZaiApiKeyAuthNonInteractive(ctx, params.endpoint),
};
}
export default definePluginEntry({
id: PROVIDER_ID,
name: "Z.AI Provider",
description: "Bundled Z.AI provider plugin",
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "Z.AI",
aliases: ["z-ai", "z.ai"],
docsPath: "/providers/models",
envVars: ["ZAI_API_KEY", "Z_AI_API_KEY"],
auth: [
buildZaiApiKeyMethod({
id: "api-key",
choiceId: "zai-api-key",
choiceLabel: "Z.AI API key",
}),
buildZaiApiKeyMethod({
id: "coding-global",
choiceId: "zai-coding-global",
choiceLabel: "Coding-Plan-Global",
choiceHint: "GLM Coding Plan Global (api.z.ai)",
endpoint: "coding-global",
}),
buildZaiApiKeyMethod({
id: "coding-cn",
choiceId: "zai-coding-cn",
choiceLabel: "Coding-Plan-CN",
choiceHint: "GLM Coding Plan CN (open.bigmodel.cn)",
endpoint: "coding-cn",
}),
buildZaiApiKeyMethod({
id: "global",
choiceId: "zai-global",
choiceLabel: "Global",
choiceHint: "Z.AI Global (api.z.ai)",
endpoint: "global",
}),
buildZaiApiKeyMethod({
id: "cn",
choiceId: "zai-cn",
choiceLabel: "CN",
choiceHint: "Z.AI CN (open.bigmodel.cn)",
endpoint: "cn",
}),
],
resolveDynamicModel: (ctx) => resolveGlm5ForwardCompatModel(ctx),
...buildProviderReplayFamilyHooks({
family: "openai-compatible",
dropReasoningFromHistory: false,
}),
prepareExtraParams: (ctx) => defaultToolStreamExtraParams(ctx.extraParams),
wrapStreamFn: (ctx) => wrapZaiStreamFn(ctx),
resolveThinkingProfile: (ctx) =>
isGlm52ModelId(ctx.modelId)
? {
levels: [
{ id: "off", label: "off" },
{ id: "low", label: "low" },
{ id: "high", label: "high" },
{ id: "max", label: "max" },
],
defaultLevel: "off",
}
: {
levels: [
{ id: "off", label: "off" },
{ id: "low", label: "on" },
],
defaultLevel: "off",
},
isModernModelRef: ({ modelId }) => {
const lower = normalizeLowercaseStringOrEmpty(modelId);
return (
lower.startsWith("glm-5") ||
lower.startsWith("glm-4.7") ||
lower.startsWith("glm-4.7-flash") ||
lower.startsWith("glm-4.7-flashx")
);
},
resolveUsageAuth: async (ctx) => {
const apiKey = ctx.resolveApiKeyFromConfigAndStore({
providerIds: [PROVIDER_ID, "z-ai"],
envDirect: [ctx.env.ZAI_API_KEY, ctx.env.Z_AI_API_KEY],
});
if (apiKey) {
return { token: apiKey };
}
const legacyToken = resolveDeprecatedPiAgentAccessToken(ctx.env, ["z-ai", PROVIDER_ID]);
return legacyToken ? { token: legacyToken } : null;
},
fetchUsageSnapshot: async (ctx) => await fetchZaiUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),
isCacheTtlEligible: () => true,
});
api.registerMediaUnderstandingProvider(zaiMediaUnderstandingProvider);
},
});

View File

@@ -0,0 +1,15 @@
// Zai provider module implements model/runtime integration.
import {
describeImageWithModel,
describeImagesWithModel,
type MediaUnderstandingProvider,
} from "openclaw/plugin-sdk/media-understanding";
export const zaiMediaUnderstandingProvider: MediaUnderstandingProvider = {
id: "zai",
capabilities: ["image"],
defaultModels: { image: "glm-4.6v" },
autoPriority: { image: 60 },
describeImage: describeImageWithModel,
describeImages: describeImagesWithModel,
};

View File

@@ -0,0 +1,116 @@
// Zai tests cover model definitions plugin behavior.
import { describe, expect, it } from "vitest";
import { buildZaiModelDefinition, ZAI_DEFAULT_COST } from "./model-definitions.js";
type ExpectedZaiModelFields = {
id: string;
reasoning?: boolean;
input?: string[];
contextWindow?: number;
maxTokens?: number;
cost?: typeof ZAI_DEFAULT_COST;
};
function expectZaiModelFields(expected: ExpectedZaiModelFields) {
const model = buildZaiModelDefinition({ id: expected.id });
expect(model.id).toBe(expected.id);
if ("reasoning" in expected) {
expect(model.reasoning).toBe(expected.reasoning);
}
if (expected.input) {
expect(model.input).toEqual(expected.input);
}
if (expected.contextWindow !== undefined) {
expect(model.contextWindow).toBe(expected.contextWindow);
}
if (expected.maxTokens !== undefined) {
expect(model.maxTokens).toBe(expected.maxTokens);
}
if (expected.cost) {
expect(model.cost).toEqual(expected.cost);
}
}
describe("zai model definitions", () => {
it("uses official GLM-5.2 Coding Plan metadata", () => {
expectZaiModelFields({
id: "glm-5.2",
reasoning: true,
input: ["text"],
contextWindow: 1_000_000,
maxTokens: 131_072,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
});
});
it("uses current OpenClaw metadata for the new GLM-5.1 model", () => {
expectZaiModelFields({
id: "glm-5.1",
reasoning: true,
input: ["text"],
contextWindow: 202800,
maxTokens: 131100,
cost: { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 },
});
});
it("uses current OpenClaw metadata for the new GLM-5V Turbo model", () => {
expectZaiModelFields({
id: "glm-5v-turbo",
reasoning: true,
input: ["text", "image"],
contextWindow: 202800,
maxTokens: 131100,
cost: { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 },
});
});
it("uses current OpenClaw metadata for the GLM-5 model", () => {
expectZaiModelFields({
id: "glm-5",
reasoning: true,
input: ["text"],
contextWindow: 202800,
maxTokens: 131100,
cost: ZAI_DEFAULT_COST,
});
});
it("publishes newer GLM 4.5/4.6 family metadata from OpenClaw", () => {
expectZaiModelFields({
id: "glm-4.6v",
input: ["text", "image"],
contextWindow: 128000,
maxTokens: 32768,
cost: { input: 0.3, output: 0.9, cacheRead: 0, cacheWrite: 0 },
});
expectZaiModelFields({
id: "glm-4.5-air",
input: ["text"],
contextWindow: 131072,
maxTokens: 98304,
cost: { input: 0.2, output: 1.1, cacheRead: 0.03, cacheWrite: 0 },
});
});
it("keeps the remaining GLM 4.7/5 pricing and token limits aligned with OpenClaw", () => {
expectZaiModelFields({
id: "glm-4.7-flash",
cost: { input: 0.07, output: 0.4, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 131072,
});
expectZaiModelFields({
id: "glm-4.7-flashx",
cost: { input: 0.06, output: 0.4, cacheRead: 0.01, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 128000,
});
expectZaiModelFields({
id: "glm-5-turbo",
contextWindow: 202800,
maxTokens: 131100,
cost: { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 },
});
});
});

View File

@@ -0,0 +1,72 @@
// Zai plugin module implements model definitions behavior.
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import manifest from "./openclaw.plugin.json" with { type: "json" };
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-5.1";
export const ZAI_CODING_DEFAULT_MODEL_ID = "glm-5.2";
const ZAI_MANIFEST_CATALOG = manifest.modelCatalog.providers.zai;
const ZAI_MANIFEST_PROVIDER = buildManifestModelProviderConfig({
providerId: "zai",
catalog: ZAI_MANIFEST_CATALOG,
});
const ZAI_MODEL_CATALOG = new Map(
ZAI_MANIFEST_PROVIDER.models.map((model) => [model.id, model] as const),
);
export const ZAI_DEFAULT_COST =
ZAI_MODEL_CATALOG.get("glm-5")?.cost ??
({
input: 1,
output: 3.2,
cacheRead: 0.2,
cacheWrite: 0,
} satisfies ModelDefinitionConfig["cost"]);
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":
return ZAI_CODING_GLOBAL_BASE_URL;
default:
return ZAI_GLOBAL_BASE_URL;
}
}
export function buildZaiCatalogModels(): ModelDefinitionConfig[] {
return ZAI_MANIFEST_PROVIDER.models.map((model) =>
Object.assign({}, model, { input: [...model.input] }),
);
}
export function buildZaiModelDefinition(params: {
id: string;
name?: string;
reasoning?: boolean;
input?: ModelDefinitionConfig["input"];
cost?: ModelDefinitionConfig["cost"];
contextWindow?: number;
maxTokens?: number;
}): ModelDefinitionConfig {
const catalog = ZAI_MODEL_CATALOG.get(params.id);
return {
id: params.id,
name: params.name ?? catalog?.name ?? `GLM ${params.id}`,
reasoning: params.reasoning ?? catalog?.reasoning ?? true,
input:
params.input ?? (catalog?.input ? ([...catalog.input] as ("text" | "image")[]) : ["text"]),
cost: params.cost ?? catalog?.cost ?? ZAI_DEFAULT_COST,
contextWindow: params.contextWindow ?? catalog?.contextWindow ?? 202800,
maxTokens: params.maxTokens ?? catalog?.maxTokens ?? 131100,
};
}

12
extensions/zai/npm-shrinkwrap.json generated Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "@openclaw/zai-provider",
"version": "2026.6.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/zai-provider",
"version": "2026.6.11"
}
}
}

View File

@@ -0,0 +1,137 @@
// Zai tests cover onboard plugin behavior.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { AuthStorage, ModelRegistry } from "openclaw/plugin-sdk/agent-sessions";
import { resolveAgentModelPrimaryValue } from "openclaw/plugin-sdk/provider-onboard";
import { expectProviderOnboardPreservesPrimary } from "openclaw/plugin-sdk/provider-test-contracts";
import { beforeAll, describe, expect, it } from "vitest";
import {
ZAI_CODING_CN_BASE_URL,
ZAI_CODING_GLOBAL_BASE_URL,
ZAI_GLOBAL_BASE_URL,
} from "./model-definitions.js";
import { applyZaiConfig, applyZaiProviderConfig } from "./onboard.js";
describe("zai onboard", () => {
let defaultCfg: ReturnType<typeof applyZaiConfig>;
let cnFlashCfg: ReturnType<typeof applyZaiConfig>;
let cnFlashxCfg: ReturnType<typeof applyZaiConfig>;
beforeAll(() => {
defaultCfg = applyZaiConfig({});
cnFlashCfg = applyZaiConfig({}, { endpoint: "coding-cn", modelId: "glm-4.7-flash" });
cnFlashxCfg = applyZaiConfig({}, { endpoint: "coding-cn", modelId: "glm-4.7-flashx" });
});
it("adds zai provider with correct settings", () => {
expect(defaultCfg.models?.providers?.zai?.baseUrl).toBe(ZAI_GLOBAL_BASE_URL);
expect(defaultCfg.models?.providers?.zai?.api).toBe("openai-completions");
const ids = defaultCfg.models?.providers?.zai?.models?.map((m) => m.id);
expect(ids).toEqual([
"glm-5.2",
"glm-5.1",
"glm-5",
"glm-5-turbo",
"glm-5v-turbo",
"glm-4.7",
"glm-4.7-flash",
"glm-4.7-flashx",
"glm-4.6",
"glm-4.6v",
"glm-4.5",
"glm-4.5-air",
"glm-4.5-flash",
"glm-4.5v",
]);
expect(
defaultCfg.models?.providers?.zai?.models?.find((model) => model.id === "glm-5.2"),
).toMatchObject({
contextWindow: 1_000_000,
maxTokens: 131_072,
});
expect(
defaultCfg.models?.providers?.zai?.models?.find((model) => model.id === "glm-5.2"),
).not.toHaveProperty("baseUrl");
});
it("resolves GLM-5.2 through the selected Coding Plan or custom endpoint", async () => {
for (const [name, cfg, expectedBaseUrl] of [
["coding-cn", applyZaiConfig({}, { endpoint: "coding-cn" }), ZAI_CODING_CN_BASE_URL],
[
"custom",
applyZaiConfig({
models: {
providers: {
zai: {
baseUrl: "https://proxy.example.test/zai",
api: "openai-completions",
models: [],
},
},
},
}),
"https://proxy.example.test/zai",
],
] as const) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), `openclaw-zai-${name}-`));
try {
const modelsPath = path.join(dir, "models.json");
await fs.writeFile(
modelsPath,
JSON.stringify({
...cfg.models,
providers: {
...cfg.models?.providers,
zai: { ...cfg.models?.providers?.zai, apiKey: "test-key" },
},
}),
);
const registry = ModelRegistry.create(AuthStorage.inMemory(), modelsPath);
expect(registry.getError()).toBeUndefined();
expect(registry.find("zai", "glm-5.2")?.baseUrl).toBe(expectedBaseUrl);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
}
});
it("supports CN endpoint for supported coding models", () => {
for (const [modelId, cfg] of [
["glm-4.7-flash", cnFlashCfg],
["glm-4.7-flashx", cnFlashxCfg],
] as const) {
expect(cfg.models?.providers?.zai?.baseUrl).toBe(ZAI_CODING_CN_BASE_URL);
expect(resolveAgentModelPrimaryValue(cfg.agents?.defaults?.model)).toBe(`zai/${modelId}`);
}
});
it("defaults Coding Plan endpoints to GLM-5.2 without changing the general API default", () => {
const codingCfg = applyZaiConfig({}, { endpoint: "coding-global" });
const existingCodingCfg = applyZaiConfig({
models: {
providers: {
zai: {
baseUrl: `${ZAI_CODING_GLOBAL_BASE_URL}/`,
api: "openai-completions",
models: [],
},
},
},
});
expect(resolveAgentModelPrimaryValue(defaultCfg.agents?.defaults?.model)).toBe("zai/glm-5.1");
expect(codingCfg.models?.providers?.zai?.baseUrl).toBe(ZAI_CODING_GLOBAL_BASE_URL);
expect(resolveAgentModelPrimaryValue(codingCfg.agents?.defaults?.model)).toBe("zai/glm-5.2");
expect(resolveAgentModelPrimaryValue(existingCodingCfg.agents?.defaults?.model)).toBe(
"zai/glm-5.2",
);
});
it("does not overwrite existing primary model in provider-only mode", () => {
expectProviderOnboardPreservesPrimary({
applyProviderConfig: applyZaiProviderConfig,
primaryModelRef: "anthropic/claude-opus-4-5",
});
});
});

74
extensions/zai/onboard.ts Normal file
View File

@@ -0,0 +1,74 @@
// Zai setup module handles plugin onboarding behavior.
import {
applyProviderConfigWithModelCatalogPreset,
type OpenClawConfig,
} from "openclaw/plugin-sdk/provider-onboard";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
buildZaiCatalogModels,
resolveZaiBaseUrl,
ZAI_CODING_CN_BASE_URL,
ZAI_CODING_DEFAULT_MODEL_ID,
ZAI_CODING_GLOBAL_BASE_URL,
ZAI_DEFAULT_MODEL_ID,
} from "./model-definitions.js";
export const ZAI_DEFAULT_MODEL_REF = `zai/${ZAI_DEFAULT_MODEL_ID}`;
function resolveZaiPresetBaseUrl(cfg: OpenClawConfig, endpoint?: string): string {
const existingProvider = cfg.models?.providers?.zai;
const existingBaseUrl = normalizeOptionalString(existingProvider?.baseUrl) ?? "";
return endpoint ? resolveZaiBaseUrl(endpoint) : existingBaseUrl || resolveZaiBaseUrl();
}
export function resolveZaiModelId(params?: {
endpoint?: string;
modelId?: string;
baseUrl?: string;
}): string {
const explicitModelId = normalizeOptionalString(params?.modelId);
if (explicitModelId) {
return explicitModelId;
}
const baseUrl = normalizeOptionalString(params?.baseUrl)?.replace(/\/+$/, "");
const usesCodingEndpoint =
params?.endpoint?.startsWith("coding-") ||
baseUrl === ZAI_CODING_GLOBAL_BASE_URL ||
baseUrl === ZAI_CODING_CN_BASE_URL;
return usesCodingEndpoint ? ZAI_CODING_DEFAULT_MODEL_ID : ZAI_DEFAULT_MODEL_ID;
}
function applyZaiPreset(
cfg: OpenClawConfig,
params?: { endpoint?: string; modelId?: string },
primaryModelRef?: string,
): OpenClawConfig {
const baseUrl = resolveZaiPresetBaseUrl(cfg, params?.endpoint);
const modelId = resolveZaiModelId({ ...params, baseUrl });
const modelRef = `zai/${modelId}`;
return applyProviderConfigWithModelCatalogPreset(cfg, {
providerId: "zai",
api: "openai-completions",
baseUrl,
catalogModels: buildZaiCatalogModels(),
aliases: [{ modelRef, alias: "GLM" }],
primaryModelRef,
});
}
export function applyZaiProviderConfig(
cfg: OpenClawConfig,
params?: { endpoint?: string; modelId?: string },
): OpenClawConfig {
return applyZaiPreset(cfg, params);
}
export function applyZaiConfig(
cfg: OpenClawConfig,
params?: { endpoint?: string; modelId?: string },
): OpenClawConfig {
const baseUrl = resolveZaiPresetBaseUrl(cfg, params?.endpoint);
const modelId = resolveZaiModelId({ ...params, baseUrl });
const modelRef = modelId === ZAI_DEFAULT_MODEL_ID ? ZAI_DEFAULT_MODEL_REF : `zai/${modelId}`;
return applyZaiPreset(cfg, params, modelRef);
}

View File

@@ -0,0 +1,349 @@
{
"id": "zai",
"activation": {
"onStartup": false
},
"enabledByDefault": true,
"providers": ["zai"],
"providerEndpoints": [
{
"endpointClass": "zai-native",
"hosts": ["api.z.ai"]
}
],
"providerRequest": {
"providers": {
"zai": {
"family": "zai"
}
}
},
"setup": {
"providers": [
{
"id": "zai",
"authMethods": ["api-key"],
"envVars": ["ZAI_API_KEY", "Z_AI_API_KEY"]
}
]
},
"modelCatalog": {
"providers": {
"zai": {
"baseUrl": "https://api.z.ai/api/paas/v4",
"api": "openai-completions",
"models": [
{
"id": "glm-5.2",
"name": "GLM-5.2",
"reasoning": true,
"input": ["text"],
"contextWindow": 1000000,
"maxTokens": 131072,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "glm-5.1",
"name": "GLM-5.1",
"reasoning": true,
"input": ["text"],
"contextWindow": 202800,
"maxTokens": 131100,
"cost": {
"input": 1.2,
"output": 4,
"cacheRead": 0.24,
"cacheWrite": 0
}
},
{
"id": "glm-5",
"name": "GLM-5",
"reasoning": true,
"input": ["text"],
"contextWindow": 202800,
"maxTokens": 131100,
"cost": {
"input": 1,
"output": 3.2,
"cacheRead": 0.2,
"cacheWrite": 0
}
},
{
"id": "glm-5-turbo",
"name": "GLM-5 Turbo",
"reasoning": true,
"input": ["text"],
"contextWindow": 202800,
"maxTokens": 131100,
"cost": {
"input": 1.2,
"output": 4,
"cacheRead": 0.24,
"cacheWrite": 0
}
},
{
"id": "glm-5v-turbo",
"name": "GLM-5V Turbo",
"reasoning": true,
"input": ["text", "image"],
"contextWindow": 202800,
"maxTokens": 131100,
"cost": {
"input": 1.2,
"output": 4,
"cacheRead": 0.24,
"cacheWrite": 0
}
},
{
"id": "glm-4.7",
"name": "GLM-4.7",
"reasoning": true,
"input": ["text"],
"contextWindow": 204800,
"maxTokens": 131072,
"cost": {
"input": 0.6,
"output": 2.2,
"cacheRead": 0.11,
"cacheWrite": 0
}
},
{
"id": "glm-4.7-flash",
"name": "GLM-4.7 Flash",
"reasoning": true,
"input": ["text"],
"contextWindow": 200000,
"maxTokens": 131072,
"cost": {
"input": 0.07,
"output": 0.4,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "glm-4.7-flashx",
"name": "GLM-4.7 FlashX",
"reasoning": true,
"input": ["text"],
"contextWindow": 200000,
"maxTokens": 128000,
"cost": {
"input": 0.06,
"output": 0.4,
"cacheRead": 0.01,
"cacheWrite": 0
}
},
{
"id": "glm-4.6",
"name": "GLM-4.6",
"reasoning": true,
"input": ["text"],
"contextWindow": 204800,
"maxTokens": 131072,
"cost": {
"input": 0.6,
"output": 2.2,
"cacheRead": 0.11,
"cacheWrite": 0
}
},
{
"id": "glm-4.6v",
"name": "GLM-4.6V",
"reasoning": true,
"input": ["text", "image"],
"contextWindow": 128000,
"maxTokens": 32768,
"cost": {
"input": 0.3,
"output": 0.9,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "glm-4.5",
"name": "GLM-4.5",
"reasoning": true,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 98304,
"cost": {
"input": 0.6,
"output": 2.2,
"cacheRead": 0.11,
"cacheWrite": 0
}
},
{
"id": "glm-4.5-air",
"name": "GLM-4.5 Air",
"reasoning": true,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 98304,
"cost": {
"input": 0.2,
"output": 1.1,
"cacheRead": 0.03,
"cacheWrite": 0
}
},
{
"id": "glm-4.5-flash",
"name": "GLM-4.5 Flash",
"reasoning": true,
"input": ["text"],
"contextWindow": 131072,
"maxTokens": 98304,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
}
},
{
"id": "glm-4.5v",
"name": "GLM-4.5V",
"reasoning": true,
"input": ["text", "image"],
"contextWindow": 64000,
"maxTokens": 16384,
"cost": {
"input": 0.6,
"output": 1.8,
"cacheRead": 0,
"cacheWrite": 0
}
}
]
}
},
"aliases": {
"z.ai": {
"provider": "zai"
},
"z-ai": {
"provider": "zai"
}
},
"discovery": {
"zai": "static"
}
},
"modelPricing": {
"providers": {
"zai": {
"openRouter": {
"provider": "z-ai"
},
"liteLLM": {
"provider": "zai"
}
}
}
},
"providerAuthChoices": [
{
"provider": "zai",
"method": "api-key",
"choiceId": "zai-api-key",
"choiceLabel": "Z.AI API key",
"groupId": "zai",
"groupLabel": "Z.AI",
"groupHint": "GLM Coding Plan / Global / CN",
"optionKey": "zaiApiKey",
"cliFlag": "--zai-api-key",
"cliOption": "--zai-api-key <key>",
"cliDescription": "Z.AI API key"
},
{
"provider": "zai",
"method": "coding-global",
"choiceId": "zai-coding-global",
"choiceLabel": "Coding-Plan-Global",
"choiceHint": "GLM Coding Plan Global (api.z.ai)",
"groupId": "zai",
"groupLabel": "Z.AI",
"groupHint": "GLM Coding Plan / Global / CN",
"optionKey": "zaiApiKey",
"cliFlag": "--zai-api-key",
"cliOption": "--zai-api-key <key>",
"cliDescription": "Z.AI API key"
},
{
"provider": "zai",
"method": "coding-cn",
"choiceId": "zai-coding-cn",
"choiceLabel": "Coding-Plan-CN",
"choiceHint": "GLM Coding Plan CN (open.bigmodel.cn)",
"groupId": "zai",
"groupLabel": "Z.AI",
"groupHint": "GLM Coding Plan / Global / CN",
"optionKey": "zaiApiKey",
"cliFlag": "--zai-api-key",
"cliOption": "--zai-api-key <key>",
"cliDescription": "Z.AI API key"
},
{
"provider": "zai",
"method": "global",
"choiceId": "zai-global",
"choiceLabel": "Global",
"choiceHint": "Z.AI Global (api.z.ai)",
"groupId": "zai",
"groupLabel": "Z.AI",
"groupHint": "GLM Coding Plan / Global / CN",
"optionKey": "zaiApiKey",
"cliFlag": "--zai-api-key",
"cliOption": "--zai-api-key <key>",
"cliDescription": "Z.AI API key"
},
{
"provider": "zai",
"method": "cn",
"choiceId": "zai-cn",
"choiceLabel": "CN",
"choiceHint": "Z.AI CN (open.bigmodel.cn)",
"groupId": "zai",
"groupLabel": "Z.AI",
"groupHint": "GLM Coding Plan / Global / CN",
"optionKey": "zaiApiKey",
"cliFlag": "--zai-api-key",
"cliOption": "--zai-api-key <key>",
"cliDescription": "Z.AI API key"
}
],
"contracts": {
"mediaUnderstandingProviders": ["zai"]
},
"mediaUnderstandingProviderMetadata": {
"zai": {
"capabilities": ["image"],
"defaultModels": {
"image": "glm-4.6v"
},
"autoPriority": {
"image": 60
}
}
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@@ -0,0 +1,35 @@
{
"name": "@openclaw/zai-provider",
"version": "2026.6.11",
"description": "OpenClaw Z.AI provider plugin",
"type": "module",
"devDependencies": {
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
],
"install": {
"clawhubSpec": "clawhub:@openclaw/zai-provider",
"npmSpec": "@openclaw/zai-provider",
"defaultChoice": "npm",
"minHostVersion": ">=2026.6.9"
},
"compat": {
"pluginApi": ">=2026.6.11"
},
"build": {
"openclawVersion": "2026.6.11",
"bundledDist": false
},
"release": {
"publishToClawHub": true,
"publishToNpm": true
}
},
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
}
}

View File

@@ -0,0 +1,4 @@
// Zai tests cover provider runtime.contract plugin behavior.
import { describeZAIProviderRuntimeContract } from "openclaw/plugin-sdk/provider-test-contracts";
describeZAIProviderRuntimeContract(() => import("./index.js"));

View File

@@ -0,0 +1,2 @@
// Zai API module exposes the plugin public contract.
export { detectZaiEndpoint, type ZaiDetectedEndpoint, type ZaiEndpointId } from "./detect.js";

View File

@@ -0,0 +1,2 @@
// Zai API module exposes the plugin public contract.
export { zaiMediaUnderstandingProvider } from "./media-understanding-provider.js";

View File

@@ -0,0 +1,16 @@
{
"extends": "../tsconfig.package-boundary.base.json",
"compilerOptions": {
"rootDir": "."
},
"include": ["./*.ts", "./src/**/*.ts"],
"exclude": [
"./**/*.test.ts",
"./dist/**",
"./node_modules/**",
"./src/test-support/**",
"./src/**/*test-helpers.ts",
"./src/**/*test-harness.ts",
"./src/**/*test-support.ts"
]
}