From 032adb0809246fcbc70376a4e2d3ad96c4ff21e7 Mon Sep 17 00:00:00 2001 From: backryun Date: Thu, 3 Sep 2026 19:49:42 +0900 Subject: [PATCH] feat(providers): refresh Z.ai Web models and browser transport (#12524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../providers/registry/zai-web/index.ts | 35 ++-- open-sse/executors/zai-web.ts | 9 +- .../executors/zai-web/browserAutomation.ts | 11 +- open-sse/executors/zai-web/protocol.ts | 106 ++++++---- open-sse/services/browserBackedChat.ts | 18 ++ open-sse/services/browserBackedChat/types.ts | 4 + tests/unit/executor-zai-web.test.ts | 197 ++++++++++-------- tests/unit/model-test-runner.test.ts | 2 +- .../zai-web-chat-endpoint-8014-probe.test.ts | 5 +- .../zai-web-models-discovery-7678.test.ts | 36 ++-- 10 files changed, 237 insertions(+), 186 deletions(-) diff --git a/open-sse/config/providers/registry/zai-web/index.ts b/open-sse/config/providers/registry/zai-web/index.ts index 98901daab9..59b2b61c31 100644 --- a/open-sse/config/providers/registry/zai-web/index.ts +++ b/open-sse/config/providers/registry/zai-web/index.ts @@ -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"], }, ], }; diff --git a/open-sse/executors/zai-web.ts b/open-sse/executors/zai-web.ts index 6e01aee98d..fb09e4ea82 100644 --- a/open-sse/executors/zai-web.ts +++ b/open-sse/executors/zai-web.ts @@ -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.` ); } diff --git a/open-sse/executors/zai-web/browserAutomation.ts b/open-sse/executors/zai-web/browserAutomation.ts index 38924ada4d..a8aabccc33 100644 --- a/open-sse/executors/zai-web/browserAutomation.ts +++ b/open-sse/executors/zai-web/browserAutomation.ts @@ -23,14 +23,17 @@ async function runStage(name: string, action: () => Promise): Promise { 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, effort: ZaiThinkingConfig["effort"] ): Promise { 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}`, () => diff --git a/open-sse/executors/zai-web/protocol.ts b/open-sse/executors/zai-web/protocol.ts index 562ca85a7f..511e5d3aa9 100644 --- a/open-sse/executors/zai-web/protocol.ts +++ b/open-sse/executors/zai-web/protocol.ts @@ -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 = { - "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 = { 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 = { 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 | 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, key: string): unknown { @@ -325,7 +331,7 @@ function getFeatureOption(body: Record, 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 @@ -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): 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, diff --git a/open-sse/services/browserBackedChat.ts b/open-sse/services/browserBackedChat.ts index 4b3c7078e7..fa51a9c266 100644 --- a/open-sse/services/browserBackedChat.ts +++ b/open-sse/services/browserBackedChat.ts @@ -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, diff --git a/open-sse/services/browserBackedChat/types.ts b/open-sse/services/browserBackedChat/types.ts index c90fb3b80e..3a197be16c 100644 --- a/open-sse/services/browserBackedChat/types.ts +++ b/open-sse/services/browserBackedChat/types.ts @@ -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; diff --git a/tests/unit/executor-zai-web.test.ts b/tests/unit/executor-zai-web.test.ts index aa26418f9c..ec77c94f5e 100644 --- a/tests/unit/executor-zai-web.test.ts +++ b/tests/unit/executor-zai-web.test.ts @@ -30,7 +30,7 @@ function installZaiFetch( const value = String(url); if (value === ZAI_HOME_URL) { return new Response( - '' + '' ); } 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(""), 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; 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; 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 }, diff --git a/tests/unit/model-test-runner.test.ts b/tests/unit/model-test-runner.test.ts index 7a19e67482..c717ea0bb0 100644 --- a/tests/unit/model-test-runner.test.ts +++ b/tests/unit/model-test-runner.test.ts @@ -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); }); // --------------------------------------------------------------------------- diff --git a/tests/unit/zai-web-chat-endpoint-8014-probe.test.ts b/tests/unit/zai-web-chat-endpoint-8014-probe.test.ts index 926697d31e..46bb23266a 100644 --- a/tests/unit/zai-web-chat-endpoint-8014-probe.test.ts +++ b/tests/unit/zai-web-chat-endpoint-8014-probe.test.ts @@ -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"` ); diff --git a/tests/unit/zai-web-models-discovery-7678.test.ts b/tests/unit/zai-web-models-discovery-7678.test.ts index 82837c541f..380c4ba504 100644 --- a/tests/unit/zai-web-models-discovery-7678.test.ts +++ b/tests/unit/zai-web-models-discovery-7678.test.ts @@ -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, - }, ] ); });