feat(providers): add Z.ai Web free web-cookie provider (#4056)

New zai-web web-session provider drives the free chat.z.ai consumer
chat UI via a pasted browser cookie, distinct from the existing
API-key zai/glm/glm-cn/glmt providers (api.z.ai). ZaiWebExecutor posts
to chat.z.ai/api/chat/completions with the cookie forwarded both as
Cookie and Authorization: Bearer <token>, and normalizes both z.ai's
internal delta_content/phase SSE envelope and a pass-through
OpenAI-shaped choices[].delta frame into standard chat-completion
chunks.

Registered in WEB_COOKIE_PROVIDERS, WEB_SESSION_CREDENTIAL_REQUIREMENTS,
the provider registry (GLM-4.6/4.5/4.5V models), the executor factory,
and tokenExtractionConfig.ts for in-app cookie capture.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-10 09:47:22 -03:00
parent 63fb1cbfa6
commit 9074ede9dc
9 changed files with 674 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
### ✨ New Features
- **Z.ai Web (free web-session provider)**: new `zai-web` web-cookie provider drives the free chat.z.ai consumer chat UI via a pasted browser session cookie, distinct from the existing API-key `zai`/`glm`/`glm-cn`/`glmt` providers (`api.z.ai`) — modeled on the `doubao-web`/`venice-web` cookie executors and the pre-existing `chatglm-web` credential requirement/token-extraction entries. `ZaiWebExecutor` (`open-sse/executors/zai-web.ts`) posts to `chat.z.ai/api/chat/completions` with the cookie forwarded both as `Cookie` and as `Authorization: Bearer <token>`, and normalizes both z.ai's internal `delta_content`/`phase` SSE envelope and a pass-through OpenAI-shaped `choices[].delta` frame into standard chat-completion chunks. Registered in `WEB_COOKIE_PROVIDERS`, `WEB_SESSION_CREDENTIAL_REQUIREMENTS`, the provider registry (`zai-web` entry, GLM-4.6/4.5/4.5V models), and `tokenExtractionConfig.ts` for in-app cookie capture. Regression guard: `tests/unit/executor-zai-web.test.ts` (16 tests — token extraction, frame parsing for both SSE shapes, streaming and non-streaming aggregation, error paths). (#4056)
- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625)
- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev)
- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen)

View File

@@ -76,6 +76,7 @@ import { chipotleProvider } from "./registry/chipotle/index.ts";
import { freeaiapikeyProvider } from "./registry/freeaiapikey/index.ts";
import { qwenProvider } from "./registry/qwen/index.ts";
import { qwen_webProvider } from "./registry/qwen/web/index.ts";
import { zai_webProvider } from "./registry/zai-web/index.ts";
import { modalProvider } from "./registry/modal/index.ts";
import { zenmuxProvider } from "./registry/zenmux/index.ts";
import { leonardoProvider } from "./registry/leonardo/index.ts";
@@ -259,6 +260,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
freeaiapikey: freeaiapikeyProvider,
qwen: qwenProvider,
"qwen-web": qwen_webProvider,
"zai-web": zai_webProvider,
modal: modalProvider,
zenmux: zenmuxProvider,
leonardo: leonardoProvider,

View File

@@ -0,0 +1,19 @@
import type { RegistryEntry } from "../../shared.ts";
export const zai_webProvider: RegistryEntry = {
id: "zai-web",
alias: "zw",
format: "openai",
executor: "zai-web",
// Free consumer web chat at chat.z.ai (Zhipu AI) — see
// `open-sse/executors/zai-web.ts` for the cookie/session wire format.
// Distinct from the API-key `zai`/`glm` providers (api.z.ai).
baseUrl: "https://chat.z.ai",
authType: "apikey",
authHeader: "cookie",
models: [
{ id: "glm-4.6", name: "GLM-4.6" },
{ id: "glm-4.5", name: "GLM-4.5" },
{ id: "glm-4.5v", name: "GLM-4.5V (Vision)" },
],
};

View File

@@ -49,6 +49,7 @@ import { V0VercelWebExecutor } from "./v0-vercel-web.ts";
import { KimiWebExecutor } from "./kimi-web.ts";
import { DoubaoWebExecutor } from "./doubao-web.ts";
import { QwenWebExecutor } from "./qwen-web.ts";
import { ZaiWebExecutor } from "./zai-web.ts";
import { KimiExecutor } from "./kimi.ts";
import { TheOldLlmExecutor } from "./theoldllm.ts";
import { ChipotleExecutor } from "./chipotle.ts";
@@ -147,6 +148,8 @@ const executors = {
db: new DoubaoWebExecutor(), // Alias
"qwen-web": new QwenWebExecutor(),
qw: new QwenWebExecutor(), // Alias
"zai-web": new ZaiWebExecutor(),
zw: new ZaiWebExecutor(), // Alias
theoldllm: new TheOldLlmExecutor(),
tllm: new TheOldLlmExecutor(), // Alias
chipotle: new ChipotleExecutor(),

View File

@@ -0,0 +1,393 @@
/**
* ZaiWebExecutor — Z.ai Web Chat (chat.z.ai, free web-session/cookie auth)
*
* Distinct from the existing API-key `zai`/`glm`/`glm-cn`/`glmt` providers
* (Anthropic/OpenAI-compatible `api.z.ai`, see `providers/apikey/regional.ts`).
* This executor targets the *consumer chat* frontend at chat.z.ai — the same
* product family as `chatglm.cn` (Zhipu AI), but the international domain —
* so users without an API key can drive it for free via their browser session,
* modeled on the `chatglm-web` credential entry (#4056) and the `doubao-web` /
* `venice-web` cookie executors.
*
* Endpoint: POST https://chat.z.ai/api/chat/completions
* Auth: full Cookie header from chat.z.ai (must contain the `token` JWT).
* Sent both as `Cookie` and as `Authorization: Bearer <token>` —
* the SPA's own fetch client sets both, and stripping either one
* has been reported (upstream repos) to 401 the request.
* Response: SSE. Frames are z.ai's internal envelope
* `{"type":"chat:completion","data":{"delta_content":"...","phase":"answer","done":false}}`
* — mirrored from the shared Zhipu chatglm.cn/chat.z.ai frontend
* protocol. Some deployments/models pass through an already
* OpenAI-shaped `{"choices":[{"delta":{"content":"..."}}]}` frame
* instead, so the parser accepts both shapes defensively.
*/
import { BaseExecutor, type ExecuteInput } from "./base.ts";
import {
makeExecutorErrorResult as makeErrorResult,
normalizeCookie,
sanitizeErrorMessage,
} from "../utils/error.ts";
const BASE_URL = "https://chat.z.ai";
const CHAT_URL = `${BASE_URL}/api/chat/completions`;
const USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36";
/** Extract the `token` cookie value (JWT) from a full Cookie header string. */
export function extractZaiToken(rawCookie: string): string {
const cookie = normalizeCookie(rawCookie.trim());
if (!cookie) return "";
const match = cookie.match(/(?:^|;\s*)token=([^;]+)/);
if (match) return match[1].trim();
// Users may paste the bare JWT with no `token=` prefix.
return cookie.includes(";") || cookie.includes("=") ? "" : cookie;
}
/**
* One parsed delta out of a z.ai SSE frame: either a content/reasoning chunk
* or a signal that the stream has finished.
*/
export interface ZaiDelta {
content: string;
reasoning: string;
done: boolean;
}
/** Parse an already OpenAI-shaped `{choices:[{delta}]}` pass-through frame. */
function parseOpenAiShapedFrame(choices: Array<Record<string, unknown>>): ZaiDelta {
const delta = (choices[0]?.delta ?? {}) as Record<string, unknown>;
const finishReason = choices[0]?.finish_reason;
return {
content: typeof delta.content === "string" ? delta.content : "",
reasoning: typeof delta.reasoning_content === "string" ? delta.reasoning_content : "",
done: finishReason != null,
};
}
/** Parse the z.ai / chatglm internal `{data:{delta_content,phase,done}}` envelope. */
function parseInternalEnvelopeFrame(
frame: Record<string, unknown>,
data: Record<string, unknown>
): ZaiDelta | null {
const phase = String(data.phase ?? "");
const deltaContent = data.delta_content ?? data.edit_content ?? data.content;
const done =
data.done === true ||
phase === "done" ||
phase === "finish" ||
String(frame.type ?? "") === "chat:completion:finish";
if (typeof deltaContent === "string" && deltaContent) {
const isThinking = phase === "thinking";
return {
content: isThinking ? "" : deltaContent,
reasoning: isThinking ? deltaContent : "",
done,
};
}
if (done) return { content: "", reasoning: "", done: true };
return null;
}
/**
* Parse a single decoded z.ai SSE `data:` JSON payload into a normalized
* delta. Handles both the internal `{data:{delta_content,phase,done}}`
* envelope and a pass-through OpenAI-shaped `{choices:[{delta}]}` frame.
*/
export function parseZaiFrame(raw: unknown): ZaiDelta | null {
if (!raw || typeof raw !== "object") return null;
const frame = raw as Record<string, unknown>;
const choices = frame.choices as Array<Record<string, unknown>> | undefined;
if (Array.isArray(choices) && choices.length > 0) {
return parseOpenAiShapedFrame(choices);
}
const data = (frame.data ?? frame) as Record<string, unknown>;
return parseInternalEnvelopeFrame(frame, data);
}
export function foldMessages(
messages: Array<{ role: string; content: unknown }>
): Array<{ role: string; content: string }> {
return messages.map((m) => ({
role: m.role,
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""),
}));
}
/** Split a chunk of decoded SSE text into complete `data:` payload strings. */
function extractSseDataPayloads(buffer: { text: string }, incoming: string): string[] {
buffer.text += incoming;
const lines = buffer.text.split("\n");
buffer.text = lines.pop() || "";
const payloads: string[] = [];
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const data = line.slice(5).trim();
if (!data || data === "[DONE]") continue;
payloads.push(data);
}
return payloads;
}
/** Parse a raw SSE payload string into a normalized delta, or null if unusable. */
function parseSsePayload(data: string): ZaiDelta | null {
try {
return parseZaiFrame(JSON.parse(data));
} catch {
return null;
}
}
type ChunkEmitter = (
controller: ReadableStreamDefaultController,
delta: Record<string, unknown>,
finish?: string | null
) => void;
/** Emit role/reasoning/content/stop chunks for one delta. Returns true when the stream ended. */
function emitDeltaChunks(
controller: ReadableStreamDefaultController,
delta: ZaiDelta,
emitChunk: ChunkEmitter,
roleState: { emitted: boolean }
): boolean {
if (!roleState.emitted && (delta.content || delta.reasoning)) {
roleState.emitted = true;
emitChunk(controller, { role: "assistant", content: "" });
}
if (delta.reasoning) emitChunk(controller, { reasoning_content: delta.reasoning });
if (delta.content) emitChunk(controller, { content: delta.content });
if (delta.done) {
emitChunk(controller, {}, "stop");
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
controller.close();
return true;
}
return false;
}
export class ZaiWebExecutor extends BaseExecutor {
constructor() {
super("zai-web", { id: "zai-web", baseUrl: BASE_URL });
}
private buildHeaders(rawCookie: string, token: string): Record<string, string> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Accept: "text/event-stream",
"User-Agent": USER_AGENT,
Origin: BASE_URL,
Referer: `${BASE_URL}/`,
};
if (rawCookie) headers.Cookie = rawCookie;
if (token) headers.Authorization = `Bearer ${token}`;
return headers;
}
private buildRequestBody(
messages: Array<{ role: string; content: unknown }>,
modelId: string
): Record<string, unknown> {
return {
stream: true,
model: modelId,
messages: foldMessages(messages),
params: {},
features: {
image_generation: false,
web_search: false,
auto_web_search: false,
},
};
}
/** Drain the streaming response body into an OpenAI-shaped SSE ReadableStream. */
private buildStreamingBody(
sourceBody: ReadableStream<Uint8Array>,
modelId: string,
emitChunk: ChunkEmitter,
signal: AbortSignal | null | undefined
): ReadableStream {
const decoder = new TextDecoder();
return new ReadableStream({
async start(controller) {
const reader = sourceBody.getReader();
const buffer = { text: "" };
const roleState = { emitted: false };
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const payloads = extractSseDataPayloads(buffer, decoder.decode(value, { stream: true }));
for (const raw of payloads) {
const delta = parseSsePayload(raw);
if (!delta) continue;
if (emitDeltaChunks(controller, delta, emitChunk, roleState)) return;
}
}
if (!roleState.emitted) emitChunk(controller, { role: "assistant", content: "" });
emitChunk(controller, {}, "stop");
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
controller.close();
} catch (err) {
if (!signal?.aborted) {
try {
controller.error(err);
} catch {
/* controller already closed */
}
}
}
},
});
}
/** Drain the response body and aggregate all deltas into a single answer/reasoning pair. */
private async collectNonStreaming(
sourceBody: ReadableStream<Uint8Array>
): Promise<{ answer: string; reasoning: string }> {
const decoder = new TextDecoder();
let answer = "";
let reasoning = "";
const reader = sourceBody.getReader();
const buffer = { text: "" };
try {
outer: while (true) {
const { done, value } = await reader.read();
if (done) break;
const payloads = extractSseDataPayloads(buffer, decoder.decode(value, { stream: true }));
for (const raw of payloads) {
const delta = parseSsePayload(raw);
if (!delta) continue;
if (delta.reasoning) reasoning += delta.reasoning;
if (delta.content) answer += delta.content;
if (delta.done) break outer;
}
}
} catch {
/* best-effort — return what we have */
}
return { answer, reasoning };
}
/** POST the chat request upstream. Returns either the upstream Response or an error result. */
private async fetchUpstream(
reqHeaders: Record<string, string>,
reqBody: Record<string, unknown>,
body: unknown,
signal: AbortSignal | null | undefined
): Promise<{ upstream: Response } | { errorResult: ReturnType<typeof makeErrorResult> }> {
let upstream: Response;
try {
upstream = await fetch(CHAT_URL, {
method: "POST",
headers: reqHeaders,
body: JSON.stringify(reqBody),
signal,
});
} catch (err) {
return {
errorResult: makeErrorResult(
502,
`Z.ai fetch failed: ${err instanceof Error ? err.message : "unknown"}`,
body,
CHAT_URL
),
};
}
if (!upstream.ok) {
const errText = await upstream.text().catch(() => "");
return {
errorResult: makeErrorResult(
upstream.status,
`Z.ai error: ${sanitizeErrorMessage(errText)}`,
body,
CHAT_URL
),
};
}
return { upstream };
}
private makeChunkEmitter(id: string, created: number, modelId: string): ChunkEmitter {
return (controller, delta, finish = null) => {
const chunk = {
id,
object: "chat.completion.chunk",
created,
model: modelId,
choices: [{ index: 0, delta, finish_reason: finish }],
};
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`));
};
}
async execute(input: ExecuteInput) {
const { body, credentials, signal, stream: wantStream } = input;
const bodyObj = (body || {}) as Record<string, unknown>;
const rawCookie = normalizeCookie(String(credentials?.apiKey ?? "").trim());
const token = extractZaiToken(rawCookie);
if (!rawCookie && !token) {
return makeErrorResult(
400,
"Missing Z.ai session — paste the full Cookie header from chat.z.ai (must contain token=<JWT>).",
body,
CHAT_URL
);
}
const messages = (bodyObj.messages as Array<{ role: string; content: unknown }>) || [];
const modelId = (bodyObj.model as string) || "glm-4.6";
const reqBody = this.buildRequestBody(messages, modelId);
const reqHeaders = this.buildHeaders(rawCookie, token);
const fetched = await this.fetchUpstream(reqHeaders, reqBody, body, signal);
if ("errorResult" in fetched) return fetched.errorResult;
const { upstream } = fetched;
const id = `chatcmpl-zai-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
const sourceBody = upstream.body ?? new ReadableStream({ start: (c) => c.close() });
const emitChunk = this.makeChunkEmitter(id, created, modelId);
if (wantStream) {
const outStream = this.buildStreamingBody(sourceBody, modelId, emitChunk, signal);
return {
response: new Response(outStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
}),
url: CHAT_URL,
headers: reqHeaders,
transformedBody: reqBody,
};
}
const { answer, reasoning } = await this.collectNonStreaming(sourceBody);
const message: Record<string, unknown> = { role: "assistant", content: answer };
if (reasoning) message.reasoning_content = reasoning;
const completion = {
id,
object: "chat.completion",
created,
model: modelId,
choices: [{ index: 0, message, finish_reason: "stop" }],
};
return {
response: new Response(JSON.stringify(completion), {
headers: { "Content-Type": "application/json" },
}),
url: CHAT_URL,
headers: reqHeaders,
transformedBody: reqBody,
};
}
}

View File

@@ -374,6 +374,17 @@ const RAW_CONFIGS: TokenExtractionConfig[] = [
"Log in to Manus at manus.im. The session cookie will be extracted.",
{ cookieDomain: ".manus.im" }
),
// ── Z.ai Web (#4056) ────────────────────────────────────────
config(
"zai-web",
"Z.ai Web (Free)",
"https://chat.z.ai/",
"https://chat.z.ai",
[{ type: "cookie", name: "token", domain: ".z.ai" }],
"Log in to Z.ai at chat.z.ai. The session token will be extracted.",
{ cookieDomain: ".z.ai" }
),
];
// ─── Registry ───────────────────────────────────────────────────────────────

View File

@@ -319,6 +319,21 @@ export const WEB_COOKIE_PROVIDERS = {
authHint:
"Login at zenmux.ai, then export all cookies using EditThisCookie or Cookie-Editor and paste the full Cookie header string here. Refresh every ~30 days.",
},
"zai-web": {
id: "zai-web",
alias: "zw",
name: "Z.ai Web (Free)",
icon: "auto_awesome",
color: "#2563EB",
textIcon: "ZW",
website: "https://chat.z.ai",
hasFree: true,
freeNote:
"Free consumer web session — GLM chat models via chat.z.ai. Distinct from the API-key zai/glm providers. No subscription required.",
subscriptionRisk: true,
riskNoticeVariant: "webCookie",
authHint: "Paste the full Cookie header from chat.z.ai (must include the token=<JWT> cookie)",
},
};
/** Resolved public site for a web-session provider (href + display host). */

View File

@@ -227,6 +227,13 @@ export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "manus_session"],
},
"zai-web": {
kind: "cookie",
credentialName: "token",
placeholder: "token=... or full Cookie header from chat.z.ai",
acceptsFullCookieHeader: true,
storageKeys: ["cookie", "token"],
},
lmarena: {
kind: "cookie",
// lmarena.ai's auth cookie is `arena-auth-prod-v1` (the legacy hint said `session`,

View File

@@ -0,0 +1,223 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
const mod = await import("../../open-sse/executors/zai-web.ts");
describe("ZaiWebExecutor", () => {
it("can be instantiated", () => {
const executor = new mod.ZaiWebExecutor();
assert.ok(executor);
});
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("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 non-string message content into JSON strings", () => {
const folded = mod.foldMessages([
{ role: "user", content: "hi" },
{ role: "user", content: { foo: "bar" } },
]);
assert.deepEqual(folded, [
{ role: "user", content: "hi" },
{ role: "user", content: '{"foo":"bar"}' },
]);
});
it("returns a credential error when no cookie is provided", async () => {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "glm-4.6",
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, /Z\.ai session/);
});
it("sends the cookie + bearer token and builds the request body", async () => {
const originalFetch = globalThis.fetch;
let capturedUrl = "";
let capturedInit: RequestInit | undefined;
globalThis.fetch = (async (url: string, init?: RequestInit) => {
capturedUrl = String(url);
capturedInit = init;
return new Response("data: [DONE]\n\n", {
headers: { "Content-Type": "text/event-stream" },
});
}) as typeof fetch;
try {
const executor = new mod.ZaiWebExecutor();
await executor.execute({
model: "glm-4.6",
body: { messages: [{ role: "user", content: "hello" }] },
stream: false,
credentials: { apiKey: "token=abc123; foo=bar" },
signal: null,
});
assert.equal(capturedUrl, "https://chat.z.ai/api/chat/completions");
const headers = capturedInit?.headers as Record<string, string>;
assert.equal(headers.Cookie, "token=abc123; foo=bar");
assert.equal(headers.Authorization, "Bearer abc123");
const parsedBody = JSON.parse(String(capturedInit?.body));
assert.equal(parsedBody.model, "glm-4.6");
assert.equal(parsedBody.stream, true);
assert.deepEqual(parsedBody.messages, [{ role: "user", content: "hello" }]);
assert.equal(parsedBody.features.web_search, false);
} finally {
globalThis.fetch = originalFetch;
}
});
it("aggregates streamed internal-envelope deltas into a non-streaming completion", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
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" } }
)) as typeof fetch;
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "glm-4.6",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "token=abc123" },
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 = globalThis.fetch;
globalThis.fetch = (async () =>
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" } }
)) as typeof fetch;
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "glm-4.6",
body: { messages: [{ role: "user", content: "hi" }] },
stream: true,
credentials: { apiKey: "token=abc123" },
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 = globalThis.fetch;
globalThis.fetch = (async () =>
new Response("session expired", { status: 401 })) as typeof fetch;
try {
const executor = new mod.ZaiWebExecutor();
const result = await executor.execute({
model: "glm-4.6",
body: { messages: [{ role: "user", content: "hi" }] },
stream: false,
credentials: { apiKey: "token=abc123" },
signal: null,
});
assert.equal(result.response.status, 401);
} finally {
globalThis.fetch = originalFetch;
}
});
});