Files
openclaw/extensions/zai/detect.ts
Peter Steinberger 735f59af73 feat(providers): add GLM-5.2 support (#92796)
* feat(providers): add GLM-5.2 support

* ci(live): add GLM-5.2 provider shard
2026-06-13 14:33:28 -07:00

256 lines
7.3 KiB
TypeScript

// Zai plugin module implements detect behavior.
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-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"]);
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 json = (await res.json()) 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;
}