Repository navigation
feat: GET /quota — live free-quota billing snapshot (with corrected platform fingerprint) - #41
Conversation
GET /quota returns a live free-quota snapshot: billing/balance entries, claimable plan previews, and the stored start-plan JWT age. The billing gateway requires the full desktop identity fingerprint (notably a stable X-Device-Mid), so the config identity is forwarded unchanged. The zcode-plan JWT carries no exp claim but stops working for billing APIs within hours while chat keeps functioning off warm upstream state - a silent degradation that is hard to debug. AuthManager now warns once when the JWT age crosses 6h, pointing at re-login.
Live verification shows the start-plan JWT has no exp and is NOT rejected by age - an 8-day-old token still serves billing/balance (200 code:0). The previous 6h staleness heuristic produced false alarms; only a real 401/3012 from the billing gateway indicates re-login is needed. /quota now reports jwt.ageHours as informational and surfaces gateway rejections via errors[].
|
OK this is reasonable, will look into this |
There was a problem hiding this comment.
Summary
Solid feature — I verified it end-to-end (real node:http server + real fetch against a mock billing gateway, isolated credential store) and the happy path works as described. However, the branch does not type-check, which will fail CI, and there is a runtime fingerprint defect that contradicts the PR's own design goal. Requesting changes for the two blocking items + a description sync.
What I verified empirically ✅
bun test: 722 pass / 0 fail (718 baseline + 4 new — 3jwt-age+ 1 rich-catalog)- e2e probe (mock
billing/balance+billing/previewupstream, assertions on what the gateway actually received):/quotasits behind the proxy-key gate (401 without key) — routing confirmed inserver.ts(/quotais registered after the auth check)- Full snapshot mapping:
serverTime,balances[](all fields incl.unitType/expiresAt),claimablePlans[].entitlements[],jwt.ageHours/issuedAt— all correct - Gateway received: no
X-ZCode-Agent✓,X-Device-Midforwarded ✓,Authorization: Bearer <jwt>✓,User-Agent: ZCode/3.11.2✓ — consistent with theclaim/client.tscontrol-plane precedent - Per-endpoint failure tolerance:
3012(in-body) +401both land inerrors[], response stays HTTP 200 - No stored credential → 503
quota_unavailableenvelope - Plain
GET /v1/modelsshape unchanged (data[], same ids/order as before);?client_version=pireturns the rich catalog — flashcontext_window: 1000000/max_tokens: 128000, efforts[low, medium, high, xhigh], and thexhighlevel is actually servable (normalizeGlm53Effortmapsxhigh→max). Vision detection viaid.includes("v")is correct for all 11 current models.
Blocking issues ❌
1. tsc --noEmit fails — CI will fail
src/server/routes-quota.ts(85,32): error TS2339: Property 'platform' does not exist on type 'ProxyIdentity'.
src/server/routes-quota.ts(85,53): error TS2339: Property 'arch' does not exist on type 'ProxyIdentity'.
ProxyIdentity is { appVersion, sourceTitle, refererOrigin, deviceMid? } — there is no platform/arch on it.
2. Runtime fingerprint defect (proven by probe)
Because those fields don't exist at runtime, the billing gateway receives the literal query param:
platform=undefined-undefined
This deviates from both the desktop client and the claim-client precedent this PR cites (claim/client.ts documents platform as ${process.platform}-${process.arch}). The live test presumably passed only because the gateway ignores the parameter — not because the desktop fingerprint was replicated. Suggested fix, which also honors the ZCODE_IDENTITY_PLATFORM/ZCODE_IDENTITY_ARCH env-override masking pattern from identity.ts (Android relies on it to emit a desktop-Linux profile):
import os from "node:os";
// ...
const platform = `${process.env.ZCODE_IDENTITY_PLATFORM ?? process.platform}-${process.env.ZCODE_IDENTITY_ARCH ?? os.arch()}`;3. CI is stuck in action_required
The fork-PR workflow needs maintainer approval before it runs. Once approved it will fail on item 1 — please fix first, then the run can be approved.
Description sync (should be done before merge)
The PR body still describes commit 1: the 6h AuthManager warning was removed in commit 2 (the final diff doesn't touch auth/manager.ts), and the sample response's "stale": true field no longer exists (jwt is { ageHours, issuedAt } only). Commit 2's direction is right per the live evidence (age is not a rejection signal) — just update the body/title so reviewers and future readers aren't misled.
Minor notes (non-blocking)
input_modalitiesviam.id.includes("v")is fragile but correct for the current catalog — fine to keep; a one-line comment would help future model additions.
…s, finite-number mapping (+8 regression tests)
CDeZT
left a comment
There was a problem hiding this comment.
Pushed fixes for all review items as two commits (280b024 + ddcf817, rebased onto the latest master merge):
Fixes
- TypeScript errors resolved —
ProxyIdentityhas noplatform/archfields; the billing fingerprint is now built from real values with the same env-override masking pattern asproxy/identity.ts(ZCODE_IDENTITY_PLATFORM/ZCODE_IDENTITY_ARCH). - Fingerprint defect fixed — no more literal
platform=undefined-undefined; now emits e.g.win32-x64. Empty/whitespace env overrides fall back to real values (no more-x64/linux-artifacts). - PR description synced — stale-JWT warning and
stalefield removed from the docs; JWT age is informational only (verified live: an 8-day-old JWT still serves billing endpoints; only a real 401/3012 signals re-login).
Hardening (per adversarial review)
- Balance/plan mapping now accepts both snake_case and camelCase aliases (
unitType,expiresAt,grantUnits,effectiveAt) — the live upstream sends mixed casing. - All numerics coerce through a finite-number helper — no
NaN(serialized asnullby JSON) can reach the response. - 8 new regression tests for the quota route (previously zero coverage).
- Clarifying comment added for the
m.id.includes("v")vision heuristic.
Verification
bun test→ 730 pass / 0 fail (722 + 8 new)bun x tsc --noEmit→ 0 errors
CI should proceed once the fork workflow approval is granted. Updated PR body below.
|
bro,do you live in the us?
…________________________________
发件人: TriDefender ***@***.***>
发送时间: 2026年9月6日 5:16
收件人: TriDefender/zcode-api ***@***.***>
抄送: Zilin ***@***.***>; Author ***@***.***>
主题: Re: [TriDefender/zcode-api] feat: /quota billing snapshot endpoint + stale start-plan JWT warning (PR #41)
@TriDefender requested changes on this pull request.
Summary
Solid feature — I verified it end-to-end (real node:http server + real fetch against a mock billing gateway, isolated credential store) and the happy path works as described. However, the branch does not type-check, which will fail CI, and there is a runtime fingerprint defect that contradicts the PR's own design goal. Requesting changes for the two blocking items + a description sync.
What I verified empirically ✅
* bun test: 722 pass / 0 fail (718 baseline + 4 new — 3 jwt-age + 1 rich-catalog)
* e2e probe (mock billing/balance + billing/preview upstream, assertions on what the gateway actually received):
* /quota sits behind the proxy-key gate (401 without key) — routing confirmed in server.ts (/quota is registered after the auth check)
* Full snapshot mapping: serverTime, balances[] (all fields incl. unitType/expiresAt), claimablePlans[].entitlements[], jwt.ageHours/issuedAt — all correct
* Gateway received: no X-ZCode-Agent ✓, X-Device-Mid forwarded ✓, Authorization: Bearer <jwt> ✓, User-Agent: ZCode/3.11.2 ✓ — consistent with the claim/client.ts control-plane precedent
* Per-endpoint failure tolerance: 3012 (in-body) + 401 both land in errors[], response stays HTTP 200
* No stored credential → 503 quota_unavailable envelope
* Plain GET /v1/models shape unchanged (data[], same ids/order as before); ?client_version=pi returns the rich catalog — flash context_window: 1000000 / max_tokens: 128000, efforts [low, medium, high, xhigh], and the xhigh level is actually servable (normalizeGlm53Effort maps xhigh → max). Vision detection via id.includes("v") is correct for all 11 current models.
Blocking issues ❌
1. tsc --noEmit fails — CI will fail
src/server/routes-quota.ts(85,32): error TS2339: Property 'platform' does not exist on type 'ProxyIdentity'.
src/server/routes-quota.ts(85,53): error TS2339: Property 'arch' does not exist on type 'ProxyIdentity'.
ProxyIdentity is { appVersion, sourceTitle, refererOrigin, deviceMid? } — there is no platform/arch on it.
2. Runtime fingerprint defect (proven by probe)
Because those fields don't exist at runtime, the billing gateway receives the literal query param:
platform=undefined-undefined
This deviates from both the desktop client and the claim-client precedent this PR cites (claim/client.ts documents platform as ${process.platform}-${process.arch}). The live test presumably passed only because the gateway ignores the parameter — not because the desktop fingerprint was replicated. Suggested fix, which also honors the ZCODE_IDENTITY_PLATFORM/ZCODE_IDENTITY_ARCH env-override masking pattern from identity.ts (Android relies on it to emit a desktop-Linux profile):
import os from "node:os";
// ...
const platform = `${process.env.ZCODE_IDENTITY_PLATFORM ?? process.platform}-${process.env.ZCODE_IDENTITY_ARCH ?? os.arch()}`;
3. CI is stuck in action_required
The fork-PR workflow needs maintainer approval before it runs. Once approved it will fail on item 1 — please fix first, then the run can be approved.
Description sync (should be done before merge)
The PR body still describes commit 1: the 6h AuthManager warning was removed in commit 2 (the final diff doesn't touch auth/manager.ts), and the sample response's "stale": true field no longer exists (jwt is { ageHours, issuedAt } only). Commit 2's direction is right per the live evidence (age is not a rejection signal) — just update the body/title so reviewers and future readers aren't misled.
Minor notes (non-blocking)
* input_modalities via m.id.includes("v") is fragile but correct for the current catalog — fine to keep; a one-line comment would help future model additions.
* /quota performs 2 upstream calls per request with no caching — fine for self-use; worth remembering if anything ever polls it aggressively.
—
Reply to this email directly, view it on GitHub<#41?email_source=notifications&email_token=BUIRFYKM5O3WRLZC5KTWRET5NR7D7A5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTKMJSGMYDOMBSGE42M4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#pullrequestreview-5123070219>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/BUIRFYKVRMEMOYWD7MFCOWT5NR7D7AVCNFSNUABGKJSXA33TNF2G64TZHMYTENZRGAYDQMRUGM5US43TOVSTWNJTGU3DSMBYGQ4TBILWAI>.
You are receiving this because you authored the thread.Message ID: ***@***.***>
|
No, expat working in africa |
Summary
Adds
GET /quota, a live free-quota snapshot endpoint backed by the ZCode billing control plane.What changed since the first review round
platform=undefined-undefined).ProxyIdentityhas noplatform/archfields, so the first draft literally sent the stringundefined-undefinedto the billing gateway and failed typecheck (TS2339 ×2). The endpoint now builds the fingerprint from real values (${process.platform}-${os.arch()}), honouring the existingZCODE_IDENTITY_PLATFORM/ZCODE_IDENTITY_ARCHenv overrides — the same masking patternproxy/identity.tsuses for theX-Platformheader (Android seedslinux-x64viaindex.ts). Empty/whitespace overrides fall back to real values. Verified end-to-end against the live gateway.auth/manager.ts. JWT age is informational only: the token has noexpand is not rejected by age — a real 401/3012 from the billing gateway signals re-login, which already surfaces inerrors[].unitType,expiresAt,grantUnits,effectiveAt) since the live upstream sends mixed casing; all numerics coerce through a finite-number helper (noNaNreaches the response); 8 new regression tests for the quota route (previously zero coverage); clarifying comment for them.id.includes("v")vision heuristic inGET /v1/models.Response shape
{ "provider": "zai", "serverTime": 1737000000, "jwt": { "ageHours": 5.12, "issuedAt": 1736600000 }, "balances": [ { "showName": "GLM Coding", "remainingUnits": 1200, "totalUnits": 1500, "usedUnits": 300, "unitType": "token", "expiresAt": 1740000000 } ], "claimablePlans": [ { "planId": "glm-coding-plan", "name": "GLM Coding Plan", "entitlements": [ { "showName": "tokens", "grantUnits": 1000, "unitType": "token", "effectiveAt": 1736000000 } ] } ], "errors": [] }Note
jwtonly carries{ ageHours, issuedAt }— the previously documentedstale: truefield no longer exists.Endpoint behaviour
quota_unavailablewhen no JWT credential is stored.Authorization: Bearer <jwt>, noX-ZCode-Agent, forwardsX-Device-Mid,User-Agent: ZCode/3.11.2):GET https://zcode.z.ai/api/v1/zcode-plan/billing/balance— verified live; balance entries carryshow_name/total_units/used_units/remaining_units/expires_atplus the camelCaseunitType/expiresAtsnapshot fields.GET https://zcode.z.ai/api/v1/zcode-plan/billing/preview— used by this implementation to expose claimable plans (same envelope style as balance).errors[]with HTTP 200.Tests
bun test: 730 pass / 0 fail (722 baseline + 4 jwt-age/rich-catalog from the first round + 8 new quota regressions).bun x tsc --noEmit: 0 errors.