Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

- Show real Coding Plan quota usage in bb's usage panel: `provider/usage` now
reports 5-hour, weekly, and MCP windows with used-percent and reset times
from the Z.ai/bigmodel quota-remaining endpoint (same account service and
short-lived project token the bridge already uses). Results are cached for
60 s so panel refreshes do not respawn a probe; quota failures degrade to
the previous plan-label-only answer instead of inventing bars.
- Support the CLI 0.17.1 automation stop-condition flow: the automation host
answers the new `automation/pause` reverse RPC, scheduled runs keep the
`CronPauseSelf` tool available (pausing their own schedule parks it as
Expand Down
6 changes: 4 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,10 @@ The fallback **ZCode models unavailable** entry appears while the catalog is
unavailable. It does not prove that a provider is authenticated. Likewise,
health checks verify protocol/catalog availability, not model connectivity.
Run `/zcode-check` in an idle session to perform one real connectivity
request; the provider usage panel reports account status and the active
Coding Plan label without invented usage numbers.
request; the provider usage panel reports account status, the active Coding
Plan label, and its live quota windows (5-hour, weekly, MCP) with reset times
from the plan's quota service, cached for a minute. Windows the service does
not report numbers for stay hidden rather than guessed.

## Individual Coding Plan

Expand Down
99 changes: 99 additions & 0 deletions knowledge/contracts/quota-usage-protocol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
type: contract
title: Z.ai Coding Plan quota API and bb usage surfaces
description: Wire contract of the Z.ai/bigmodel quota-remaining endpoint (5-hour/weekly/MCP windows) and its mapping onto bb's provider/usage windows and provider.rateLimits deltas; live-verified on the zai family, bigmodel fixture-only.
status: stable
tags: [contract, usage, quota, coding-plan]
sources:
- title: docs/coding-plan-quota-remaining-api.md at zai-org/ZCode a60ba6a
resource: https://github.com/zai-org/ZCode/blob/a60ba6a/docs/coding-plan-quota-remaining-api.md
- title: packages/services/src/usage-stats/providers/bigmodelUsageQuotaProvider.ts at a60ba6a
resource: https://github.com/zai-org/ZCode/blob/a60ba6a/packages/services/src/usage-stats/providers/bigmodelUsageQuotaProvider.ts
- title: packages/shared/src/usage-quota.ts and zcodeEndpoint.ts at a60ba6a
resource: https://github.com/zai-org/ZCode/blob/a60ba6a/packages/shared/src/usage-quota.ts
- title: SDK 0.6.49 providerUsageWindowSchema / provider.rateLimits delta (bundled-types)
resource: ../../node_modules/@get-bb/plugin-sdk/bundled-types/bb-plugin-sdk-provider-bridge.d.ts
- title: Quota fetch and window mapping implementation
resource: ../../src/coding-plan.ts
- title: provider/usage wiring and cache
resource: ../../src/bridge.ts
generated:
by: zcode-agent/GLM-5.3
at: 2026-11-09T00:00:00Z
verified:
- by: zcode-agent/GLM-5.3 (live probe through CodingPlanHost, GLM Coding Max)
at: 2026-11-09T13:10:00Z
---

# Z.ai Coding Plan quota API and bb usage surfaces

Researched 2026-11-09 against upstream sync `a60ba6a` (CLI 0.17.1) and
implemented the same day. The zai family was exercised live through
`CodingPlanHost.usageWindows()` (GLM Coding Max: 5-hour/weekly/MCP windows
with real reset timestamps); the bigmodel family has fixture coverage only.

## Quota endpoint

```http
GET {origin}/api/monitor/usage/quota/limit
authorization: <Coding Plan API key> (raw key, no Bearer prefix)
```

- `origin` follows the plan family and reuses the same origins as
`src/coding-plan.ts` (`api.z.ai` for `zai`, `bigmodel.cn` for `bigmodel`).
Upstream env overrides: `ZAI_BUSINESS_BASE_URL`, `BIGMODEL_API_BASE_URL`,
and the full-URL `ZCODE_BIGMODEL_USAGE_QUOTA_URL` / `BIGMODEL_USAGE_QUOTA_URL`.
- Envelope: `{code?, msg?, success?, data: {level?, limits[]}}`; the plugin's
existing 0/200-code envelope check applies.
- Each limit: `{type, unit?, number?, usage?, currentValue?, remaining?,
percentage?, nextResetTime?, usageDetails?[{modelCode?, usage?}]}`.
`percentage` is **used** percent (display inverts to remaining);
`nextResetTime` is epoch milliseconds.

Window identity (upstream dashboard matching rules):

| Window | Match |
| -------- | ------------------------------------------------------- |
| 5-hour | `type === "TOKENS_LIMIT" && unit === 3 && number === 5` |
| Weekly | `type === "TOKENS_LIMIT" && unit === 6` |
| MCP time | `type === "TIME_LIMIT" && unit === 5 && number === 1` |

Adjacent endpoints discovered but not wired: `/api/v1/coding-plan/reset`
(5h/weekly reset opportunities, status/use/history - the Desktop
"CodingPlanQuotaReset" feature) and the official-MCP quota provider with its
own credential source.

## bb surfaces

1. `provider/usage` (pull; wired to the quota endpoint since this change):
`status: "ok"` + `accountEmail` + `planLabel` +
`windows: [{label, usedPercent, resetsAt (ISO | null), cost?}]`. The
installed bb 0.46 daemon validates `resetsAt`/windows, so the pipeline is
live. Limits without a finite 0..100 `percentage` are omitted, not invented
(the same honesty rule as stage-2 usage); `resetsAt` drops to `null` when
`nextResetTime` is missing or stale.
2. `provider.rateLimits` thread delta (push; still unused): a
`subscription-window`-kind state with per-window `resetsAtMs` and
`allowed/warning/blocked` status, plus the `rateLimited` provider-recovery
kind. Upstream CLI classifies HTTP 429 as retryable rate-limited with
`retryAfterMs` (`failure-classifier.ts`); a quota-exhausted turn failure is
the natural trigger to push this state.
3. Per-turn `usage` and `contextWindow` deltas are already implemented in
`src/usage.ts`; the contextWindow delta additionally accepts a rich
`snapshot` (categories, `autoCompactAtTokens`) the plugin does not send yet.

## Implemented mapping (`src/coding-plan.ts`, `src/bridge.ts`)

- `quotaWindows(limits, now)` maps in a fixed display order: `5-hour window`
(TOKENS_LIMIT/unit3/number5), `Weekly window` (TOKENS_LIMIT/unit6),
`MCP quota` (TIME_LIMIT/unit5/number1); `usedPercent = percentage`
(already used-percent), `resetsAt = ISO(nextResetTime)` only while it is in
the future.
- `CodingPlanHost.usageWindows()` does one bounded GET with the short-lived
project token via the shared `request()` plumbing and degrades to
`undefined` on any account failure.
- The bridge fetches windows only from the `provider/usage` handler (never
`model/list`/`provider/health`, which share `probe`), behind a single-slot
cache keyed by the exact request payload with a 60 s TTL
(`usageCacheTtlMs` test seam) so panel refreshes neither respawn the CLI
probe nor re-query the account service.
7 changes: 5 additions & 2 deletions knowledge/decisions/feature-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@ back to the generic tool row so errors stay visible; unknown tools are
unchanged. Remaining presentation gaps: full unified-diff views (bb renders
`oldText`/`newText` itself), richer delegation summaries.

## Stage 2 — honest provider maintenance (done in v0.7.0)
## Stage 2 — honest provider maintenance (done in v0.7.0, windows in Unreleased)

`provider/usage` now answers from sessionless discovery: account status plus
the active individual Coding Plan subscription label (captured in
`src/coding-plan.ts`), with empty windows rather than invented numbers.
`src/coding-plan.ts`), originally with empty windows rather than invented
numbers; the Unreleased quota change fills real 5-hour/weekly/MCP windows from
the Z.ai quota-remaining endpoint behind a 60 s cache
([quota usage protocol](../contracts/quota-usage-protocol.md)).
`/zcode-check [providerId/modelId]` runs the native
`provider/testModelConnectivity` on demand - one real model request, no
session prompt - and health guidance points to it.
Expand Down
1 change: 1 addition & 0 deletions knowledge/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ not replace the user-facing [README](../README.md) or [docs/](../docs/).
- [Protocol contracts](contracts/protocol-contracts.md) - The bb bridge, ZCode native protocol, and marketplace catalog contracts, with pinned versions, native model identities and context-budget translation rules.
- [Off-peak protocol](contracts/off-peak-protocol.md) - The undocumented Z.ai idle-time ticket gateway (wire shapes, idle-plan per-run execution, 3102 continuation) and the plugin design derived from it; draft until live-gateway verification.
- [Dynamic workflow protocol](contracts/dynamic-workflow-protocol.md) - POC-verified wire contract for ZCode dynamic workflows on CLI 0.17.0: gate params, tool family, skill gate, approval shape, the `causalityGraph` display payload, the task-notification wake turn, the cross-process `workflows/runs` read API, and the live-status sources (projection background jobs + v4 run journal events); draft - one happy path exercised.
- [Quota usage protocol](contracts/quota-usage-protocol.md) - The Z.ai/bigmodel Coding Plan quota-remaining endpoint (5-hour/weekly/MCP windows, used-percent semantics, nextResetTime) and its mapping onto bb's `provider/usage` windows and `provider.rateLimits` deltas; live-verified on the zai family, bigmodel fixture-only.

## Decisions

Expand Down
20 changes: 20 additions & 0 deletions knowledge/log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Knowledge log

## 2026-11-09 (quota usage research and implementation)

- **Researched** how to show real ZCode usage in bb. The per-turn `usage` and
`contextWindow` deltas already flow; the gaps are `provider/usage` windows
(answered with `windows: []` since v0.7.0) and the untouched
`provider.rateLimits` delta + `rateLimited` recovery kind. Upstream sync
`a60ba6a` documents the Z.ai/bigmodel quota-remaining endpoint
(`GET /api/monitor/usage/quota/limit`, raw Coding Plan key auth, same origins
`src/coding-plan.ts` already uses) with 5-hour/weekly/MCP windows
(used-percent + `nextResetTime`), plus a 5h/weekly reset-opportunity API.
- **Implemented** the pull side: `CodingPlanHost.usageWindows()` fetches the
quota endpoint with the short-lived project token, `quotaWindows()` maps the
three windows in a fixed order (finite 0..100 percentage required, future
`nextResetTime` only), and `provider/usage` serves them behind a 60 s
single-slot cache so panel refreshes do not respawn the sessionless probe;
quota failures degrade to the previous plan-label-only answer. Verified live
through the real Desktop sign-in (GLM Coding Max: 5-hour 75%, weekly 19%,
MCP 1%, real reset timestamps); bigmodel covered by fixtures. Wire contract:
[quota usage protocol](contracts/quota-usage-protocol.md).

## 2026-11-01 (crash self-healing)

- **Fixed** threads stranded by an unexpected ZCode process exit. `session.ended`
Expand Down
54 changes: 44 additions & 10 deletions src/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ import {
type Snapshot,
} from "./protocol.js";
import { Timeline, toolPresentation } from "./timeline.js";
import { CodingPlanHost, type AccountHost } from "./coding-plan.js";
import {
CodingPlanHost,
type AccountHost,
type UsageWindow,
} from "./coding-plan.js";
import { nativeInput } from "./input.js";
import { Usage } from "./usage.js";
import { runtimeSettings } from "./settings.js";
Expand Down Expand Up @@ -404,6 +408,8 @@ export function createBridge(
automationTickMs?: number;
/** Test seam: replace the off-peak gateway client entirely. */
offPeakClientFactory?: () => OffPeakServerClient;
/** Test seam: provider/usage window cache lifetime (default 60 s). */
usageCacheTtlMs?: number;
} = {},
) {
const write =
Expand All @@ -418,6 +424,13 @@ export function createBridge(
const sessions = new Map<string, Session>();
const constructing = new Set<string>();
const startingClients = new Map<string, ZCodeConnection>();
// Single-slot cache for provider/usage: one supported plan per installation,
// keyed by the exact request payload. Quota lookups ride the heavyweight
// sessionless probe, so panel refreshes must not respawn a CLI child or
// re-query the account service on every call.
const usageCacheTtlMs = config.usageCacheTtlMs ?? 60_000;
let usageCache:
{ key: string; at: number; usage: Record<string, unknown> } | undefined;
// Most recent native session identity per thread, surviving session
// release: cold dispatch compares a task's bound session against it to
// detect threads that were rebound (fork/rewind) after the task was
Expand Down Expand Up @@ -2195,7 +2208,11 @@ export function createBridge(
}
}

async function probe(raw: Message, cwd = homedir()) {
async function probe(
raw: Message,
cwd = homedir(),
options: { quota?: boolean } = {},
) {
const launch = resolveLaunch(
raw.providerOptions,
config.env ?? process.env,
Expand Down Expand Up @@ -2250,7 +2267,10 @@ export function createBridge(
)
)
throw new Error(accountError);
return { snapshot, planLabel: account.planLabel ?? null };
const windows: UsageWindow[] = options.quota
? ((await account.usageWindows?.()) ?? [])
: [];
return { snapshot, planLabel: account.planLabel ?? null, windows };
} finally {
account.close();
await client.close();
Expand Down Expand Up @@ -2416,20 +2436,34 @@ export function createBridge(
}
if (method === "provider/usage") {
const p = record(parsed.data);
const key = JSON.stringify(parsed.data);
if (
usageCache &&
usageCache.key === key &&
usageCache.at + usageCacheTtlMs > Date.now()
) {
result(id, { supported: true, usage: usageCache.usage });
return;
}
try {
const { snapshot, planLabel } = await probe(
const { snapshot, planLabel, windows } = await probe(
p,
text(p.cwd) || homedir(),
{ quota: true },
);
const available = snapshot.settings.model.available.filter(
(model) => !model.disabledReason,
);
result(id, {
supported: true,
usage: available.length
? { status: "ok", accountEmail: null, planLabel, windows: [] }
: { status: "unauthenticated" },
});
const usage = available.length
? { status: "ok", accountEmail: null, planLabel, windows }
: { status: "unauthenticated" };
if (available.length)
usageCache = {
key,
at: Date.now(),
usage: usage as Record<string, unknown>,
};
result(id, { supported: true, usage });
} catch (failure) {
result(id, {
supported: true,
Expand Down
83 changes: 83 additions & 0 deletions src/coding-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,65 @@ export interface AccountHost {
close(): void;
/** Label of the active subscription, when an account host verified one. */
readonly planLabel?: string | null;
/** Quota windows for usage display, when the account can report them. */
usageWindows?(): Promise<UsageWindow[] | undefined>;
}

/** One bb usage window derived from a native quota limit. */
export interface UsageWindow {
label: string;
usedPercent: number;
resetsAt: string | null;
}

// Upstream dashboard matching rules (docs/coding-plan-quota-remaining-api.md):
// TOKENS_LIMIT/unit 3/number 5 is the 5-hour window, TOKENS_LIMIT/unit 6 the
// weekly window, TIME_LIMIT/unit 5/number 1 the MCP time window. The order
// fixes the panel display order independently of the response array.
const quotaWindowRules: readonly {
label: string;
type: string;
unit: number;
number: number | null;
}[] = [
{ label: "5-hour window", type: "TOKENS_LIMIT", unit: 3, number: 5 },
{ label: "Weekly window", type: "TOKENS_LIMIT", unit: 6, number: null },
{ label: "MCP quota", type: "TIME_LIMIT", unit: 5, number: 1 },
];

const quotaNumber = (value: unknown): number | undefined =>
typeof value === "number" && Number.isFinite(value) ? value : undefined;

/**
* Maps native quota-remaining limits onto bb usage windows. The native
* `percentage` is already used-percent; limits a rule cannot match with a
* finite 0..100 percentage are skipped rather than guessed.
*/
export function quotaWindows(limits: unknown, now: number): UsageWindow[] {
const rows = (Array.isArray(limits) ? limits : []).map(record);
return quotaWindowRules
.map((rule) => {
const limit = rows.find(
(row) =>
text(row.type) === rule.type &&
row.unit === rule.unit &&
(rule.number === null || row.number === rule.number) &&
(quotaNumber(row.percentage) ?? -1) >= 0 &&
(quotaNumber(row.percentage) ?? 101) <= 100,
);
const usedPercent = quotaNumber(limit?.percentage);
if (!limit || usedPercent === undefined) return undefined;
const nextReset = quotaNumber(limit.nextResetTime);
return {
label: rule.label,
usedPercent,
resetsAt:
nextReset !== undefined && nextReset > now
? new Date(nextReset).toISOString()
: null,
};
})
.filter((window): window is UsageWindow => window !== undefined);
}

/** Implements ZCode's desktop-host contract for individual Coding Plan only. */
Expand Down Expand Up @@ -179,6 +238,30 @@ export class CodingPlanHost implements AccountHost {
};
}

/**
* Quota windows for bb's usage panel: one bounded GET against the same
* account service, authorized by the short-lived project token (the key
* upstream's usage dashboard uses). Degrades to undefined so the panel
* keeps the plan label without invented bars.
*/
async usageWindows(): Promise<UsageWindow[] | undefined> {
if (!this.login) return undefined;
try {
const material = await this.getMaterial();
const data = record(
await this.request(
"/api/monitor/usage/quota/limit",
material.token,
undefined,
true,
),
);
return quotaWindows(data.limits, this.now());
} catch {
return undefined;
}
}

private async request(
path: string,
authorization: string,
Expand Down
Loading
Loading