Skip to content

feat(provider): add Z.ai GLM as a BYOK provider#170

Merged
NotASithLord merged 1 commit into
mainfrom
feat/glm-zai-provider
Jul 8, 2026
Merged

feat(provider): add Z.ai GLM as a BYOK provider#170
NotASithLord merged 1 commit into
mainfrom
feat/glm-zai-provider

Conversation

@fpolica91

Copy link
Copy Markdown
Collaborator

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/completions shape, same SSE streaming, same tools/tool_choice function-calling. The only deltas are the base URL (https://api.z.ai/api/paas/v4 — note /v4, not /v1), Bearer auth on an id.secret-shaped key, and bare model ids (glm-5.2, not vendor/model). So the adapter reuses the existing to-openai.js / from-openai.js converters — it's a thin sibling of the OpenRouter adapter, minus the gateway-specific bits.

Changes

Area File Change
Adapter peerd-provider/adapters/glm.js (new) endpoint, default glm-5.2, runner glm-4.5-air, secret glm_api_key; reuses connect-timeout + error-classify + retry
Registry peerd-provider/registry.js, index.js register + export
Cost meter peerd-provider/pricing.js glm-5.2 / glm-4.6 rate card ($1.4/$4.4, cached $0.26)
Trim trigger peerd-provider/context-window.js glm-5.2 = 1M, glm-4.6 = 200K, glm-4.5-air = 128K
Egress peerd-egress/fetch/allowlist.js add https://api.z.ai (MV3 CSP already permits all https:)
Picker background/model-catalog.js glm catalog entries
Settings UI options/sections/providers.js Z.ai logo card + fallback row + copy; neutral key placeholder for the id.secret key shape
Test tests/peerd-provider/glm-retry.test.ts (new) retry-set + endpoint/auth/model coverage
Gate packaging/check-tscheck.ts bump // @ts-check floor 496 → 497

The 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)

  • Thinking mode — GLM surfaces reasoning via delta.reasoning_content (gated by extra_body.thinking.type). Surfacing it belongs in the shared from-openai.js parser (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.
  • GLM-4.5-Air pricing — left out of the rate card rather than guessed; an unknown id degrades to an honest "estimate unavailable" the user can override. GLM-5.2/4.6 (the flagship tier) are priced.

Verification

  • bun run typecheck — clean
  • bun run lint — clean
  • bun run check:tscheck — 497/497 (floor bumped)
  • bun run check:boundary — clean
  • bun test ./tests2663 pass, 0 fail (incl. the 10 new callGlm tests)

🤖 Generated with Claude Code

@fpolica91 fpolica91 requested a review from NotASithLord as a code owner July 6, 2026 00:42

@NotASithLord NotASithLord left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@NotASithLord NotASithLord left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@NotASithLord NotASithLord merged commit bb9f5e1 into main Jul 8, 2026
12 checks passed
@NotASithLord NotASithLord deleted the feat/glm-zai-provider branch July 8, 2026 02:36
NotASithLord pushed a commit that referenced this pull request Jul 8, 2026
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
NotASithLord added a commit that referenced this pull request Jul 8, 2026
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>
@NotASithLord NotASithLord mentioned this pull request Jul 12, 2026
NotASithLord added a commit that referenced this pull request Jul 12, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants