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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

- Make ZCode visible in the **Provider usage** panel: the panel federates over
the `provider-usage.v1` RPC contract that only builtin provider plugins
implemented, so the quota windows shipped in the previous change were
unreachable from it. The plugin now registers a discoverable usage source
(host-scoped resources, panel window grammar, 60 s measurement cache with
refresh bypass) whose readings flow through the daemon's `provider/usage`
path. Disconnected hosts and failed collections report error usage states
instead of blocking the panel.
- 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
Expand Down
28 changes: 26 additions & 2 deletions knowledge/contracts/quota-usage-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,30 @@ own credential source.
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
2. **"Provider usage" panel federation** (the panel never calls
`provider/usage` itself): bb's builtin provider-usage plugin discovers
usage sources server-side via `experimental_discoverRpc` of
`provider-usage.v1.listResources` and reads each with
`provider-usage.v1.getResource`. Only plugins that register that contract
with `experimental_discoverable: true` appear; the builtin provider
plugins (codex, claude-code, acp) implement it, and `providers.list({
capability: "usage" })` (fed by the manifest's `maintenance.usage`) is
display metadata, not a data source. Resources are
`{accountKey: null, id: JSON([hostId, providerId]), providerId, label,
scope: {kind: "host", hostId, hostName}}`; windows carry the panel grammar
(`kind: five-hour|daily|weekly|custom`, stable `id`, `model`, `cost`);
measurements flow server → `system.usageLimits({hostId, providerId,
refresh})` → daemon `provider/usage` → the bridge quota fetch. The panel's
plugin caches per resource and invalidates on `thread.idle`/`thread.failed`.
Implemented in `src/usageSource.ts` (2026-11-09) after the first quota PR
shipped windows that the panel could not see.
3. `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
4. 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.

Expand All @@ -97,3 +114,10 @@ own credential source.
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.
- `src/usageSource.ts` registers the panel-facing `provider-usage.v1` source
(contract in `src/usageContract.ts`, wired from `server.ts`): host-scoped
resources for the zcode provider, window-grammar mapping by label, a 60 s
observedAt cache with `refresh` bypass, and error usage states for
disconnected hosts and failed collections. `src/usageSource.ts` takes
injected deps; `tests/usage-source.test.ts` covers listing, mapping,
caching, and degradation.
14 changes: 14 additions & 0 deletions knowledge/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

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

- **Found** why the quota windows were invisible in the Provider usage panel
after the first merge: the panel never calls `provider/usage`. It federates
over the `provider-usage.v1` RPC contract that only builtin provider
plugins (codex, claude-code, acp) implemented server-side; a provider
bridge answering `provider/usage` alone is not a panel source, and
`maintenance.usage`/`providers.list({capability:"usage"})` is display
metadata only. The production worker was verified end-to-end first (spawned
exactly as the daemon does; it returned GLM Coding Max with all three
windows), isolating the gap to the missing federation registration.
Implemented the source in `src/usageSource.ts` (discoverable
`provider-usage.v1.listResources`/`getResource`, host-scoped resources,
panel window grammar, 60 s cache with refresh bypass, error states);
federation semantics recorded in
[quota usage protocol](contracts/quota-usage-protocol.md).
- **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
Expand Down
8 changes: 8 additions & 0 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
runRowView,
} from "./src/runs.js";
import { workflowRunsRpcContract } from "./src/runsContract.js";
import { registerPluginUsageSource } from "./src/usageSource.js";

export default async function plugin(bb: BbPluginApi) {
const settings = bb.settings.define({
Expand Down Expand Up @@ -287,4 +288,11 @@ export default async function plugin(bb: BbPluginApi) {
"ZCode dynamic-workflow run history for this workspace (sessionless cold read).",
},
);

// The "Provider usage" panel federates over the provider-usage.v1 RPC
// contract; without a discoverable source it lists nothing even though the
// bridge answers provider/usage. Resources are the zcode provider on every
// connected host; measurements flow through the daemon's provider/usage
// path (the bridge's quota fetch) via system.usageLimits.
registerPluginUsageSource(bb);
}
132 changes: 132 additions & 0 deletions src/usageContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { z } from "zod";

/**
* The wire contract of bb's federated "Provider usage" panel. The panel
* discovers usage sources through experimental RPC discovery of
* `provider-usage.v1.listResources` and reads each resource with
* `provider-usage.v1.getResource`; builtin provider plugins (codex,
* claude-code, acp) implement the same two methods server-side. This is not
* part of the plugin SDK - matching the builtin wire is the integration.
*
* Pure zod: nothing here touches node-only APIs.
*/

const accountKeySchema = z
.string()
.min(1)
.nullable()
.describe(
"Provider-issued quota account identity. Null means unknown; unknown accounts must not be merged.",
);

const usageWindowSchema = z.object({
kind: z.enum(["five-hour", "daily", "weekly", "custom"]),
id: z.string().min(1),
label: z.string().min(1),
usedPercent: z
.number()
.nonnegative()
.describe("Percentage consumed; may exceed 100 for overage."),
resetsAt: z
.string()
.nullable()
.describe("ISO timestamp, or null when no reset is known."),
model: z
.string()
.nullable()
.describe("Applicable model family, or null for all models."),
cost: z
.object({
usedUsdCents: z.number().nonnegative(),
limitUsdCents: z.number().positive(),
})
.nullable(),
});

const usageStateSchema = z.discriminatedUnion("status", [
z.object({
status: z.literal("ok"),
accountEmail: z.string().nullable(),
planLabel: z.string().nullable(),
windows: z.array(usageWindowSchema),
}),
z.object({
status: z.literal("not_installed"),
accountEmail: z.string().nullable(),
planLabel: z.string().nullable(),
}),
z.object({
status: z.literal("unauthenticated"),
accountEmail: z.string().nullable(),
planLabel: z.string().nullable(),
}),
z.object({
status: z.literal("expired"),
accountEmail: z.string().nullable(),
planLabel: z.string().nullable(),
}),
z.object({
status: z.literal("error"),
accountEmail: z.string().nullable(),
planLabel: z.string().nullable(),
message: z.string(),
}),
]);

const usageResourceSchema = z.object({
accountKey: accountKeySchema,
id: z
.string()
.min(1)
.describe("Stable resource ID within this source plugin."),
providerId: z.string().min(1),
label: z.string().min(1),
scope: z.discriminatedUnion("kind", [
z.object({ kind: z.literal("shared") }),
z.object({
kind: z.literal("host"),
hostId: z.string().min(1),
hostName: z.string().min(1),
}),
]),
});

export const usageSourceRpcContract = {
"provider-usage.v1.listResources": {
input: z.object({}).strict(),
output: z.object({
label: z
.string()
.min(1)
.optional()
.describe(
"Declares a shared group even when empty. Host-only sources omit it; machine groups use host names.",
),
resources: z.array(usageResourceSchema),
}),
},
"provider-usage.v1.getResource": {
input: z
.object({
resourceId: z.string().min(1),
refresh: z
.boolean()
.describe(
"False permits a cached measurement but still returns actual usage. True requests a fresh collection attempt for this resource only.",
),
})
.strict(),
output: z.object({
accountKey: accountKeySchema,
observedAt: z
.number()
.int()
.nonnegative()
.nullable()
.describe(
"Last successful measurement time in epoch milliseconds; null if never observed.",
),
usage: usageStateSchema,
}),
},
} as const;
Loading
Loading