Provider Usage Query Architecture and JSON Contract #
The usage query core extracts quota, rate limit, and account usage queries into the provider layer (@earendil-works/pi-ai). It provides a language-neutral interface and a standardized JSON contract for CLI and external consumers.
JSON Contract #
Running prime-agent usage --json outputs exactly one JSON object on stdout (with no ANSI codes or supplementary text):
{
"fetchedAt": 1742294400000,
"reports": [
{
"provider": "openai-codex",
"providerName": "ChatGPT Codex",
"accountId": "acct_12345",
"email": "user@example.com",
"planType": "plus",
"limits": [
{
"label": "Primary",
"usedPercent": 12.5,
"windowLabel": "5h window",
"resetsAt": 1742312400000,
"status": "ok"
},
{
"label": "Secondary",
"usedPercent": 40.0,
"windowLabel": "24h window",
"resetsAt": 1742380800000,
"status": "ok"
}
],
"error": null
}
],
"skipped": [
"anthropic"
]
}
Fields #
fetchedAt: Epoch timestamp in milliseconds when the query was executed.reports: Array of usage reports, one per credential entry (supporting multi-account configurations).provider: Provider identifier string (e.g."openai-codex").providerName: Human-readable display name for the provider.accountId: Stored or token-extracted account identifier (nullif unavailable).email: Stored or token-extracted account email address (nullif unavailable).planType: Subscription tier or plan type when reported by the upstream API (nullif not reported).limits: Array of quota and rate limit windows:label: Name of the limit window (e.g."Primary","Secondary","Feature · Primary").usedPercent: Current usage percentage as a number between 0 and 100 (nullif unknown).windowLabel: Duration of the rate window (e.g."5h window","24h window").resetsAt: Epoch timestamp in milliseconds when the limit window resets (nullif unknown).status: One of"ok","warning","exhausted", or"unknown".
error: Error message string if the provider query failed, ornullon success. (Provider errors are captured as data and do not cause non-zero process exits.)
skipped: Array of configured provider IDs whose OAuth provider implementation does not provide a quota feed (deduplicated in stable order).
How to Add a Provider to /usage #
To add a quota/usage feed for a provider:
- Implement the optional
usagemethod on the provider'sOAuthProviderInterfacein its oauth provider file (packages/ai/src/utils/oauth/<provider>.ts):async usage( credentials: OAuthCredentials, options?: { fetchImpl?: FetchLike; signal?: AbortSignal }, ): Promise<UsageReport> { // Fetch quota from provider backend and return a UsageReport } - No provider registry changes are needed:
collectUsagedynamically checks whethergetOAuthProvider(entry.provider)?.usageis defined. Providers without this hook are automatically appended toskipped.
Non-OAuth Providers #
Non-OAuth providers (such as API-key-only models or custom proxies) currently have no stored OAuth accounts in AuthStorage. If a feed is added for API-key providers, it can implement the same UsageReport return shape via an adapter function or provider extension. Currently, /usage queries stored OAuth accounts.
Every provider with a feed implements one: openai-codex, anthropic, google-antigravity,
opencode-go, zai, deepseek, hyper and alibaba. A known provider with no feed reports
that as its reason and appears in skipped instead of looking unlimited.
Native Hosts (Zig) #
A macOS Swift app cannot run this package, so the query exists a second time as a C ABI
library: the usage-core repository, checked out at native/usage/ as a submodule (Zig,
pinned to 0.16.0, built by zig build).
The core is pure there too — usage_advance returns the requests to answer or the finished
report, and usage_supported_providers names the feeds and the credential kinds each accepts
— so the app performs the request with NSURLSession (system trust store, proxies, HTTP/2) and
hands back the response. A request is {"kind":"http",...} to send or {"kind":"command",...}
for the host to run; a host that runs no commands answers status: 127 and the feed routes
around it. Every call returns JSON the caller frees with usage_free; failures are
{"error":"..."} in that JSON, and NULL arguments mean an absent string.
Two implementations of one contract only stay honest if they are compared, so
packages/ai/test/usage-zig-parity.test.ts computes both from the same recorded bodies at
test time (fixtures under native/usage/tests/fixtures/, one captured live from
wham/usage with user_id/account_id/email removed), and zig build test compiles
and links a C program against the installed library to exercise the ABI the way a native
caller does. npm run check runs both.
When the contract changes, change it in both: the typescript parser and its fixture, then the Zig parser and the parity test.