diff --git a/changelog.d/fixes/8014-zai-web-auth.md b/changelog.d/fixes/8014-zai-web-auth.md new file mode 100644 index 0000000000..e836c3dacd --- /dev/null +++ b/changelog.d/fixes/8014-zai-web-auth.md @@ -0,0 +1 @@ +- fix(providers): repoint the zai-web executor at chat.z.ai's current v2 chat-completions endpoint, fixing model-independent 404s (#8014) diff --git a/open-sse/executors/zai-web.ts b/open-sse/executors/zai-web.ts index bd61a83ffd..d6a7e81421 100644 --- a/open-sse/executors/zai-web.ts +++ b/open-sse/executors/zai-web.ts @@ -9,7 +9,9 @@ * 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 + * Endpoint: POST https://chat.z.ai/api/v2/chat/completions + * (the older unversioned `/api/chat/completions` path is stale and + * 404s model-independently as of 2026-07 — see #8014) * Auth: full Cookie header from chat.z.ai (must contain the `token` JWT). * Sent both as `Cookie` and as `Authorization: Bearer ` — * the SPA's own fetch client sets both, and stripping either one @@ -29,7 +31,7 @@ import { } from "../utils/error.ts"; const BASE_URL = "https://chat.z.ai"; -const CHAT_URL = `${BASE_URL}/api/chat/completions`; +const CHAT_URL = `${BASE_URL}/api/v2/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"; diff --git a/tests/unit/executor-zai-web.test.ts b/tests/unit/executor-zai-web.test.ts index 2634c73e20..ceb7e4a7b5 100644 --- a/tests/unit/executor-zai-web.test.ts +++ b/tests/unit/executor-zai-web.test.ts @@ -120,7 +120,7 @@ describe("ZaiWebExecutor", () => { signal: null, }); - assert.equal(capturedUrl, "https://chat.z.ai/api/chat/completions"); + assert.equal(capturedUrl, "https://chat.z.ai/api/v2/chat/completions"); const headers = capturedInit?.headers as Record; assert.equal(headers.Cookie, "token=abc123; foo=bar"); assert.equal(headers.Authorization, "Bearer abc123"); diff --git a/tests/unit/zai-web-chat-endpoint-8014-probe.test.ts b/tests/unit/zai-web-chat-endpoint-8014-probe.test.ts new file mode 100644 index 0000000000..82394c4e46 --- /dev/null +++ b/tests/unit/zai-web-chat-endpoint-8014-probe.test.ts @@ -0,0 +1,42 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const mod = await import("../../open-sse/executors/zai-web.ts"); + +test("#8014 RED: ZaiWebExecutor must POST to the current chat.z.ai v2 chat-completions endpoint, not the stale unversioned path", async () => { + const originalFetch = globalThis.fetch; + let capturedUrl = ""; + globalThis.fetch = (async (url: string) => { + capturedUrl = String(url); + if (capturedUrl === "https://chat.z.ai/api/chat/completions") { + return new Response(JSON.stringify({ detail: "Not Found" }), { status: 404 }); + } + return new Response("data: [DONE]\n\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: "hello" }] }, + stream: false, + credentials: { apiKey: "token=abc123" }, + signal: null, + }); + + assert.equal( + capturedUrl, + "https://chat.z.ai/api/v2/chat/completions", + `zai-web executor POSTed to a stale endpoint (${capturedUrl}) — matches #8014's model-independent 404 "Not Found"` + ); + assert.notEqual( + result.response.status, + 404, + "chat call must not 404 when the endpoint path is correct" + ); + } finally { + globalThis.fetch = originalFetch; + } +});