mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 18:22:48 +03:00
* fix: complete Z.ai web browser transport * refactor: address Z.ai review feedback * test(zai-web): reconcile the #8014 endpoint guard with the chats/new + signed flow Rebasing onto release/v3.8.49 pulled in #8503, which repointed CHAT_URL to /api/v2/chat/completions and added an endpoint probe. This branch already targets v2, so the executor conflict resolved to this branch's superset (NEW_CHAT_URL + signature constants alongside the same v2 CHAT_URL). The two tests needed adapting, because #8503's assertions assume the pre-rework flow: - executor-zai-web.test.ts: the completion URL now carries the request signature as a query string, so an exact-equality check on the endpoint can never match. Assert the v2 prefix instead. - zai-web-chat-endpoint-8014-probe.test.ts: the probe drove the executor with a bare cookie credential and no captcha proof, which now routes through the browser transport — fetch was never called and the probe captured nothing. Supplied a direct-path credential, and matched on pathname across all requests (the executor also probes the homepage for the frontend version and calls /api/v1/chats/new first). The guard's intent is unchanged and slightly strengthened: it now asserts no request reaches the stale unversioned path and that exactly one completions request is issued, against v2. 54/54 across the zai suites; typecheck:core and eslint clean. * fix(zai-web): surface upstream error frames instead of finishing empty Reported on this PR: HTTP 200, `out=0`, stream "complete", no content and no diagnosis. Cause. HTTP-level failures are already handled — fetchUpstream turns any !ok response into a makeErrorResult with the sanitized body. The gap is a 200 whose SSE body carries an error payload: parseZaiFrame returns null for it, drainSseDeltas drops it, and buildZaiStreamingBody then closes with an empty assistant message + stop + [DONE]. The caller reads that as a successful empty completion, so a rejected signature, an expired captcha and a stale token all look identical — which is why this had to be diagnosed by reading code rather than logs. Hard Rule #6. Fix. parseZaiFrame now classifies an affirmatively error-shaped frame (`error` at the top level or under `data`, string or {detail|message|msg}) as a terminal delta, checked before the delta paths so it cannot fall through to the "no usable delta" null. The stream emits it as `[Z.ai error] <message>`, matching the mid-stream convention the other web executors already use (zed-hosted's createErrorChunk) — the 200 is on the wire, so the status cannot change, but the caller must not be left reading a blank success. Content streamed before the failure is preserved. Message goes through sanitizeErrorMessage (Rule #12). Deliberately NOT changed: a contentless frame still parses to null. That is live-validated behaviour, not an oversight — z.ai emits phase frames with no delta_content, and executor-zai-web.test.ts pins it ("returns null for frames with no usable delta"). Treating "nothing parseable arrived" as a failure would invent policy on top of an observed protocol and risk false errors on the happy path, so this only adds recognition of explicit error frames. Tests (TDD, RED then GREEN): zai-web-silent-empty-repro.test.ts — 7 cases. Error frame classified and terminal; surfaced through the stream with the upstream's own text; surfaced after partial content without losing it; plus a REGRESSION GUARD that contentless/phase-only frames are still skipped, and two controls that the happy path and reasoning-only output are untouched. The guard and controls passed before the fix; the four error cases did not. 94/94 across the zai + stream suites; typecheck:core, eslint and check:file-size clean. * refactor(sse): extract the zai-web transports so the complexity ratchet holds The v3.8.49 merge-train rebaseline (#8686) set the ceiling to the tip's own measurement, leaving zero headroom, so this branch's +5 cyclomatic / +3 cognitive own-growth had nowhere to sit once rebased onto it. Eight violations, all in code this branch introduces, resolved by extraction — no behaviour change: - `execute` (152 lines, complexity 25, cognitive 20) now delegates to `resolveZaiRequest()` for the four client-error rejections and to a `fetchViaSignedApi()` method for the CAPTCHA/signature path, so it reads as "validate, pick a transport, shape the response". - `fetchThroughBrowser` (126 lines, cognitive 16) hands its image decoding to `resolveZaiBrowserAttachments()`, its Playwright options to `buildZaiBrowserChatOptions()`, and its call-log payload to `buildZaiBrowserAuditBody()`. - `configureZaiBrowserEffort` (cognitive 35 — the worst of the set) repeated a wrap-and-relabel try/catch four times inside an if/else. `runStage`, which already existed one function below, is now module-scoped and reused, and the toggle collapses to `checked !== config.enabled` (same four cases). - `validateWebCookieProvider` (complexity 19) moves its can-we-probe-this cascade into `resolveWebCookieProbe()`, which returns either a rejection or the URL + headers to use. - `acquireBrowserContext`'s creation closure (complexity 17) hands cookie and localStorage seeding to `seedContextSession()`. That last extraction also clears a violation that predates this branch — `acquireBrowserContext` was already over the 80-line ceiling — so cyclomatic lands at 2187 against a baseline of 2188. Verified: check:complexity-ratchets green both metrics; typecheck:core clean; ESLint clean on all four files; 85 tests across the zai-web, web-cookie validation, browser-pool and model-test-runner suites pass. * fix(zai-web): surface upstream errors on the non-streaming path collectZaiNonStreaming ignored delta.error — a 200 whose SSE body carries an error frame (rejected signature, expired captcha, stale token) came back as a successful empty completion. Now it throws on an error frame, matching the streaming path's [Z.ai error] convention; the caller's existing try/catch returns makeErrorResult(502) instead of an empty 200. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: backryun <busan011@ormbiz.co.kr> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
689 lines
24 KiB
TypeScript
689 lines
24 KiB
TypeScript
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { Buffer } from "node:buffer";
|
|
import type { BrowserBackedChatRequest } from "../../open-sse/services/browserBackedChat.ts";
|
|
|
|
const mod = await import("../../open-sse/executors/zai-web.ts");
|
|
const browserChat = await import("../../open-sse/services/browserBackedChat.ts");
|
|
|
|
const ZAI_HOME_URL = "https://chat.z.ai/";
|
|
const ZAI_NEW_CHAT_URL = "https://chat.z.ai/api/v1/chats/new";
|
|
const ZAI_COMPLETION_PATH = "/api/v2/chat/completions";
|
|
const TEST_TOKEN = `e30.${Buffer.from(JSON.stringify({ id: "user-123" })).toString("base64url")}.sig`;
|
|
const TEST_CREDENTIAL = JSON.stringify({
|
|
token: TEST_TOKEN,
|
|
captcha_verify_param: "captcha-proof",
|
|
});
|
|
|
|
interface ZaiFetchCapture {
|
|
completionInit?: RequestInit;
|
|
completionUrl?: string;
|
|
newChatInit?: RequestInit;
|
|
}
|
|
|
|
function installZaiFetch(
|
|
completionResponse: () => Response,
|
|
capture: ZaiFetchCapture = {}
|
|
): typeof globalThis.fetch {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
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>'
|
|
);
|
|
}
|
|
if (value === ZAI_NEW_CHAT_URL) {
|
|
capture.newChatInit = init;
|
|
return Response.json({ id: "chat-123" });
|
|
}
|
|
if (new URL(value).pathname === ZAI_COMPLETION_PATH) {
|
|
capture.completionUrl = value;
|
|
capture.completionInit = init;
|
|
return completionResponse();
|
|
}
|
|
return new Response("not found", { status: 404 });
|
|
}) as typeof globalThis.fetch;
|
|
return originalFetch;
|
|
}
|
|
|
|
function makeBrowserResult(content: string) {
|
|
return {
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body: Buffer.from(
|
|
[
|
|
`data: ${JSON.stringify({ type: "chat:completion", data: { delta_content: content, phase: "answer", done: false } })}`,
|
|
`data: ${JSON.stringify({ type: "chat:completion", data: { phase: "done", done: true } })}`,
|
|
"",
|
|
"",
|
|
].join("\n")
|
|
),
|
|
isStealth: true,
|
|
timing: {
|
|
acquireContextMs: 1,
|
|
navigateMs: 1,
|
|
submitMs: 1,
|
|
captureResponseMs: 1,
|
|
totalMs: 4,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("ZaiWebExecutor", () => {
|
|
it("can be instantiated", () => {
|
|
const executor = new mod.ZaiWebExecutor();
|
|
assert.ok(executor);
|
|
});
|
|
|
|
it("preserves browser transport failure details and timing", () => {
|
|
assert.equal(
|
|
mod.describeZaiBrowserFailure({
|
|
status: 502,
|
|
body: Buffer.from(
|
|
JSON.stringify({
|
|
error: { message: "browserBackedChat failed: response.body unavailable" },
|
|
})
|
|
),
|
|
timing: { captureResponseMs: 30_001, totalMs: 33_412 },
|
|
}),
|
|
"Z.ai browser transport failed (502; capture 30001ms, total 33412ms): " +
|
|
"browserBackedChat failed: response.body unavailable"
|
|
);
|
|
assert.match(
|
|
mod.describeZaiBrowserFailure({
|
|
status: 0,
|
|
body: Buffer.alloc(0),
|
|
timing: { captureResponseMs: 30_000, totalMs: 33_000 },
|
|
}),
|
|
/no matching response.*did not issue the expected authenticated chat completion request/
|
|
);
|
|
});
|
|
|
|
it("extracts the token cookie value from a full Cookie header", () => {
|
|
assert.equal(mod.extractZaiToken("token=abc123; other=xyz"), "abc123");
|
|
assert.equal(mod.extractZaiToken("Cookie: other=xyz; token=abc123"), "abc123");
|
|
});
|
|
|
|
it("extracts the current localStorage Bearer token and JSON credential", () => {
|
|
assert.equal(mod.extractZaiToken("Bearer abc123"), "abc123");
|
|
assert.equal(mod.extractZaiToken("Authorization: Bearer abc123"), "abc123");
|
|
assert.equal(mod.extractZaiToken(TEST_CREDENTIAL), TEST_TOKEN);
|
|
assert.equal(mod.extractZaiCaptchaVerifyParam(TEST_CREDENTIAL), "captcha-proof");
|
|
assert.equal(mod.extractZaiUserId(TEST_TOKEN), "user-123");
|
|
});
|
|
|
|
it("reproduces the live frontend HMAC signature algorithm", () => {
|
|
assert.equal(
|
|
mod.buildZaiSignature({
|
|
prompt: "Reply with exactly: OMNIROUTE_ZAI_WEB_TEST",
|
|
requestId: "3b907de9-793c-41d1-8b8e-6ed6a714ee08",
|
|
timestamp: 1784855934807,
|
|
userId: "user-123",
|
|
}),
|
|
"14f17673ccd4ec86476549ebe60f181529572f7a0cfe8ba179206cf2d37cf442"
|
|
);
|
|
});
|
|
|
|
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"
|
|
),
|
|
"prod-fe-1.1.79"
|
|
);
|
|
assert.equal(mod.parseZaiFrontendVersion("<html></html>"), null);
|
|
});
|
|
|
|
it("accepts a bare JWT/token with no cookie name prefix", () => {
|
|
// a bare token with no '=' and no ';' falls through to the raw string
|
|
assert.equal(
|
|
mod.extractZaiToken("eyJhbGciOiJIUzI1NiJ9.payload.sig"),
|
|
"eyJhbGciOiJIUzI1NiJ9.payload.sig"
|
|
);
|
|
assert.equal(mod.extractZaiToken("plainsessiontoken"), "plainsessiontoken");
|
|
});
|
|
|
|
it("returns empty string when no cookie is provided", () => {
|
|
assert.equal(mod.extractZaiToken(""), "");
|
|
});
|
|
|
|
it("parses the internal z.ai delta_content/phase SSE envelope", () => {
|
|
const delta = mod.parseZaiFrame({
|
|
type: "chat:completion",
|
|
data: { delta_content: "Hello", phase: "answer", done: false },
|
|
});
|
|
assert.deepEqual(delta, { content: "Hello", reasoning: "", done: false });
|
|
});
|
|
|
|
it("routes thinking-phase content into the reasoning field", () => {
|
|
const delta = mod.parseZaiFrame({
|
|
type: "chat:completion",
|
|
data: { delta_content: "pondering...", phase: "thinking", done: false },
|
|
});
|
|
assert.deepEqual(delta, { content: "", reasoning: "pondering...", done: false });
|
|
});
|
|
|
|
it("detects end-of-stream from the internal envelope", () => {
|
|
const delta = mod.parseZaiFrame({
|
|
type: "chat:completion",
|
|
data: { phase: "done", done: true },
|
|
});
|
|
assert.equal(delta?.done, true);
|
|
});
|
|
|
|
it("parses an OpenAI-shaped pass-through frame", () => {
|
|
const delta = mod.parseZaiFrame({
|
|
choices: [{ delta: { content: "Hi there" }, finish_reason: null }],
|
|
});
|
|
assert.deepEqual(delta, { content: "Hi there", reasoning: "", done: false });
|
|
});
|
|
|
|
it("detects end-of-stream from an OpenAI-shaped finish_reason", () => {
|
|
const delta = mod.parseZaiFrame({
|
|
choices: [{ delta: {}, finish_reason: "stop" }],
|
|
});
|
|
assert.equal(delta?.done, true);
|
|
});
|
|
|
|
it("returns null for frames with no usable delta", () => {
|
|
assert.equal(mod.parseZaiFrame(null), null);
|
|
assert.equal(mod.parseZaiFrame({}), null);
|
|
assert.equal(mod.parseZaiFrame({ data: { phase: "answer" } }), null);
|
|
});
|
|
|
|
it("folds multimodal message content into text without leaking image payloads", () => {
|
|
const folded = mod.foldMessages([
|
|
{ role: "user", content: "hi" },
|
|
{ role: "user", content: { foo: "bar" } },
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "inspect this" },
|
|
{ type: "image_url", image_url: { url: "data:image/png;base64,aW1hZ2U=" } },
|
|
],
|
|
},
|
|
]);
|
|
assert.deepEqual(folded, [
|
|
{ role: "user", content: "hi" },
|
|
{ role: "user", content: "" },
|
|
{ role: "user", content: "inspect this" },
|
|
]);
|
|
});
|
|
|
|
it("enables Deep Think for every public model and limits effort to GLM-5.2", () => {
|
|
assert.deepEqual(mod.resolveZaiThinkingConfig("glm-5.2", {}), {
|
|
supported: true,
|
|
enabled: true,
|
|
effort: "max",
|
|
effortSupported: true,
|
|
});
|
|
assert.deepEqual(mod.resolveZaiThinkingConfig("zw/glm-5.2", { reasoning_effort: "medium" }), {
|
|
supported: true,
|
|
enabled: true,
|
|
effort: "high",
|
|
effortSupported: true,
|
|
});
|
|
assert.deepEqual(mod.resolveZaiThinkingConfig("glm-5.2", { 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" }), {
|
|
supported: true,
|
|
enabled: true,
|
|
effort: "max",
|
|
effortSupported: false,
|
|
});
|
|
});
|
|
|
|
it("maps GLM-5V-Turbo vision and internal VLM controls from live capabilities", () => {
|
|
assert.deepEqual(mod.getZaiModelCapabilities("zw/GLM-5v-Turbo"), {
|
|
mcp: false,
|
|
reasoningEffort: false,
|
|
returnFc: true,
|
|
thinking: true,
|
|
vision: true,
|
|
vlmTools: true,
|
|
vlmWebSearch: true,
|
|
vlmWebsiteMode: true,
|
|
webSearch: true,
|
|
});
|
|
assert.deepEqual(mod.resolveZaiVlmConfig("GLM-5v-Turbo", {}), {
|
|
toolsEnabled: true,
|
|
webSearchEnabled: true,
|
|
websiteModeEnabled: 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", {}), {
|
|
toolsEnabled: false,
|
|
webSearchEnabled: false,
|
|
websiteModeEnabled: false,
|
|
});
|
|
assert.deepEqual(mod.resolveZaiVlmConfig("GLM-5.1", { web_search: true }), {
|
|
toolsEnabled: false,
|
|
webSearchEnabled: true,
|
|
websiteModeEnabled: 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",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: false,
|
|
credentials: { apiKey: "" },
|
|
signal: null,
|
|
});
|
|
|
|
assert.equal(result.response.status, 400);
|
|
assert.equal(new URL(result.url).hostname, "chat.z.ai");
|
|
const parsed = await result.response.json();
|
|
assert.match(parsed.error.message, /web-session credential/);
|
|
});
|
|
|
|
it("uses the browser transport with only the Local Storage token", async () => {
|
|
let capturedRequest: BrowserBackedChatRequest | null = null;
|
|
browserChat.__setBrowserBackedChatOverrideForTesting(async (request) => {
|
|
capturedRequest = request;
|
|
return makeBrowserResult("Browser");
|
|
});
|
|
|
|
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 },
|
|
signal: null,
|
|
});
|
|
|
|
const completion = await result.response.json();
|
|
assert.equal(completion.choices[0].message.content, "Browser");
|
|
assert.equal(capturedRequest?.localStorage?.token, TEST_TOKEN);
|
|
assert.equal(capturedRequest?.localStorageOrigin, "https://chat.z.ai");
|
|
assert.equal(capturedRequest?.inputSelector, "#chat-input");
|
|
assert.equal(
|
|
capturedRequest?.submitButtonSelector,
|
|
'[aria-label="Send Message"] button:not([disabled])'
|
|
);
|
|
assert.equal(capturedRequest?.submitButtonMode, "dom");
|
|
assert.equal(capturedRequest?.userMessage, "hi");
|
|
assert.match(capturedRequest?.chatPageUrl ?? "", /model=GLM-5\.2/);
|
|
assert.equal(typeof capturedRequest?.beforeSubmit, "function");
|
|
assert.equal(result.headers["X-OmniRoute-Transport"], "browser");
|
|
assert.equal(result.transformedBody.browser_backed, true);
|
|
assert.equal(result.transformedBody.enable_thinking, true);
|
|
assert.equal(result.transformedBody.reasoning_effort, "max");
|
|
} finally {
|
|
browserChat.__resetBrowserBackedChatOverrideForTesting();
|
|
}
|
|
});
|
|
|
|
it("configures GLM-5V-Turbo controls on the browser transport", async () => {
|
|
let capturedRequest: BrowserBackedChatRequest | null = null;
|
|
browserChat.__setBrowserBackedChatOverrideForTesting(async (request) => {
|
|
capturedRequest = request;
|
|
return makeBrowserResult("VLM");
|
|
});
|
|
|
|
try {
|
|
const executor = new mod.ZaiWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "GLM-5v-Turbo",
|
|
body: { messages: [{ role: "user", content: "use the model tools" }] },
|
|
stream: false,
|
|
credentials: { apiKey: TEST_TOKEN },
|
|
signal: null,
|
|
});
|
|
|
|
const completion = await result.response.json();
|
|
assert.equal(completion.choices[0].message.content, "VLM");
|
|
assert.match(capturedRequest?.chatPageUrl ?? "", /model=GLM-5V-Turbo/);
|
|
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);
|
|
} finally {
|
|
browserChat.__resetBrowserBackedChatOverrideForTesting();
|
|
}
|
|
});
|
|
|
|
it("uploads GLM-5V-Turbo image input through the authenticated browser page", async () => {
|
|
let capturedRequest: BrowserBackedChatRequest | null = null;
|
|
browserChat.__setBrowserBackedChatOverrideForTesting(async (request) => {
|
|
capturedRequest = request;
|
|
return makeBrowserResult("The image says OMNIROUTE.");
|
|
});
|
|
|
|
try {
|
|
const executor = new mod.ZaiWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "GLM-5v-Turbo",
|
|
body: {
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "What word is in this image?" },
|
|
{
|
|
type: "image_url",
|
|
image_url: { url: "data:image/png;base64,aW1hZ2UtYnl0ZXM=" },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
stream: false,
|
|
// Supplying a CAPTCHA proof must not select the direct path for image
|
|
// requests because the browser page owns Z.ai's authenticated upload.
|
|
credentials: { apiKey: TEST_CREDENTIAL },
|
|
signal: null,
|
|
});
|
|
|
|
assert.equal(result.response.status, 200);
|
|
assert.equal(capturedRequest?.userMessage, "What word is in this image?");
|
|
assert.equal(capturedRequest?.attachments?.length, 1);
|
|
assert.equal(capturedRequest?.attachments?.[0]?.name, "omniroute-image-1.png");
|
|
assert.equal(capturedRequest?.attachments?.[0]?.mimeType, "image/png");
|
|
assert.equal(capturedRequest?.attachments?.[0]?.buffer.toString("utf8"), "image-bytes");
|
|
assert.equal(result.transformedBody.image_count, 1);
|
|
assert.deepEqual(result.transformedBody.messages, [
|
|
{ role: "user", content: "What word is in this image?" },
|
|
]);
|
|
} finally {
|
|
browserChat.__resetBrowserBackedChatOverrideForTesting();
|
|
}
|
|
});
|
|
|
|
it("rejects image input on Z.ai text-only models", async () => {
|
|
const executor = new mod.ZaiWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "glm-5.2",
|
|
body: {
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "inspect" },
|
|
{
|
|
type: "image_url",
|
|
image_url: { url: "data:image/png;base64,aW1hZ2U=" },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
stream: false,
|
|
credentials: { apiKey: TEST_TOKEN },
|
|
signal: null,
|
|
});
|
|
|
|
assert.equal(result.response.status, 400);
|
|
const parsed = await result.response.json();
|
|
assert.match(parsed.error.message, /use GLM-5V-Turbo/);
|
|
});
|
|
|
|
it("creates a chat, signs the v2 request, and forwards the CAPTCHA proof", async () => {
|
|
const capture: ZaiFetchCapture = {};
|
|
const originalFetch = installZaiFetch(
|
|
() =>
|
|
new Response("data: [DONE]\n\n", {
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
}),
|
|
capture
|
|
);
|
|
|
|
try {
|
|
const executor = new mod.ZaiWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "GLM-5.1",
|
|
body: {
|
|
model: "GLM-5.1",
|
|
messages: [{ role: "user", content: "hello" }],
|
|
temperature: 0.4,
|
|
web_search: true,
|
|
},
|
|
stream: false,
|
|
credentials: { apiKey: TEST_CREDENTIAL },
|
|
signal: null,
|
|
});
|
|
|
|
assert.ok(capture.newChatInit);
|
|
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.equal(newChatBody.chat.history.currentId.length, 36);
|
|
assert.equal(newChatBody.chat.enable_thinking, true);
|
|
assert.equal(newChatBody.chat.auto_web_search, true);
|
|
|
|
const completionUrl = new URL(String(capture.completionUrl));
|
|
assert.equal(completionUrl.pathname, ZAI_COMPLETION_PATH);
|
|
assert.equal(completionUrl.searchParams.get("token"), TEST_TOKEN);
|
|
assert.equal(completionUrl.searchParams.get("user_id"), "user-123");
|
|
assert.equal(completionUrl.searchParams.get("version"), "0.0.1");
|
|
assert.equal(
|
|
completionUrl.searchParams.get("signature_timestamp"),
|
|
completionUrl.searchParams.get("timestamp")
|
|
);
|
|
|
|
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.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.stream, true);
|
|
assert.deepEqual(parsedBody.messages, [{ role: "user", content: "hello" }]);
|
|
assert.equal(parsedBody.signature_prompt, "hello");
|
|
assert.equal(parsedBody.captcha_verify_param, "captcha-proof");
|
|
assert.equal(parsedBody.chat_id, "chat-123");
|
|
assert.equal(parsedBody.params.temperature, 0.4);
|
|
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(result.headers.Authorization, "Bearer [REDACTED]");
|
|
assert.equal(result.transformedBody.captcha_verify_param, "[REDACTED]");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
it("sends GLM-5.2 Deep Think High through the direct request path", async () => {
|
|
const capture: ZaiFetchCapture = {};
|
|
const originalFetch = installZaiFetch(
|
|
() =>
|
|
new Response("data: [DONE]\n\n", {
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
}),
|
|
capture
|
|
);
|
|
|
|
try {
|
|
const executor = new mod.ZaiWebExecutor();
|
|
await executor.execute({
|
|
model: "glm-5.2",
|
|
body: {
|
|
model: "glm-5.2",
|
|
messages: [{ role: "user", content: "think carefully" }],
|
|
reasoning_effort: "high",
|
|
},
|
|
stream: false,
|
|
credentials: { apiKey: TEST_CREDENTIAL },
|
|
signal: null,
|
|
});
|
|
|
|
// #8014: completions must target the versioned v2 path. 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}`
|
|
);
|
|
const newChatBody = JSON.parse(String(capture.newChatInit?.body));
|
|
assert.equal(newChatBody.chat.enable_thinking, true);
|
|
assert.equal(newChatBody.chat.reasoning_effort, "high");
|
|
|
|
const completionBody = JSON.parse(String(capture.completionInit?.body));
|
|
assert.equal(completionBody.features.enable_thinking, true);
|
|
assert.equal(completionBody.features.reasoning_effort, "high");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
it("sends GLM-5V-Turbo VLM tools and web-search flags through the direct path", async () => {
|
|
const capture: ZaiFetchCapture = {};
|
|
const originalFetch = installZaiFetch(
|
|
() =>
|
|
new Response("data: [DONE]\n\n", {
|
|
headers: { "Content-Type": "text/event-stream" },
|
|
}),
|
|
capture
|
|
);
|
|
|
|
try {
|
|
const executor = new mod.ZaiWebExecutor();
|
|
await executor.execute({
|
|
model: "GLM-5v-Turbo",
|
|
body: {
|
|
model: "GLM-5v-Turbo",
|
|
messages: [{ role: "user", content: "inspect this image" }],
|
|
},
|
|
stream: false,
|
|
credentials: { apiKey: TEST_CREDENTIAL },
|
|
signal: null,
|
|
});
|
|
|
|
const newChatBody = JSON.parse(String(capture.newChatInit?.body));
|
|
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);
|
|
|
|
const completionBody = JSON.parse(String(capture.completionInit?.body));
|
|
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);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
it("aggregates streamed internal-envelope deltas into a non-streaming completion", async () => {
|
|
const originalFetch = installZaiFetch(
|
|
() =>
|
|
new Response(
|
|
[
|
|
`data: ${JSON.stringify({ type: "chat:completion", data: { delta_content: "Hel", phase: "answer", done: false } })}`,
|
|
`data: ${JSON.stringify({ type: "chat:completion", data: { delta_content: "lo", phase: "answer", done: false } })}`,
|
|
`data: ${JSON.stringify({ type: "chat:completion", data: { phase: "done", done: true } })}`,
|
|
"data: [DONE]",
|
|
"",
|
|
"",
|
|
].join("\n"),
|
|
{ headers: { "Content-Type": "text/event-stream" } }
|
|
)
|
|
);
|
|
|
|
try {
|
|
const executor = new mod.ZaiWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "GLM-5.1",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: false,
|
|
credentials: { apiKey: TEST_CREDENTIAL },
|
|
signal: null,
|
|
});
|
|
|
|
const completion = await result.response.json();
|
|
assert.equal(completion.choices[0].message.content, "Hello");
|
|
assert.equal(completion.choices[0].finish_reason, "stop");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
it("streams internal-envelope deltas as OpenAI-shaped SSE chunks", async () => {
|
|
const originalFetch = installZaiFetch(
|
|
() =>
|
|
new Response(
|
|
[
|
|
`data: ${JSON.stringify({ type: "chat:completion", data: { delta_content: "Hi", phase: "answer", done: false } })}`,
|
|
`data: ${JSON.stringify({ type: "chat:completion", data: { phase: "done", done: true } })}`,
|
|
"",
|
|
"",
|
|
].join("\n"),
|
|
{ headers: { "Content-Type": "text/event-stream" } }
|
|
)
|
|
);
|
|
|
|
try {
|
|
const executor = new mod.ZaiWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "GLM-5.1",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: true,
|
|
credentials: { apiKey: TEST_CREDENTIAL },
|
|
signal: null,
|
|
});
|
|
|
|
const text = await result.response.text();
|
|
assert.match(text, /"content":"Hi"/);
|
|
assert.match(text, /"finish_reason":"stop"/);
|
|
assert.match(text, /data: \[DONE\]/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
it("propagates upstream HTTP errors", async () => {
|
|
const originalFetch = installZaiFetch(() => new Response("session expired", { status: 401 }));
|
|
|
|
try {
|
|
const executor = new mod.ZaiWebExecutor();
|
|
const result = await executor.execute({
|
|
model: "GLM-5.1",
|
|
body: { messages: [{ role: "user", content: "hi" }] },
|
|
stream: false,
|
|
credentials: { apiKey: TEST_CREDENTIAL },
|
|
signal: null,
|
|
});
|
|
|
|
assert.equal(result.response.status, 401);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
});
|