feat(providers): refresh Z.ai Web models and browser transport (#12524)

Validado em lote numa worktree combinada com #12524, #12538, #12277 e #12367 sobre o tip de release/v3.8.51: os quatro boardaram sem conflito (áreas disjuntas — zai-web, nvidia, clova, cursor/devin/fable). typecheck:core limpo, check:provider-consistency OK (272 entradas REGISTRY, 355 providers canônicos), check:known-symbols OK, e 305/305 nos testes tocados pelos quatro PRs. Os IDs de modelo adicionados foram conferidos individualmente. Obrigado, @backryun.
This commit is contained in:
backryun
2026-09-03 19:49:42 +09:00
committed by GitHub
parent 2a6d45abea
commit 032adb0809
10 changed files with 237 additions and 186 deletions

View File

@@ -14,30 +14,27 @@ export const zai_webProvider: RegistryEntry = {
// Z.ai's visible "Tools" switch enables its internal VLM/MCP tools. It does
// not accept caller-supplied OpenAI `tools`, which remains disabled here.
models: [
{
id: "glm-5.3-flash",
name: "GLM-5.3-Flash",
toolCalling: false,
supportsReasoning: true,
supportedThinkingEfforts: ["low", "high", "max"],
supportsVision: true,
},
{
id: "glm-5.3",
name: "GLM-5.3",
toolCalling: false,
supportsReasoning: true,
supportedThinkingEfforts: ["low", "high", "max"],
},
{
id: "glm-5.2",
name: "GLM-5.2",
toolCalling: false,
supportsReasoning: true,
},
{
id: "GLM-5.1",
name: "GLM-5.1",
toolCalling: false,
supportsReasoning: true,
},
{
id: "GLM-5-Turbo",
name: "GLM-5-Turbo",
toolCalling: false,
supportsReasoning: true,
},
{
id: "GLM-5v-Turbo",
name: "GLM-5V-Turbo",
toolCalling: false,
supportsReasoning: true,
supportsVision: true,
supportedThinkingEfforts: ["high", "max"],
},
],
};

View File

@@ -5,8 +5,9 @@
* browser-issued CAPTCHA proof for chat completions. The browser transport is
* the default; callers with a short-lived proof can use the direct HTTP path.
*
* Completions go to /api/v2/chat/completions; the older unversioned
* /api/chat/completions path is stale and 404s model-independently (#8014).
* Completions go to /api/v2/chat/completions. Z.ai's CAPTCHA rejects true
* headless Chromium with F001, so the browser transport uses off-screen headed
* Chromium while retaining the shared browser pool.
*/
import { createHash, randomUUID } from "node:crypto";
import { BaseExecutor, type ExecuteInput } from "./base.ts";
@@ -67,6 +68,7 @@ export {
parseZaiFrontendVersion,
resolveZaiThinkingConfig,
resolveZaiVlmConfig,
zaiUpstreamModelId,
} from "./zai-web/protocol.ts";
export type {
ZaiModelCapabilities,
@@ -174,6 +176,7 @@ function buildZaiBrowserChatOptions(input: {
userAgent: ZAI_USER_AGENT,
locale: "en-US",
timezone: "Asia/Seoul",
headless: false,
inputSelector: "#chat-input",
submitButtonSelector: '[aria-label="Send Message"] button:not([disabled])',
submitButtonMode: "dom",
@@ -243,7 +246,7 @@ function resolveZaiRequest(
const modelId = (bodyObj.model as string) || model || ZAI_DEFAULT_MODEL;
if (imageUrls.length > 0 && !getZaiModelCapabilities(modelId).vision) {
return fail(
`Z.ai model ${unprefixedModelId(modelId)} does not accept image input; use GLM-5V-Turbo.`
`Z.ai model ${unprefixedModelId(modelId)} does not accept image input; use GLM-5.3-Flash.`
);
}

View File

@@ -23,14 +23,17 @@ async function runStage(name: string, action: () => Promise<void>): Promise<void
async function selectZaiBrowserModel(page: Page, modelName: string): Promise<void> {
const selector = page.locator('[aria-label="Select a model"]').first();
await selector.waitFor({ state: "visible", timeout: 10_000 });
if ((await selector.innerText()).includes(modelName)) return;
if ((await selector.getByText(modelName, { exact: true }).count()) > 0) return;
// The landing-page hero animation can remain above the already-visible
// selector and make coordinate-based clicks time out.
await selector.evaluate((element) => (element as HTMLElement).click());
const menu = page.locator('[role="menu"]').filter({ hasText: modelName }).first();
await menu.waitFor({ state: "visible", timeout: 5_000 });
const modelButton = menu.locator("button").filter({ hasText: modelName }).first();
const modelButton = menu
.getByText(modelName, { exact: true })
.first()
.locator("xpath=ancestor::button[1]");
await modelButton.evaluate((element) => (element as HTMLElement).click());
await page
.locator('[aria-label="Select a model"]')
@@ -73,13 +76,13 @@ async function setZaiBrowserWebSearch(page: Page, enabled: boolean): Promise<voi
}
}
/** Pick the High/Max effort button inside an already-open Deep Think menu. */
/** Pick the Low/High/Max effort button inside an already-open Deep Think menu. */
async function selectZaiBrowserEffortLevel(
menu: ReturnType<Page["locator"]>,
effort: ZaiThinkingConfig["effort"]
): Promise<void> {
const effortButton = menu.locator("button").filter({
hasText: effort === "high" ? "High" : "Max",
hasText: effort === "low" ? "Low" : effort === "high" ? "High" : "Max",
});
if ((await effortButton.getAttribute("data-selected")) === "true") return;
await runStage(`select ${effort}`, () =>

View File

@@ -7,8 +7,8 @@ import { normalizeCookie, sanitizeErrorMessage } from "../../utils/error.ts";
export const ZAI_BASE_URL = "https://chat.z.ai";
export const ZAI_NEW_CHAT_URL = `${ZAI_BASE_URL}/api/v1/chats/new`;
export const ZAI_CHAT_URL = `${ZAI_BASE_URL}/api/v2/chat/completions`;
export const ZAI_DEFAULT_MODEL = "GLM-5.1";
export const ZAI_DEFAULT_FE_VERSION = "prod-fe-1.1.79";
export const ZAI_DEFAULT_MODEL = "glm-5.3";
export const ZAI_DEFAULT_FE_VERSION = "prod-fe-1.1.92";
export const ZAI_USER_AGENT =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
export const ZAI_FE_VERSION_CACHE_TTL_MS = 15 * 60 * 1000;
@@ -21,7 +21,7 @@ export interface NewChatRequest {
userMessageId: string;
}
export type ZaiReasoningEffort = "high" | "max";
export type ZaiReasoningEffort = "low" | "high" | "max";
export interface ZaiThinkingConfig {
enabled: boolean;
@@ -61,12 +61,23 @@ const NO_ZAI_MODEL_CAPABILITIES: ZaiModelCapabilities = Object.freeze({
});
/**
* Verified against chat.z.ai/api/models (prod-fe-1.1.79).
* Verified against chat.z.ai/api/models (prod-fe-1.1.92).
* `returnFc` is the site's internal function-call result capability; it is
* distinct from accepting caller-supplied OpenAI `tools`.
*/
const ZAI_MODEL_CAPABILITIES: Record<string, ZaiModelCapabilities> = {
"glm-5.2": {
"glm-5.3-flash": {
mcp: false,
reasoningEffort: true,
returnFc: true,
thinking: true,
vision: true,
vlmTools: false,
vlmWebSearch: false,
vlmWebsiteMode: false,
webSearch: true,
},
"glm-5.3": {
mcp: true,
reasoningEffort: true,
returnFc: true,
@@ -77,9 +88,9 @@ const ZAI_MODEL_CAPABILITIES: Record<string, ZaiModelCapabilities> = {
vlmWebsiteMode: false,
webSearch: true,
},
"glm-5.1": {
"glm-5.2": {
mcp: true,
reasoningEffort: false,
reasoningEffort: true,
returnFc: true,
thinking: true,
vision: false,
@@ -88,28 +99,6 @@ const ZAI_MODEL_CAPABILITIES: Record<string, ZaiModelCapabilities> = {
vlmWebsiteMode: false,
webSearch: true,
},
"glm-5-turbo": {
mcp: true,
reasoningEffort: false,
returnFc: true,
thinking: true,
vision: false,
vlmTools: false,
vlmWebSearch: false,
vlmWebsiteMode: false,
webSearch: true,
},
"glm-5v-turbo": {
mcp: false,
reasoningEffort: false,
returnFc: true,
thinking: true,
vision: true,
vlmTools: true,
vlmWebSearch: true,
vlmWebsiteMode: true,
webSearch: true,
},
};
export function asRecord(value: unknown): Record<string, unknown> | null {
@@ -136,6 +125,7 @@ export function describeZaiBrowserFailure(result: {
status: number;
body: Buffer;
observedPostUrls?: string[];
observedPostResponses?: Array<{ url: string; status: number }>;
timing: { captureResponseMs: number; totalMs: number };
}): string {
const status = result.status > 0 ? String(result.status) : "no matching response";
@@ -144,10 +134,16 @@ export function describeZaiBrowserFailure(result: {
result.observedPostUrls && result.observedPostUrls.length > 0
? ` Observed POST targets: ${result.observedPostUrls.join(", ")}.`
: "";
const observedResponses =
result.observedPostResponses && result.observedPostResponses.length > 0
? ` Observed POST responses: ${result.observedPostResponses
.map(({ url, status }) => `${url} [${status}]`)
.join(", ")}.`
: "";
const detail =
browserFailureDetail(result.body) ||
(result.status === 0
? `The page did not issue the expected authenticated chat completion request.${observed}`
? `The page did not issue the expected authenticated chat completion request.${observed}${observedResponses}`
: "The browser response body was empty.");
return `Z.ai browser transport failed (${status}; ${timing}): ${detail}`;
}
@@ -307,17 +303,27 @@ export function unprefixedModelId(modelId: string): string {
return modelId.trim().split("/").at(-1) || modelId.trim();
}
export function browserModelName(modelId: string): string {
/** Map OmniRoute's public Flash id to the opaque id used by chat.z.ai's wire API. */
export function zaiUpstreamModelId(modelId: string): string {
const unprefixed = unprefixedModelId(modelId);
if (unprefixed.toLowerCase() === "glm-5.2") return "GLM-5.2";
if (unprefixed.toLowerCase() === "glm-5v-turbo") return "GLM-5V-Turbo";
return unprefixed;
return unprefixed.toLowerCase() === "glm-5.3-flash" ? "x-preview-l" : unprefixed;
}
function zaiCapabilityModelId(modelId: string): string {
const unprefixed = unprefixedModelId(modelId).toLowerCase();
return unprefixed === "x-preview-l" ? "glm-5.3-flash" : unprefixed;
}
export function browserModelName(modelId: string): string {
const normalized = zaiCapabilityModelId(modelId);
if (normalized === "glm-5.3-flash") return "GLM-5.3-Flash";
if (normalized === "glm-5.3") return "GLM-5.3";
if (normalized === "glm-5.2") return "GLM-5.2";
return unprefixedModelId(modelId);
}
export function getZaiModelCapabilities(modelId: string): ZaiModelCapabilities {
return (
ZAI_MODEL_CAPABILITIES[unprefixedModelId(modelId).toLowerCase()] ?? NO_ZAI_MODEL_CAPABILITIES
);
return ZAI_MODEL_CAPABILITIES[zaiCapabilityModelId(modelId)] ?? NO_ZAI_MODEL_CAPABILITIES;
}
function getFeatureOption(body: Record<string, unknown>, key: string): unknown {
@@ -325,7 +331,7 @@ function getFeatureOption(body: Record<string, unknown>, key: string): unknown {
return asRecord(body.features)?.[key];
}
/** Resolve each model's Deep Think control; only GLM-5.2 accepts High/Max effort. */
/** Resolve each model's Deep Think control using its currently exposed effort vocabulary. */
export function resolveZaiThinkingConfig(
modelId: string,
body: Record<string, unknown>
@@ -339,19 +345,26 @@ export function resolveZaiThinkingConfig(
: typeof reasoning?.effort === "string"
? reasoning.effort.trim().toLowerCase()
: "";
const disabled = body.enable_thinking === false || rawEffort === "none" || rawEffort === "off";
const supportsLowEffort = zaiCapabilityModelId(modelId) !== "glm-5.2";
const effort: ZaiReasoningEffort =
rawEffort === "low" || rawEffort === "medium" || rawEffort === "high" ? "high" : "max";
rawEffort === "low" && supportsLowEffort
? "low"
: rawEffort === "low" || rawEffort === "medium" || rawEffort === "high"
? "high"
: "max";
return {
supported,
enabled: supported && !disabled,
// The current GLM-5.3/5.2 consumer models expose effort selection but no
// non-thinking mode. Keep Deep Think enabled even when a generic client
// sends an off/none compatibility value.
enabled: supported,
effort,
effortSupported: capabilities.reasoningEffort,
};
}
/** Resolve GLM-5V-Turbo's visible Web Search and Tools controls. */
/** Resolve the selected model's visible Web Search and Tools controls. */
export function resolveZaiVlmConfig(modelId: string, body: Record<string, unknown>): ZaiVlmConfig {
const capabilities = getZaiModelCapabilities(modelId);
const toolsOption = getFeatureOption(body, "vlm_tools_enable");
@@ -425,7 +438,7 @@ export function buildZaiCompletionUrl(input: {
hostname: "chat.z.ai",
protocol: "https:",
referrer: "",
title: "Z.ai - Advanced AI Chatbot & Agent powered by GLM-5.2",
title: "Z.ai - Advanced AI Chatbot & Agent powered by GLM-5.3",
timezone_offset: "0",
local_time: now.toISOString(),
utc_time: now.toUTCString(),
@@ -448,13 +461,14 @@ export function buildZaiNewChatBody(
): NewChatRequest {
const prompt = latestUserPrompt(messages);
const userMessageId = randomUUID();
const upstreamModelId = zaiUpstreamModelId(modelId);
return {
userMessageId,
payload: {
chat: {
id: "",
title: "New Chat",
models: [modelId],
models: [upstreamModelId],
params: {},
history: {
messages: {
@@ -465,7 +479,7 @@ export function buildZaiNewChatBody(
role: "user",
content: prompt,
timestamp: Math.floor(Date.now() / 1000),
models: [modelId],
models: [upstreamModelId],
},
},
currentId: userMessageId,
@@ -530,7 +544,7 @@ export function buildZaiRequestBody(input: {
}
return {
stream: true,
model: input.modelId,
model: zaiUpstreamModelId(input.modelId),
messages: foldMessages(input.messages),
signature_prompt: input.prompt,
params,

View File

@@ -236,6 +236,7 @@ export async function browserBackedChat(
userAgent,
locale,
timezone,
headless,
inputSelector,
submitButtonSelector,
submitButtonMode = "playwright",
@@ -257,11 +258,13 @@ export async function browserBackedChat(
userAgent,
locale,
timezone,
headless,
});
const acquireContextMs = Date.now() - tAcquireStart;
const page = await openPage(pooled);
const observedPostUrls: string[] = [];
const observedPostResponses: Array<{ url: string; status: number }> = [];
page.on("request", (request) => {
if (request.method() !== "POST") return;
try {
@@ -273,6 +276,19 @@ export async function browserBackedChat(
// Ignore malformed/non-HTTP request URLs.
}
});
page.on("response", (response) => {
if (response.request().method() !== "POST") return;
try {
const url = new URL(response.url());
if (!url.hostname.endsWith(chatUrlMatchDomain)) return;
observedPostResponses.push({
url: `${url.origin}${url.pathname}`,
status: response.status(),
});
} catch {
// Ignore malformed/non-HTTP response URLs.
}
});
try {
const tNavStart = Date.now();
await withAbort(
@@ -379,6 +395,7 @@ export async function browserBackedChat(
body,
isStealth: pooled.isStealth,
observedPostUrls,
observedPostResponses,
timing: {
acquireContextMs,
navigateMs,
@@ -404,6 +421,7 @@ export async function browserBackedChat(
body,
isStealth: pooled.isStealth,
observedPostUrls,
observedPostResponses,
timing: {
acquireContextMs,
navigateMs: 0,

View File

@@ -29,6 +29,8 @@ export interface BrowserBackedChatRequest {
locale?: string;
/** Browser IANA timezone. Defaults to America/New_York. */
timezone?: string;
/** Launch a headed browser when the provider rejects true headless mode. */
headless?: boolean;
/** Selector for the provider chat input. */
inputSelector: string;
/** Optional selector for the provider submit button. */
@@ -64,6 +66,8 @@ export interface BrowserBackedChatResult {
isStealth: boolean;
/** Sanitized POST targets observed while submitting. */
observedPostUrls?: string[];
/** Sanitized POST response targets and statuses observed while submitting. */
observedPostResponses?: Array<{ url: string; status: number }>;
timing: {
acquireContextMs: number;
navigateMs: number;

View File

@@ -30,7 +30,7 @@ function installZaiFetch(
const value = String(url);
if (value === ZAI_HOME_URL) {
return new Response(
'<script src="https://z-cdn.chatglm.cn/z-ai/frontend/prod-fe-1.1.79/assets/index.js"></script>'
'<script src="https://z-cdn.chatglm.cn/z-ai/frontend/prod-fe-1.1.92/assets/index.js"></script>'
);
}
if (value === ZAI_NEW_CHAT_URL) {
@@ -90,13 +90,18 @@ describe("ZaiWebExecutor", () => {
"Z.ai browser transport failed (502; capture 30001ms, total 33412ms): " +
"browserBackedChat failed: response.body unavailable"
);
assert.match(
assert.equal(
mod.describeZaiBrowserFailure({
status: 0,
body: Buffer.alloc(0),
observedPostUrls: ["https://chat.z.ai/api/v1/chats/new"],
observedPostResponses: [{ url: "https://chat.z.ai/api/v1/chats/new", status: 200 }],
timing: { captureResponseMs: 30_000, totalMs: 33_000 },
}),
/no matching response.*did not issue the expected authenticated chat completion request/
"Z.ai browser transport failed (no matching response; capture 30000ms, total 33000ms): " +
"The page did not issue the expected authenticated chat completion request. " +
"Observed POST targets: https://chat.z.ai/api/v1/chats/new. " +
"Observed POST responses: https://chat.z.ai/api/v1/chats/new [200]."
);
});
@@ -128,9 +133,9 @@ describe("ZaiWebExecutor", () => {
it("parses the deployed frontend version from the homepage asset path", () => {
assert.equal(
mod.parseZaiFrontendVersion(
"https://z-cdn.chatglm.cn/z-ai/frontend/prod-fe-1.1.79/assets/index.js"
"https://z-cdn.chatglm.cn/z-ai/frontend/prod-fe-1.1.92/assets/index.js"
),
"prod-fe-1.1.79"
"prod-fe-1.1.92"
);
assert.equal(mod.parseZaiFrontendVersion("<html></html>"), null);
});
@@ -211,86 +216,87 @@ describe("ZaiWebExecutor", () => {
]);
});
it("enables Deep Think for every public model and limits effort to GLM-5.2", () => {
assert.deepEqual(mod.resolveZaiThinkingConfig("glm-5.2", {}), {
it("maps the three public models to their current Deep Think effort vocabularies", () => {
assert.deepEqual(mod.resolveZaiThinkingConfig("glm-5.3-flash", { reasoning_effort: "low" }), {
supported: true,
enabled: true,
effort: "max",
effort: "low",
effortSupported: true,
});
assert.deepEqual(mod.resolveZaiThinkingConfig("zw/glm-5.2", { reasoning_effort: "medium" }), {
assert.deepEqual(mod.resolveZaiThinkingConfig("zw/glm-5.3", { reasoning_effort: "medium" }), {
supported: true,
enabled: true,
effort: "high",
effortSupported: true,
});
assert.deepEqual(mod.resolveZaiThinkingConfig("glm-5.2", { reasoning: { effort: "high" } }), {
assert.deepEqual(mod.resolveZaiThinkingConfig("glm-5.3", { reasoning: { effort: "high" } }), {
supported: true,
enabled: true,
effort: "high",
effortSupported: true,
});
assert.deepEqual(mod.resolveZaiThinkingConfig("glm-5.2", { reasoning_effort: "off" }), {
supported: true,
enabled: false,
effort: "max",
effortSupported: true,
});
assert.deepEqual(mod.resolveZaiThinkingConfig("GLM-5.1", { reasoning_effort: "max" }), {
assert.deepEqual(mod.resolveZaiThinkingConfig("glm-5.3", { reasoning_effort: "off" }), {
supported: true,
enabled: true,
effort: "max",
effortSupported: false,
effortSupported: true,
});
assert.deepEqual(mod.resolveZaiThinkingConfig("glm-5.3", { enable_thinking: false }), {
supported: true,
enabled: true,
effort: "max",
effortSupported: true,
});
assert.deepEqual(mod.resolveZaiThinkingConfig("glm-5.2", { reasoning_effort: "low" }), {
supported: true,
enabled: true,
effort: "high",
effortSupported: true,
});
});
it("maps GLM-5V-Turbo vision and internal VLM controls from live capabilities", () => {
assert.deepEqual(mod.getZaiModelCapabilities("zw/GLM-5v-Turbo"), {
it("maps GLM-5.3-Flash vision and web controls from live capabilities", () => {
assert.deepEqual(mod.getZaiModelCapabilities("zw/glm-5.3-flash"), {
mcp: false,
reasoningEffort: false,
reasoningEffort: true,
returnFc: true,
thinking: true,
vision: true,
vlmTools: true,
vlmWebSearch: true,
vlmWebsiteMode: true,
vlmTools: false,
vlmWebSearch: false,
vlmWebsiteMode: false,
webSearch: true,
});
assert.deepEqual(mod.resolveZaiVlmConfig("GLM-5v-Turbo", {}), {
toolsEnabled: true,
webSearchEnabled: true,
websiteModeEnabled: true,
assert.deepEqual(mod.getZaiModelCapabilities("x-preview-l"), {
mcp: false,
reasoningEffort: true,
returnFc: true,
thinking: true,
vision: true,
vlmTools: false,
vlmWebSearch: false,
vlmWebsiteMode: false,
webSearch: true,
});
assert.deepEqual(
mod.resolveZaiVlmConfig("GLM-5v-Turbo", {
features: {
vlm_tools_enable: false,
vlm_web_search_enable: false,
vlm_website_mode: false,
},
}),
{
toolsEnabled: false,
webSearchEnabled: false,
websiteModeEnabled: true,
}
);
assert.deepEqual(mod.resolveZaiVlmConfig("GLM-5.1", {}), {
assert.deepEqual(mod.resolveZaiVlmConfig("glm-5.3-flash", { web_search: true }), {
toolsEnabled: false,
webSearchEnabled: true,
websiteModeEnabled: false,
});
assert.deepEqual(mod.resolveZaiVlmConfig("glm-5.3", {}), {
toolsEnabled: false,
webSearchEnabled: false,
websiteModeEnabled: false,
});
assert.deepEqual(mod.resolveZaiVlmConfig("GLM-5.1", { web_search: true }), {
toolsEnabled: false,
webSearchEnabled: true,
websiteModeEnabled: false,
});
assert.equal(mod.zaiUpstreamModelId("zai-web/glm-5.3-flash"), "x-preview-l");
assert.equal(mod.zaiUpstreamModelId("zai-web/glm-5.3"), "glm-5.3");
assert.equal(mod.getZaiModelCapabilities("GLM-5.1").thinking, false);
});
it("returns a credential error when no session credential is provided", async () => {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "GLM-5.1",
model: "glm-5.3",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "" },
@@ -313,7 +319,6 @@ describe("ZaiWebExecutor", () => {
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "glm-5.2",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: TEST_TOKEN },
@@ -322,8 +327,10 @@ describe("ZaiWebExecutor", () => {
const completion = await result.response.json();
assert.equal(completion.choices[0].message.content, "Browser");
assert.equal(completion.model, "glm-5.3");
assert.equal(capturedRequest?.localStorage?.token, TEST_TOKEN);
assert.equal(capturedRequest?.localStorageOrigin, "https://chat.z.ai");
assert.equal(capturedRequest?.headless, false);
assert.equal(capturedRequest?.inputSelector, "#chat-input");
assert.equal(
capturedRequest?.submitButtonSelector,
@@ -331,7 +338,7 @@ describe("ZaiWebExecutor", () => {
);
assert.equal(capturedRequest?.submitButtonMode, "dom");
assert.equal(capturedRequest?.userMessage, "hi");
assert.match(capturedRequest?.chatPageUrl ?? "", /model=GLM-5\.2/);
assert.match(capturedRequest?.chatPageUrl ?? "", /model=GLM-5\.3/);
assert.equal(typeof capturedRequest?.beforeSubmit, "function");
assert.equal(result.headers["X-OmniRoute-Transport"], "browser");
assert.equal(result.transformedBody.browser_backed, true);
@@ -342,7 +349,7 @@ describe("ZaiWebExecutor", () => {
}
});
it("configures GLM-5V-Turbo controls on the browser transport", async () => {
it("configures GLM-5.3-Flash on the browser transport", async () => {
let capturedRequest: BrowserBackedChatRequest | null = null;
browserChat.__setBrowserBackedChatOverrideForTesting(async (request) => {
capturedRequest = request;
@@ -352,8 +359,8 @@ describe("ZaiWebExecutor", () => {
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "GLM-5v-Turbo",
body: { messages: [{ role: "user", content: "use the model tools" }] },
model: "glm-5.3-flash",
body: { messages: [{ role: "user", content: "use flash" }] },
stream: false,
credentials: { apiKey: TEST_TOKEN },
signal: null,
@@ -361,19 +368,19 @@ describe("ZaiWebExecutor", () => {
const completion = await result.response.json();
assert.equal(completion.choices[0].message.content, "VLM");
assert.match(capturedRequest?.chatPageUrl ?? "", /model=GLM-5V-Turbo/);
assert.match(capturedRequest?.chatPageUrl ?? "", /model=GLM-5\.3-Flash/);
assert.equal(typeof capturedRequest?.beforeSubmit, "function");
assert.equal(result.transformedBody.enable_thinking, true);
assert.equal(result.transformedBody.vlm_tools_enable, true);
assert.equal(result.transformedBody.vlm_web_search_enable, true);
assert.equal(result.transformedBody.vlm_website_mode, true);
assert.equal("reasoning_effort" in result.transformedBody, false);
assert.equal(result.transformedBody.reasoning_effort, "max");
assert.equal(result.transformedBody.vlm_tools_enable, false);
assert.equal(result.transformedBody.vlm_web_search_enable, false);
assert.equal(result.transformedBody.vlm_website_mode, false);
} finally {
browserChat.__resetBrowserBackedChatOverrideForTesting();
}
});
it("uploads GLM-5V-Turbo image input through the authenticated browser page", async () => {
it("uploads GLM-5.3-Flash image input through the authenticated browser page", async () => {
let capturedRequest: BrowserBackedChatRequest | null = null;
browserChat.__setBrowserBackedChatOverrideForTesting(async (request) => {
capturedRequest = request;
@@ -383,7 +390,7 @@ describe("ZaiWebExecutor", () => {
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "GLM-5v-Turbo",
model: "glm-5.3-flash",
body: {
messages: [
{
@@ -445,7 +452,7 @@ describe("ZaiWebExecutor", () => {
assert.equal(result.response.status, 400);
const parsed = await result.response.json();
assert.match(parsed.error.message, /use GLM-5V-Turbo/);
assert.match(parsed.error.message, /use GLM-5\.3-Flash/);
});
it("creates a chat, signs the v2 request, and forwards the CAPTCHA proof", async () => {
@@ -461,9 +468,9 @@ describe("ZaiWebExecutor", () => {
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "GLM-5.1",
model: "glm-5.3",
body: {
model: "GLM-5.1",
model: "glm-5.3",
messages: [{ role: "user", content: "hello" }],
temperature: 0.4,
web_search: true,
@@ -477,7 +484,7 @@ describe("ZaiWebExecutor", () => {
const newChatHeaders = capture.newChatInit?.headers as Record<string, string>;
assert.equal(newChatHeaders.Authorization, `Bearer ${TEST_TOKEN}`);
const newChatBody = JSON.parse(String(capture.newChatInit?.body));
assert.deepEqual(newChatBody.chat.models, ["GLM-5.1"]);
assert.deepEqual(newChatBody.chat.models, ["glm-5.3"]);
assert.equal(newChatBody.chat.history.currentId.length, 36);
assert.equal(newChatBody.chat.enable_thinking, true);
assert.equal(newChatBody.chat.auto_web_search, true);
@@ -494,11 +501,11 @@ describe("ZaiWebExecutor", () => {
const headers = capture.completionInit?.headers as Record<string, string>;
assert.equal(headers.Authorization, `Bearer ${TEST_TOKEN}`);
assert.equal(headers["X-FE-Version"], "prod-fe-1.1.79");
assert.equal(headers["X-FE-Version"], "prod-fe-1.1.92");
assert.match(headers["X-Signature"], /^[a-f0-9]{64}$/);
const parsedBody = JSON.parse(String(capture.completionInit?.body));
assert.equal(parsedBody.model, "GLM-5.1");
assert.equal(parsedBody.model, "glm-5.3");
assert.equal(parsedBody.stream, true);
assert.deepEqual(parsedBody.messages, [{ role: "user", content: "hello" }]);
assert.equal(parsedBody.signature_prompt, "hello");
@@ -508,7 +515,7 @@ describe("ZaiWebExecutor", () => {
assert.equal(parsedBody.features.web_search, false);
assert.equal(parsedBody.features.auto_web_search, true);
assert.equal(parsedBody.features.enable_thinking, true);
assert.equal("reasoning_effort" in parsedBody.features, false);
assert.equal(parsedBody.features.reasoning_effort, "max");
assert.equal(result.headers.Authorization, "Bearer [REDACTED]");
assert.equal(result.transformedBody.captcha_verify_param, "[REDACTED]");
} finally {
@@ -516,7 +523,7 @@ describe("ZaiWebExecutor", () => {
}
});
it("sends GLM-5.2 Deep Think High through the direct request path", async () => {
it("sends GLM-5.3 Deep Think Low through the direct request path", async () => {
const capture: ZaiFetchCapture = {};
const originalFetch = installZaiFetch(
() =>
@@ -529,36 +536,36 @@ describe("ZaiWebExecutor", () => {
try {
const executor = new mod.ZaiWebExecutor();
await executor.execute({
model: "glm-5.2",
model: "glm-5.3",
body: {
model: "glm-5.2",
model: "glm-5.3",
messages: [{ role: "user", content: "think carefully" }],
reasoning_effort: "high",
reasoning_effort: "low",
},
stream: false,
credentials: { apiKey: TEST_CREDENTIAL },
signal: null,
});
// #8014: completions must target the versioned v2 path. The query string
// The query string
// carries the per-request signature payload, so match the endpoint prefix.
assert.ok(
String(capture.completionUrl).startsWith("https://chat.z.ai/api/v2/chat/completions?"),
`expected the v2 completions endpoint, got ${capture.completionUrl}`
`expected the current completions endpoint, got ${capture.completionUrl}`
);
const newChatBody = JSON.parse(String(capture.newChatInit?.body));
assert.equal(newChatBody.chat.enable_thinking, true);
assert.equal(newChatBody.chat.reasoning_effort, "high");
assert.equal(newChatBody.chat.reasoning_effort, "low");
const completionBody = JSON.parse(String(capture.completionInit?.body));
assert.equal(completionBody.features.enable_thinking, true);
assert.equal(completionBody.features.reasoning_effort, "high");
assert.equal(completionBody.features.reasoning_effort, "low");
} finally {
globalThis.fetch = originalFetch;
}
});
it("sends GLM-5V-Turbo VLM tools and web-search flags through the direct path", async () => {
it("maps GLM-5.3-Flash to its opaque wire id on the direct path", async () => {
const capture: ZaiFetchCapture = {};
const originalFetch = installZaiFetch(
() =>
@@ -571,10 +578,11 @@ describe("ZaiWebExecutor", () => {
try {
const executor = new mod.ZaiWebExecutor();
await executor.execute({
model: "GLM-5v-Turbo",
model: "glm-5.3-flash",
body: {
model: "GLM-5v-Turbo",
messages: [{ role: "user", content: "inspect this image" }],
model: "glm-5.3-flash",
messages: [{ role: "user", content: "answer quickly" }],
web_search: true,
},
stream: false,
credentials: { apiKey: TEST_CREDENTIAL },
@@ -582,19 +590,26 @@ describe("ZaiWebExecutor", () => {
});
const newChatBody = JSON.parse(String(capture.newChatInit?.body));
assert.deepEqual(newChatBody.chat.models, ["x-preview-l"]);
assert.deepEqual(
newChatBody.chat.history.messages[newChatBody.chat.history.currentId].models,
["x-preview-l"]
);
assert.equal(newChatBody.chat.enable_thinking, true);
assert.equal(newChatBody.chat.auto_web_search, true);
assert.equal(newChatBody.chat.extra.vlm_tools_enable, true);
assert.equal(newChatBody.chat.extra.vlm_web_search_enable, true);
assert.equal(newChatBody.chat.extra.vlm_website_mode, true);
assert.equal(newChatBody.chat.reasoning_effort, "max");
assert.equal(newChatBody.chat.extra.vlm_tools_enable, false);
assert.equal(newChatBody.chat.extra.vlm_web_search_enable, false);
assert.equal(newChatBody.chat.extra.vlm_website_mode, false);
const completionBody = JSON.parse(String(capture.completionInit?.body));
assert.equal(completionBody.model, "x-preview-l");
assert.equal(completionBody.features.enable_thinking, true);
assert.equal(completionBody.features.auto_web_search, false);
assert.equal(completionBody.features.vlm_tools_enable, true);
assert.equal(completionBody.features.vlm_web_search_enable, true);
assert.equal(completionBody.features.vlm_website_mode, true);
assert.equal("reasoning_effort" in completionBody.features, false);
assert.equal(completionBody.features.reasoning_effort, "max");
assert.equal(completionBody.features.auto_web_search, true);
assert.equal(completionBody.features.vlm_tools_enable, false);
assert.equal(completionBody.features.vlm_web_search_enable, false);
assert.equal(completionBody.features.vlm_website_mode, false);
} finally {
globalThis.fetch = originalFetch;
}
@@ -619,7 +634,7 @@ describe("ZaiWebExecutor", () => {
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "GLM-5.1",
model: "glm-5.3",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: TEST_CREDENTIAL },
@@ -651,7 +666,7 @@ describe("ZaiWebExecutor", () => {
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "GLM-5.1",
model: "glm-5.3",
body: { messages: [{ role: "user", content: "hi" }] },
stream: true,
credentials: { apiKey: TEST_CREDENTIAL },
@@ -673,7 +688,7 @@ describe("ZaiWebExecutor", () => {
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "GLM-5.1",
model: "glm-5.3",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: TEST_CREDENTIAL },

View File

@@ -314,7 +314,7 @@ test("resolveModelTestTimeoutMs defaults ordinary model checks to 30 seconds", (
test("resolveModelTestTimeoutMs gives zai-web checks up to 60 seconds", () => {
assert.equal(resolveModelTestTimeoutMs("zai-web", "glm-5.2", 30_000), 60_000);
assert.equal(resolveModelTestTimeoutMs("zai-web", "zai-web/GLM-5V-Turbo", 90_000), 90_000);
assert.equal(resolveModelTestTimeoutMs("zai-web", "zai-web/glm-5.3-flash", 90_000), 90_000);
});
// ---------------------------------------------------------------------------

View File

@@ -9,7 +9,7 @@ const TEST_TOKEN = "e30.eyJpZCI6InVzZXItMTIzIn0.sig";
/**
* #8014 guard: the executor must target the versioned v2 completions endpoint,
* never the stale unversioned `/api/chat/completions` path, which 404s
* model-independently as of 2026-07.
* model-independently.
*
* Setup notes for this flow (the executor now creates a remote chat first and
* signs the completion request):
@@ -43,7 +43,7 @@ test("#8014: ZaiWebExecutor must POST to the current chat.z.ai v2 chat-completio
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "glm-4.6",
model: "glm-5.3",
body: { messages: [{ role: "user", content: "hello" }] },
stream: false,
credentials: {
@@ -55,7 +55,6 @@ test("#8014: ZaiWebExecutor must POST to the current chat.z.ai v2 chat-completio
assert.ok(requested.length > 0, "the direct path must actually reach fetch");
assert.ok(
// Exact-URL match (not a substring test): `requested` holds whole URLs.
!requested.some((url) => url === STALE_URL),
`zai-web executor POSTed to the stale endpoint — matches #8014's model-independent 404 "Not Found"`
);

View File

@@ -13,7 +13,7 @@ const providersDb = await import("../../src/lib/db/providers.ts");
const modelsRoute = await import("../../src/app/api/providers/[id]/models/route.ts");
const registry = await import("../../open-sse/config/providers/registry/zai-web/index.ts");
const CURATED_ZAI_WEB_MODEL_IDS = ["glm-5.2", "GLM-5.1", "GLM-5-Turbo", "GLM-5v-Turbo"];
const CURATED_ZAI_WEB_MODEL_IDS = ["glm-5.3-flash", "glm-5.3", "glm-5.2"];
async function resetStorage() {
core.resetDbInstance();
@@ -31,34 +31,32 @@ test("zai-web publishes the live reasoning and vision capabilities", () => {
registry.zai_webProvider.models.map((model) => ({
id: model.id,
supportsReasoning: model.supportsReasoning === true,
supportedThinkingEfforts: model.supportedThinkingEfforts,
supportsVision: model.supportsVision === true,
toolCalling: model.toolCalling === true,
})),
[
{
id: "glm-5.3-flash",
supportsReasoning: true,
supportedThinkingEfforts: ["low", "high", "max"],
supportsVision: true,
toolCalling: false,
},
{
id: "glm-5.3",
supportsReasoning: true,
supportedThinkingEfforts: ["low", "high", "max"],
supportsVision: false,
toolCalling: false,
},
{
id: "glm-5.2",
supportsReasoning: true,
supportedThinkingEfforts: ["high", "max"],
supportsVision: false,
toolCalling: false,
},
{
id: "GLM-5.1",
supportsReasoning: true,
supportsVision: false,
toolCalling: false,
},
{
id: "GLM-5-Turbo",
supportsReasoning: true,
supportsVision: false,
toolCalling: false,
},
{
id: "GLM-5v-Turbo",
supportsReasoning: true,
supportsVision: true,
toolCalling: false,
},
]
);
});