feat(provider): add Z.ai GLM as a BYOK provider#170
Conversation
NotASithLord
left a comment
There was a problem hiding this comment.
Adversarial review pass on the diff + CI.
Implementation: sound, and low risk. The adapter is a faithful, thin mirror of openrouter.js reusing to-openai.js/from-openai.js, the shared retry/backoff/connect-timeout plumbing, and the registry/DI contract (no direct peerd-egress import — safeFetch/getSecret injected as usual). The Authorization: Bearer key attaches at fetch-header time and never touches the request body — verified directly in glm.js, matches the stated BYOK posture. Pricing and context-window entries are honestly scoped (unpriced tiers correctly degrade to "estimate unavailable" rather than guessed numbers). tests/peerd-provider/glm-retry.test.ts covers the retry set (429/500/503/529 recover, 400/401/403/404 fail fast), endpoint/auth/model defaults, and stream-to-event shaping. check-tscheck.ts floor bump (497) matches the new // @ts-check file. No manifest change needed — https: CSP already covers the new host, consistent with the documented policy. CI is fully green (12/12 checks, including bun test, lint+boundary+drift, and both packaging targets).
Blocking: the commit is missing the DCO sign-off. 8a59290a has no Signed-off-by trailer — every other recent PR in this repo carries one (git commit -s), and it's a checklist item on essentially every accepted PR here. Please amend (or rebase) to add Signed-off-by: <name> <email> before merge.
Nit (non-blocking): no UI-level test for the new Z.ai provider row in options/sections/providers.js (providerLogo/keyPlaceholder branch), but that matches the test depth of the existing OpenRouter/Ollama rows, so not asking for more than the established bar.
Once the sign-off is added this looks mergeable as-is.
Generated by Claude Code
Z.ai publishes GLM behind an OpenAI-compatible API (api.z.ai/api/paas/v4), so this is a thin adapter over the existing to-openai/from-openai converters — same shape as the OpenRouter adapter, minus the gateway-specific bits (attribution headers, live catalog, live context window). - adapters/glm.js: endpoint https://api.z.ai/api/paas/v4/chat/completions, default model glm-5.2, web-actor runner glm-4.5-air, vault secret glm_api_key. Reuses the connect-timeout + error-classify + retry plumbing. - registry/index: register + export the adapter. The chassis (provider status, setKey, test, model picker, settings validation, runner model, failover) all read the registry generically, so no per-route wiring. - pricing/context-window: glm-5.2 rate card ($1.4/$4.4, cached $0.26) and the 1M / 200K / 128K windows for 5.2 / 4.6 / 4.5-air. - allowlist: add https://api.z.ai (the MV3 CSP already permits all https). - options UI: a Z.ai logo card + fallback row + copy; GLM keys (id.secret) get a neutral placeholder rather than a misleading sk- hint. - model-catalog: the glm-* picker entries. - test: callGlm retry-set + endpoint/auth/model coverage, mirroring the OpenRouter test. Thinking mode (delta.reasoning_content) is intentionally not wired here — it belongs in the shared from-openai parser, not this adapter, and GLM-5.2 tool- calls fine without it. Future work. Co-Authored-By: Claude <noreply@anthropic.com>
8a59290 to
a268c1e
Compare
NotASithLord
left a comment
There was a problem hiding this comment.
Rebased onto current main (now including the OpenAI adapter from #—/main and the #119/#187/#189 merges): kept both adapters side by side in the registry, pricing, model catalog, and settings rows; re-derived the tscheck floor as 510 → 511 with a ledger line for glm.js. Full local battery green on the rebase: typecheck, lint, tscheck 511/511, boundary, imports, docpaths, packaged page boot, 2822 bun tests, no gen drift. CI 12/12 green. The DCO block is dropped — the checkbox is being removed from the PR template (#190).
Generated by Claude Code
A skeptical, adversarially-verified review of the four just-merged PRs surfaced a set of correctness, integration, doc, and test-hygiene defects. Each fix below survived a 2–3 vote adversarial verification pass. Correctness - manifests.js: the `research` / `browse-only` presets allowed a spill producer (fetch_url, read_page mode:'content') but omitted read_web_cache, so a spilled body's trusted paging footer instructed a tool the manifest gate then refused — the spilled tail unreachable, turns burned on guaranteed-refused calls. Add read_web_cache to both presets (read-only, local cache, no new authority). - read-page.js: mode:'content' clipped the raw DOM at 2MB but reported truncated:false, silently dropping the tail. Surface an explicit htmlTruncated signal (distinct from the paging `truncated` flag). - read-web-cache.js: the evicted-entry hint said "re-fetch the URL", which for a read_page spill steers the actor to fetch_url a page it already rendered (losing the post-JS DOM). Make the recovery hint source-aware. Provider integration (#170 rebase + adjacent) - error-classify.js: Z.ai signals out-of-credit as "insufficient balance … recharge" on HTTP 429; the needles missed it, so a spent GLM account got 3 pointless retries and no failover. Add the needles (+ regression test). - pricing.js: glm-4.6 was priced identically to the glm-5.2 flagship ($1.4/$4.4), a ~3x overestimate that inflated cost and tripped spend limits early. Correct to its published tier ($0.43/$1.74) (+ regression test). - openai.js: the stream parser was called without a provider label, so in-stream errors on OpenAI chats were attributed to "openrouter". Pass { provider: 'openai' }. Docs / classification - CLAUDE.md, README.md, peerd-provider/index.js: provider inventory said OpenAI "doesn't exist yet" and omitted OpenAI + Z.ai GLM; both ship. Update (and defer to registry.js per the no-hard-coded-inventories rule). - idb.js: web_extract_cache was labeled "fetched public bytes"; read_page content mode can store a logged-in tab's rendered DOM. Correct the comment. - error-display.js: usage-limit copy hard-coded "Anthropic / OpenRouter billing"; genericize (the error already names the provider dynamically). - docs/benchmarks/2026-07-web-thread.md: reconcile the failure-taxonomy count (categories overlap) and the 6/29 code-arm denominator (one run did not complete scoring). Test hygiene - spawn.test.ts: the "strips EVERY capability" test passed vacuously (the fixture omitted webCache). Complete the fixture, keep the universal claim honest, and add the read_page→webCache grant assertion #189 introduced. - read-page-content.test.ts: pin the grab-failure fail-open path the header claimed but never exercised, plus the new htmlTruncated signal. NOT fixed here (flagged for a deliberate follow-up): unpriced runner models (glm-4.5-air, OpenRouter's default haiku) render $0.00 and bypass the spend limit — the correct fix is a cost-path change (thread `estimated` through the tally; fail-closed on unpriced under a set limit) affecting multiple providers, not an obvious one-liner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8EYHtUpzTX6YGfEXPRPKF
A skeptical, adversarially-verified review of the four just-merged PRs surfaced a set of correctness, integration, doc, and test-hygiene defects. Each fix below survived a 2–3 vote adversarial verification pass. Correctness - manifests.js: the `research` / `browse-only` presets allowed a spill producer (fetch_url, read_page mode:'content') but omitted read_web_cache, so a spilled body's trusted paging footer instructed a tool the manifest gate then refused — the spilled tail unreachable, turns burned on guaranteed-refused calls. Add read_web_cache to both presets (read-only, local cache, no new authority). - read-page.js: mode:'content' clipped the raw DOM at 2MB but reported truncated:false, silently dropping the tail. Surface an explicit htmlTruncated signal (distinct from the paging `truncated` flag). - read-web-cache.js: the evicted-entry hint said "re-fetch the URL", which for a read_page spill steers the actor to fetch_url a page it already rendered (losing the post-JS DOM). Make the recovery hint source-aware. Provider integration (#170 rebase + adjacent) - error-classify.js: Z.ai signals out-of-credit as "insufficient balance … recharge" on HTTP 429; the needles missed it, so a spent GLM account got 3 pointless retries and no failover. Add the needles (+ regression test). - pricing.js: glm-4.6 was priced identically to the glm-5.2 flagship ($1.4/$4.4), a ~3x overestimate that inflated cost and tripped spend limits early. Correct to its published tier ($0.43/$1.74) (+ regression test). - openai.js: the stream parser was called without a provider label, so in-stream errors on OpenAI chats were attributed to "openrouter". Pass { provider: 'openai' }. Docs / classification - CLAUDE.md, README.md, peerd-provider/index.js: provider inventory said OpenAI "doesn't exist yet" and omitted OpenAI + Z.ai GLM; both ship. Update (and defer to registry.js per the no-hard-coded-inventories rule). - idb.js: web_extract_cache was labeled "fetched public bytes"; read_page content mode can store a logged-in tab's rendered DOM. Correct the comment. - error-display.js: usage-limit copy hard-coded "Anthropic / OpenRouter billing"; genericize (the error already names the provider dynamically). - docs/benchmarks/2026-07-web-thread.md: reconcile the failure-taxonomy count (categories overlap) and the 6/29 code-arm denominator (one run did not complete scoring). Test hygiene - spawn.test.ts: the "strips EVERY capability" test passed vacuously (the fixture omitted webCache). Complete the fixture, keep the universal claim honest, and add the read_page→webCache grant assertion #189 introduced. - read-page-content.test.ts: pin the grab-failure fail-open path the header claimed but never exercised, plus the new htmlTruncated signal. NOT fixed here (flagged for a deliberate follow-up): unpriced runner models (glm-4.5-air, OpenRouter's default haiku) render $0.00 and bypass the spend limit — the correct fix is a cost-path change (thread `estimated` through the tally; fail-closed on unpriced under a set limit) affecting multiple providers, not an obvious one-liner. Claude-Session: https://claude.ai/code/session_01L8EYHtUpzTX6YGfEXPRPKF Co-authored-by: Claude <noreply@anthropic.com>
Promotes [Unreleased] to [0.2.7] and bumps the version. The changelog was empty going into this cut, so wrote entries for the 5 user-facing PRs since v0.2.6 (Z.ai GLM as a BYOK provider #170, the fetch_url clean-markdown + spill-and-page pipeline #187, read_page mode:'content' #189, the experimental web-actor code-REPL surface + the actor-cascade stop fix #119, and the post-merge correctness fixes #194), skipping the 3 that are internal-only (a benchmark report doc, a PR-template chore). Grounded in each PR's own description, not invented; zero em dashes. Signed-off-by: Ariel Deschapell <arieldeschapell@Ariels-MacBook-Air.local> Co-authored-by: Ariel Deschapell <arieldeschapell@Ariels-MacBook-Air.local> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
What
Adds Z.ai GLM as a first-class BYOK provider, so you can use GLM-5.2 (and the rest of the GLM lineup) directly from z.ai — not routed through OpenRouter.
Why z.ai is a clean fit
Z.ai's API is OpenAI-compatible (confirmed against docs.z.ai): same
/chat/completionsshape, same SSE streaming, sametools/tool_choicefunction-calling. The only deltas are the base URL (https://api.z.ai/api/paas/v4— note/v4, not/v1),Bearerauth on anid.secret-shaped key, and bare model ids (glm-5.2, notvendor/model). So the adapter reuses the existingto-openai.js/from-openai.jsconverters — it's a thin sibling of the OpenRouter adapter, minus the gateway-specific bits.Changes
peerd-provider/adapters/glm.js(new)glm-5.2, runnerglm-4.5-air, secretglm_api_key; reuses connect-timeout + error-classify + retrypeerd-provider/registry.js,index.jspeerd-provider/pricing.jsglm-5.2/glm-4.6rate card ($1.4/$4.4, cached $0.26)peerd-provider/context-window.jsglm-5.2= 1M,glm-4.6= 200K,glm-4.5-air= 128Kpeerd-egress/fetch/allowlist.jshttps://api.z.ai(MV3 CSP already permits allhttps:)background/model-catalog.jsglmcatalog entriesoptions/sections/providers.jsid.secretkey shapetests/peerd-provider/glm-retry.test.ts(new)packaging/check-tscheck.ts// @ts-checkfloor 496 → 497The chassis (provider status, setKey, test, model picker, settings validation, runner-model resolution, failover) all read the registry descriptor generically, so no per-route wiring was needed — registering the adapter is enough.
Out of scope (future work)
delta.reasoning_content(gated byextra_body.thinking.type). Surfacing it belongs in the sharedfrom-openai.jsparser (which would also benefit other reasoning-model providers), not this adapter. GLM-5.2 tool-calls and streams content fine without it; the field is silently ignored today, same as OpenRouter.Verification
bun run typecheck— cleanbun run lint— cleanbun run check:tscheck— 497/497 (floor bumped)bun run check:boundary— cleanbun test ./tests— 2663 pass, 0 fail (incl. the 10 newcallGlmtests)🤖 Generated with Claude Code