Repository navigation
[v3.8.50] Fix Z.ai web browser transport and model capabilities - #8451
diegosouzapw merged 6 commits into
Conversation
|
Thanks for the deep rework here — the localStorage-token flow, the HMAC signature reverse-engineering, the CAPTCHA-aware browser transport, and the Deep Think/vision capability mapping are all clearly the product of real, live testing against chat.z.ai, and the 28 new/updated unit tests hold up (I ran the full suite locally and everything passes). This is exactly the kind of fix we want to land. Before we can merge as-is, CI is currently red on two gates we'll need addressed on your branch:
Separately, since this PR also changes the global rate-limit-queue and model-test timeout defaults (15s→60s, 30s→60s) for every provider, not just zai-web, two of our docs ( Everything else — the executor logic, error sanitization, and test coverage — looks good on our side. We'll take care of confirming the exact ESLint violation and reviewing the file split with you; the docs update we're happy to fold in ourselves if that's easier. Thanks again for the thorough live validation work on this one. |
1130c12 to
9085fdf
Compare
|
@diegosouzapw Thanks for the detailed review — all requested changes have been addressed in commit 9085fdf7e:
Validation after the split: 126 relevant unit tests passed, along with npm run typecheck:core, npm run check:docs-all, the exact ESLint command above, and the pre-commit quality hooks. I also checked the five currently red jobs. Their reported file-size/test failures are in existing base files and tests outside this PR diff; none point to the Z.ai changes. The branch is ready for another review or CI rerun. |
5de8cfe to
c1c0862
Compare
|
Rebased onto What conflicted, and why#8503 merged at 07:56 and touched the two files this branch also rewrites:
|
|
[21:35:52] 📊 [USAGE] ZAI-WEB | in=97 | out=0 | account=4dd836b1... (estimated) |
|
@SamIsHere006 — thanks for the logs, that's a useful failure signature: HTTP 200 with One question decides where this belongs, and I can't answer it from the log you posted: which build are you on? The model name doesn't settle it — What does settle it: does your log contain a request to
If it helps, What I can verify, for whoever picks this upI checked
The body it sends is That is consistent with #8503's own scope note, which said so explicitly rather than claiming otherwise:
So if you are on release, a 404 turning into "200 with an empty stream" after the endpoint was repointed is the shape you would expect, and #8014 would need reopening rather than staying closed as fixed. I'm deliberately not asserting that yet — I have no live z.ai session to confirm causation, and the same symptom could have other causes. If you are on this branch instead, then it is a bug here and the log line above is the place to start. Either way the answer to the |
c1c0862 to
cdc64ee
Compare
|
@SamIsHere006 — I could not reproduce your case directly (no live z.ai session here), but reading the stream path turned up something that explains why your log is so uninformative, and it is worth fixing regardless of which build you are on. Pushed as a commit on this branch. Why
|
298a37b to
76f6f27
Compare
|
Root refresh (2026-07-28): rebased onto release/v3.8.49 at d6c0693 and force-pushed safely with lease. New head: 76f6f277a. Validation on the rebased tree: 91/91 focused tests, core typecheck, dashboard frozen-baseline typecheck, file-size gate, complexity ratchets, changed-file ESLint, and diff check all pass. |
76f6f27 to
3656ff9
Compare
3656ff9 to
a5c9d70
Compare
Heads-up: this PR is running against a stale base
I verified this on a clean checkout of the current tip ( Currently failing here:
Please update the branch (the Update branch button, or rebase onto Note this is not a promise that every red disappears — a failure specific to the changes in this PR will survive the rebase, and No action was taken on this PR beyond this comment. |
a5c9d70 to
ea00f97
Compare
18d5030 to
855bf9f
Compare
|
Review fix pushed (non-streaming error surface). Found during review: the silent-empty fix only covered the streaming path. Fix (single commit, author preserved, maintainer co-author):
Validation: Still needs the VPS browser smoke (real chat.z.ai token + CAPTCHA, Hard Rule #18) before merge. |
…th the chats/new + signed flow Rebasing onto release/v3.8.49 pulled in cheaper-inference#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 cheaper-inference#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.
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 cheaper-inference#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.
…et holds The v3.8.49 merge-train rebaseline (cheaper-inference#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.
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>




Summary
zai-webauthentication around thechat.z.aiLocal Storage token and an authenticated browser transport that obtains the per-request CAPTCHA proof.Root cause
The previous provider path expected a cookie-based credential and stale remote model discovery. It could not reliably complete Z.ai's current CAPTCHA-gated browser flow, coordinate clicks were intercepted by the landing-page animation, multimodal parts were not uploaded as files, and local abort/queue failures could be surfaced with misleading status or success state.
Impact
Users now copy only the
tokenvalue fromchat.z.aiLocal Storage. OmniRoute performs the authenticated page request, model/Deep Think selection, CAPTCHA handling, and image upload. The public model list is curated and no longer imports obsolete entries.Latest rebase and validation
Rebased
agent/zai-web-browser-transportonto:The rebase completed without conflicts.
git range-diffmatched all five original commits one-to-one:Fresh checks on the rebased branch:
npm run typecheck:core: passed.npm run check:dashboard-typecheck: passed; 254 dashboard errors remain within the frozen baseline.npm run lint: passed.npm run check:file-size: passed (121 frozen files; 44 frozen test files).npm run check:complexity-ratchets: passed (complexity=2202vs baseline 2774;cognitiveComplexity=980vs baseline 1223).npm run check:complexity: passed.npm run check:cognitive-complexity: passed.git diff --check upstream/release/v3.8.50...HEAD && git diff --check: passed.Current relevant file sizes:
open-sse/executors/zai-web.tsopen-sse/executors/zai-web/protocol.tsopen-sse/executors/zai-web/stream.tsopen-sse/executors/zai-web/browserAutomation.tsopen-sse/services/browserBackedChat.ts