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
264 lines
7.8 KiB
TypeScript
264 lines
7.8 KiB
TypeScript
// 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;
|
|
}
|