Skip to content

Add Z.ai GLM Coding Plan agent usage collector - #7566

Closed
FelipeMayerDev wants to merge 5 commits into
omacom:quattrofrom
FelipeMayerDev:add-zai-agent-collector
Closed

FelipeMayerDev wants to merge 5 commits into
omacom:quattrofrom
FelipeMayerDev:add-zai-agent-collector

Conversation

@FelipeMayerDev

@FelipeMayerDev FelipeMayerDev commented Aug 20, 2026 •

Copy link
Copy Markdown

What

Adds a zai agent usage collector plus a Z.ai brand mark, so the agents widget gains a Z.ai (GLM Coding Plan) tab. Per the plugin README, adding an agent is adding a bin/omarchy-agent-usage-<agent> collector that prints the record contract; the panel picks it up automatically.

How it works

  • Limits (Coding Plan): queries …/api/monitor/usage/quota/limit and maps the two limit windows — (unit=3, number=5) → Session (5-hour) and (unit=6, number=1) → Weekly (7-day) — into the standard limits[] shape (label, percent 0–1 from percentage/100, resetsAt from nextResetTime). The endpoint returns each window as a CREDIT_LIMIT row (older responses used TOKENS_LIMIT); both are accepted. Plan tier comes from data.level (e.g. "Lite"). Verified live against a real Coding Plan key.
  • Graceful degrade: that endpoint only answers for keys carrying a Coding Plan. A generic pay-as-you-go key gets {"success": false, "msg": "…coding plan…"} on HTTP 200; the collector then reports usageStatusText and shows local stats only, the same way claude degrades without a signed-in CLI.
  • Local stats: scans ~/.pi/agent/sessions and ~/.omp/agent/sessions for assistant messages on the Z.ai provider (glm-* models), mirroring how codex reads pi/omp sessions — tokens by model/day, prompts, sessions, active dates.
  • Credentials: ~/.config/omarchy/agents/zai.json → ZAI_API_KEY/ZHIPU_API_KEY env → ~/.omp/agent/.env / ~/.pi/agent/.env. Both hosts supported: global api.z.ai and China open.bigmodel.cn (platform: "zai" | "zhipu").
  • Brand mark: assets/zai.svg (white, dark surfaces) + assets/zai-light.svg (Display the active keyboard layout in waybar #111, light surfaces), following the codex mark convention; the panel selects the twin by surface luminance.

Contract

Emits the same record shape as the existing collectors (schemaVersion, id, name, ready, hasLocalStats, hasPromptStats, tierLabel, usageStatusText, authHelpText, limits[]{label,percent,resetsAt}, plus the standard stats block). Stdlib-only Python, matching omarchy-agent-usage-codex/fireworks.

Testing

  • Direct run with a real Coding Plan key: tierLabel: "Lite", Session + Weekly limits populated (percent + weekly resetsAt).
  • Direct run with a pay-as-you-go key: limits: [] + status "No GLM Coding Plan on this key", local stats still shown.
  • Through omarchy-agent-usage-update zai (temp OMARCHY_PATH): passes the dispatcher's jq -e validity gate and writes zai.json.
  • Local glm-* stats picked up from pi/omp sessions (models, per-day chart, prompt/session counts).
  • Mark rendered on dark and light surfaces at hero and bar-glyph sizes.

Notes / follow-ups (intentionally out of scope)

  • MCP-monthly quota (TIME_LIMIT) and the account-global model/tool-usage endpoints are omitted — token stats already come from local sessions, which are richer and machine-scoped.

Adds omarchy-agent-usage-zai so the agents widget gains a Z.ai tab.

Rate limits and plan tier come from Z.ai's coding-plan monitor endpoint
(.../api/monitor/usage/quota/limit), mapping the rolling 5-hour and weekly
TOKENS_LIMIT windows. Keys without a Coding Plan get "no coding plan" and the
record falls back to local stats alone, the same way the claude collector
degrades without a signed-in CLI.

Local stats come from pi/omp sessions that ran through the Z.ai provider
(glm-* models), mirroring how the codex collector reads pi/omp sessions.
Credentials are discovered from ~/.config/omarchy/agents/zai.json, then
ZAI_API_KEY/ZHIPU_API_KEY, then omp/pi .env files; both the global (api.z.ai)
and China (open.bigmodel.cn) hosts are supported.
Copilot AI balanced review requested due to automatic review settings August 20, 2026 02:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review any files in this pull request.


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

The coding-plan monitor endpoint reports each 5-hour/weekly window as a CREDIT_LIMIT row, not TOKENS_LIMIT. Accept both types so real Coding Plan keys surface their Session and Weekly limits instead of an empty list.
assets/zai.svg (white) for dark surfaces and assets/zai-light.svg (omacom#111) for light surfaces, following the codex mark convention; the panel picks the twin by surface luminance.
Copilot AI review requested due to automatic review settings August 20, 2026 03:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review any files in this pull request.

@FelipeMayerDev
FelipeMayerDev requested a balanced review from Copilot August 21, 2026 14:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot wasn't able to review any files in this pull request.

@JoseEchave

Copy link
Copy Markdown

Thanks for this — the live quota endpoint work is the hard part, and I can confirm the local half works on a real machine: I ran scan_sessions() against this machine's pi sessions (162 zai assistant messages, glm-5.3, 8 session files) and it produced a clean record: today 111 prompts / 4.3M tokens, 4 active days, correct model breakdown.

Great foundation. A few things I'd want to land before merge, roughly in priority order — the first two I can demonstrate with real data:

Blocking

1. --limits-only is a no-op, but the panel calls it on every open

Panel.qml fires usage.refreshLimits() in onOpenedChanged — so every panel open runs the dispatcher with --limits-only against every collector. claude honors it (15-minute scan reuse + ≥15s probe interval); codex too (its flag doc: "only fresh limits, so it may reuse a scan for up to 15 minutes"). This collector re-walks every session file and re-probes Z.ai on each open — panel-opened-a-few-times becomes probe spam, which is exactly why Anthropic's probe has PROBE_MIN_INTERVAL_SECONDS. Porting the claude pattern (probe cache with fetchedAtMs, interval, keep-last-known limits while the window hasn't reset) would also cover the missing retryAdvised (below).

2. Reasoning tokens are dropped

pi reports usage.reasoning separately from usage.output; both opencode scans in the stock collectors add it back ("thinking tokens out of output; both are generated"). On this machine: 79,402 reasoning tokens today (~2% of the day's total, more on thinking-heavy sessions). One line:

out = number(usage.get("output")) + number(usage.get("reasoning"))

3. Dead timestamp fallback silently lands usage on "today"

local_day(entry.get("timestamp") or message.get("ts")) — pi's message-level field is timestamp (ms), not ts; ts is None in every real session I checked. So the fallback only ever fires when entry.timestamp is missing, and then local_day(None) returns today, quietly inflating today's numbers. scan_pi_sessions in the codex collector reads entry.get("timestamp") or message.get("timestamp").

4. No test

Every sibling collector has a test/shell.d/agent-usage-*-scanner-test.sh with fixture sessions under a fake HOME — a fixture pi tree with one zai and one non-zai session would have caught #2 and #3.

Should-fix

  • No retryAdvised on transport failure. Main.qml retries advising agents after 30s; without it, boot-before-network shows "Could not reach the Z.ai monitor endpoint" for the full refreshIntervalSec (default 15 min).
  • authHelpText is wrong when a key exists. 401/403/HTTP 500/timeout all set "Set ZAI_API_KEY…" — misleading when the key was found (via config or pi's .env) and the problem is the endpoint.
  • Substring provider match. "zai" in provider.lower() contradicts the exact-match discipline the stock collectors are explicit about (anthropic, openai-codex — a custom zai-proxy gateway is not this subscription).
  • One malformed line loses all stats. entry.get("message") or {} keeps truthy non-dicts → AttributeError → main's catch-all → "Z.ai unavailable" with every local number gone. Same trap in (body or {}).get("msg") for an array body. The stock scans guard with isinstance plus per-line try.
  • iso_from_ms assumes milliseconds; normalize_reset_at in claude accepts either (ts < 1e12 → *= 1000). And an unparseable percentage currently yields a 0% meter — skipping the row is safer.

Nits

  • Hero reads "GLM Coding Plan · Lite"; "Z.ai" as the name would match how the brand sits beside Claude Code / Codex / Fireworks (the SVG title already says Z.ai).
  • Missing from this PR: the collector row in shell/plugins/agents/README.md's table and the providers default in manifest.json.
  • The .env parser misses export KEY=... lines.
  • The hard-coded (unit, number) window map silently drops any future window; an unknown pair is worth at least a stderr note.

Follow-up idea (not this PR)

GLM's Anthropic-compatible endpoint used through Claude Code writes glm-* transcripts into ~/.claude/projects, which the claude collector currently attributes to the Claude tab (this machine has exactly that). Claiming glm-* models there for this tab would fix the attribution, but it crosses collector ownership, so probably maintainers' call whether that belongs here.

@JoseEchave

Copy link
Copy Markdown

Follow-up on the limits side, in case it's useful: Z.ai publishes the official credit formula at https://docs.z.ai/devpack/overview#usage-instruction, and it's computable entirely from the local session tokens this collector already scans — so the meters could say 1,638 of 2,000 credits instead of just a percentage, and there'd be a local fallback (estimated) even on keys where the monitor endpoint answers "no coding plan".

From the docs:

credits = (input × in_mult + cached_input × cached_mult + output × out_mult) / 10,000
  • Multipliers: GLM-5.3 → 6.9 / 1.7 / 24, GLM-5-Turbo → 5.7 / 1.5 / 21, GLM-4.7 → 4.6 / 1.2 / 16 (GLM-5.2/5.1 route to 5.3 upstream, so they bill at its rates)
  • Off-peak is 50% off; peak = Mon–Fri 14:00–18:00 Singapore (UTC+8)
  • Allowances: Lite 2,000 per 5h / 10,000 weekly, Pro 12,000 / 60,000, Max 28,000 / 140,000
  • pi reports usage.reasoning separately from output; both bill at the output multiplier. Cache writes have no listed rate — billing them as plain input is the conservative reading.

I've been running this locally alongside your scanner (sessions on glm-5.3, all off-peak) and the numbers track: ~1,600 credits against the 2,000 Lite session allowance over one heavy day of use. Happy to share the implementation notes if useful — but even just appending a credits-used value to each limits[] row for the panel to render would make the Lite/Pro/Max tier difference visible at a glance.

- --limits-only/--force reuse a probe cache (15s min interval) and a
  local-stats scan cache; keep last-known limits while the window is open
- fold reasoning tokens into output; date turns by message.timestamp
- exact provider match + isinstance guards so one bad line can't drop stats
- retryAdvised on transport failure; blame the key only on 401/403
- iso_from_ms accepts seconds; skip unparseable percentage; note unmapped windows
- rename tab to Z.ai; README row + manifest providers default; .env export= parsing
- add scanner and limits regression tests
@FelipeMayerDev

Copy link
Copy Markdown
Author

Thanks @JoseEchave — all addressed in acffe0c:

  • B1 --limits-only/--force now reuse a probe cache (15s min interval, last-known limits kept until the window resets) plus a local-stats scan cache, ported from the claude/codex pattern.
  • B2 reasoning folded into output. B3 dating fixed to message.timestamp (the ts fallback was dead). B4 added agent-usage-zai-scanner-test.sh (covers Why not use the 1Password AUR? #2/Archinstall configuration is lacking #3) and agent-usage-zai-limits-test.sh (probe cache, retry, auth text).
  • Should-fix: retryAdvised on transport failure; authHelpText blames the key only on 401/403; exact provider match; isinstance guards so one bad line can't drop all stats; iso_from_ms accepts seconds; unparseable percentage rows skipped.
  • Nits: tab renamed to Z.ai; README row + manifest.json providers default; .env export KEY= parsed; unmapped (unit, number) windows log to stderr.

Success path verified live against a real Coding Plan key (tier + both windows).

@FelipeMayerDev

Copy link
Copy Markdown
Author

On the credit formula: clean follow-up, leaving it out of this PR as you flagged. The limits[] shape has room to append a credits value per row when we add it — same for the estimated local fallback on no-plan keys.

@FelipeMayerDev

Copy link
Copy Markdown
Author

@JoseEchave — went through your review point by point against the current head (acffe0c) and re-ran both suites locally to back the summary above with evidence:

Blocking

  1. --limits-only — probe cache (fetchedAtMs + 15s min interval); last-known limits stay up while their window hasn't reset; the flag reuses the scan for 15 min (20s on bare runs). Covered by agent-usage-zai-limits-test.sh: probe reuse, --force re-probe, transport retry, and 401-only auth blame.
  2. Reasoning tokens — folded into output (output + reasoning), same as the opencode scans; the scanner test asserts it on a fixture turn.
  3. Timestamp — entry.timestamp or message.timestamp; the dead ts fallback is gone, and the scanner test covers the dating.
  4. Tests — both suites green (14/14 assertions; the unmapped limit window stderr line in the run is nit Nvidia Support #4's note doing its job on a fixture row).

Should-fix / nits — all in the same commit: retryAdvised on transport failures; authHelpText blames the key only on 401/403; exact provider match; per-line isinstance guards so one bad line can't drop the stats; iso_from_ms accepts seconds-epoch; unparseable percentage rows dropped; collector/tab renamed Z.ai; README row + manifest.json providers default; export KEY= parsing; stderr note on unmapped windows.

Your reasoning-token count (79,402 that day) now lands in the model totals — happy to re-run the scanner against your fixture if anything looks off on your machine.

@FelipeMayerDev

Copy link
Copy Markdown
Author

Updated the PR with the security hardening already reviewed through the standalone plugin marketplace submission (78cb045):

  • safe no-follow cache locks with owner/type/mode checks; invalid lock paths degrade to unlocked execution
  • serialized quota probes with cache freshness recheck
  • bounded 1 MiB monitor responses and capped external status/tier strings
  • regression coverage proving a planted lock symlink is not followed or truncated

Focused Z.ai suites are green: 15 assertions across limits/cache and session scanning. The branch remains mergeable.

@dhh @ryanrhughes when you have a chance, could you take another look?

@dhh

dhh commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Thank you! Brought in with your authorship in #14039, with kept limits marked stale after a failed check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants