Skip to content

feat: GET /quota — live free-quota billing snapshot (with corrected platform fingerprint) - #41

Merged
TriDefender merged 6 commits into
TriDefender:masterfrom
CDeZT:feat/quota-endpoint-and-jwt-stale-warning
Sep 6, 2026
Merged

TriDefender merged 6 commits into
TriDefender:masterfrom
CDeZT:feat/quota-endpoint-and-jwt-stale-warning

Conversation

@CDeZT

@CDeZT CDeZT commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds GET /quota, a live free-quota snapshot endpoint backed by the ZCode billing control plane.

What changed since the first review round

  • Fixed the platform fingerprint (was platform=undefined-undefined). ProxyIdentity has no platform/arch fields, so the first draft literally sent the string undefined-undefined to the billing gateway and failed typecheck (TS2339 ×2). The endpoint now builds the fingerprint from real values (${process.platform}-${os.arch()}), honouring the existing ZCODE_IDENTITY_PLATFORM / ZCODE_IDENTITY_ARCH env overrides — the same masking pattern proxy/identity.ts uses for the X-Platform header (Android seeds linux-x64 via index.ts). Empty/whitespace overrides fall back to real values. Verified end-to-end against the live gateway.
  • Removed the stale-JWT warning (commit 2). The final diff no longer touches auth/manager.ts. JWT age is informational only: the token has no exp and is not rejected by age — a real 401/3012 from the billing gateway signals re-login, which already surfaces in errors[].
  • Hardening (per adversarial review): balance/plan mapping accepts both snake_case and camelCase aliases (unitType, expiresAt, grantUnits, effectiveAt) since the live upstream sends mixed casing; all numerics coerce through a finite-number helper (no NaN reaches the response); 8 new regression tests for the quota route (previously zero coverage); clarifying comment for the m.id.includes("v") vision heuristic in GET /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 jwt only carries { ageHours, issuedAt } — the previously documented stale: true field no longer exists.

Endpoint behaviour

  • Auth-gated like the other admin endpoints; 503 quota_unavailable when no JWT credential is stored.
  • Queries the upstream billing endpoints with the stored OAuth JWT (Authorization: Bearer <jwt>, no X-ZCode-Agent, forwards X-Device-Mid, User-Agent: ZCode/3.11.2):
    • GET https://zcode.z.ai/api/v1/zcode-plan/billing/balance — verified live; balance entries carry show_name/total_units/used_units/remaining_units/expires_at plus the camelCase unitType/expiresAt snapshot 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).
  • Per-endpoint failures (e.g. 3012 or 401) are tolerated into errors[] with HTTP 200.
  • Per-request it makes 2 upstream calls with no caching — acceptable for a personal/proxy-admin endpoint; not auto-polled.

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.

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[].
@TriDefender

Copy link
Copy Markdown
Owner

OK this is reasonable, will look into this

@TriDefender TriDefender 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.

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.

@CDeZT CDeZT left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pushed fixes for all review items as two commits (280b024 + ddcf817, rebased onto the latest master merge):

Fixes

  • TypeScript errors resolved — ProxyIdentity has no platform/arch fields; the billing fingerprint is now built from real values with the same env-override masking pattern as proxy/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 stale field 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 as null by 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.

@CDeZT CDeZT changed the title feat: /quota billing snapshot endpoint + stale start-plan JWT warning feat: GET /quota — live free-quota billing snapshot (with corrected platform fingerprint) Sep 6, 2026
@TriDefender
TriDefender merged commit c93c532 into TriDefender:master Sep 6, 2026
@CDeZT

CDeZT commented Sep 6, 2026 via email

Copy link
Copy Markdown
Contributor Author

@TriDefender

Copy link
Copy Markdown
Owner

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 ✓, 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 unsubscribehttps://github.com/notifications/unsubscribe-auth/BUIRFYKVRMEMOYWD7MFCOWT5NR7D7AVCNFSNUABGKJSXA33TNF2G64TZHMYTENZRGAYDQMRUGM5US43TOVSTWNJTGU3DSMBYGQ4TBILWAI. You are receiving this because you authored the thread.Message ID: @.***>

No, expat working in africa

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