docs: stand as its own project
The core moved out of prime-agent's native/usage into its own repository: same toolchain, same fixtures, same ABI. The README now describes this repo on its own terms, records how the TypeScript reference and the parity suite stay in prime-agent, and replaces the stale status with what the eight providers actually answer from (including the `cli` credential kind and the command request the host runs). History is preserved: the commits below are the core's own, extracted with `git subtree split --prefix=native/usage`.
123456789# zig build output.zig-cache/zig-out/
# swift build outputswift/.build/*.xcodeproj
.DS_Store1123453478956789101112131415131617181920212223182425202126272829302631323334352829303136373839403541424344451 unmodified line4748495045464751525354495556575853545960615657625963646566626364656667686869707172707374757677787980737475818283787980818283848586848586878889888991929091929394959697989910010110210310410510610796979899100101102108109110111112113114115116# usage (Zig)# usage-core
The usage query core, for hosts that cannot run the TypeScript implementation. This isthe same query `prime-agent usage --json` performs, as a C ABI library that a macOS appcan call in-process.Provider usage and quota, as a pure Zig core with a C ABI, a CLI, and a Swift wrapper.
`packages/ai` keeps the TypeScript implementation. It stays the reference: it generatedthe fixtures here, the parity test compares the two on the same bodies, and the JSONshapes are the contract in `packages/ai/docs/usage-query.md`.Hosts ask the core what to send and hand the bodies back, so the rules about what aprovider's quota *means* live in one place while TLS, proxies, cookies and HTTP/2 staywith whatever the host already has: `URLSession` in a macOS app, `fetch` in the agent CLI.
It reports quota for Alibaba Model Studio (token plan), Anthropic, DeepSeek, GoogleAntigravity, Charm Hyper, OpenAI Codex, opencode-go and zai — each with its own credentialkind (`oauth`, `apiKey`, `session`, or `cli` for a provider whose own CLI is already signedin) and its own plumbing of percentages, reset times and balances into one shape.
## The boundary: the core is pure, the host owns I/O
The library never opens a socket, reads a clock, or touches the filesystem. It exports:The core never opens a socket, reads a clock, or touches the filesystem. Providers arestate machines: `advance(state, responses)` returns the requests to answer or the finishedreport, so a provider that needs a value from one response to build the next request simplytakes another round.
| function | purpose || --- | --- || `usage_abi_version()` | ABI version, bumped on any signature or shape change. || `usage_supported_providers()` | JSON array of provider ids with a feed. || `usage_supported_providers()` | JSON array of provider ids, names and credential kinds. || `usage_credentials_from_access_token(jwt, len)` | `{"accountId"?, "email"?}` from OAuth token claims. || `usage_build_request(provider, len, token, token_len, account_id, account_id_len)` | `{"url", "headers": [[name, value], …]}` for the host to send. || `usage_parse(provider, len, body, body_len, now_ms)` | `{"planType"?, "limits": [...]}`. || `usage_advance(provider, len, credential_json, credential_len, responses_json, responses_len, now_ms)` | Requests to answer, or the finished report. || `usage_free(ptr)` | Release a returned string. NULL is accepted. |
Every call returns a NUL-terminated JSON string the caller frees with `usage_free`, andreports failure **inside** that JSON (`{"error":"..."}`). NULL means only that allocationfailed. Optional strings may be NULL with length 0.failed. A request is either `{"kind":"http", method, url, headers, body}` for the host tosend, or `{"kind":"command", command, args, timeoutMs}` for the host to run. The core neverspawns anything; a host that will not run commands answers `status: 127` and the feed routesaround it.
That split is deliberate. A usage query is one GET, and doing it here would mean shippinga TLS stack with its own trust store, proxy handling, and HTTP version behaviour. A hostthat already has NSURLSession gets all of that for free, so the app asks the core for therequest, performs it, and hands the body back for parsing.That split is deliberate. A usage query is one GET, and doing it here would mean shipping aTLS stack with its own trust store, proxy handling and HTTP version behaviour.
## Build and test
Pinned toolchain: **Zig 0.16.0**. The standard library moved `args`, `stdio`, `fs`, andPinned toolchain: **Zig 0.16.0**. The standard library moved `args`, `stdio`, `fs` and`clock` APIs in 0.16, so the version is not incidental.
```sh# hermetic install (checksum from ziglang.org/download/index.json)1 unmodified lineshasum -a 256 /tmp/zig.tar.xz # b23d70deaa879b5c2d486ed3316f7eaa53eacf6fc9cc747de152450d401489mkdir -p ~/.local/share/zig && tar -xJf /tmp/zig.tar.xz -C ~/.local/share/zigexport PATH="$HOME/.local/share/zig/zig-aarch64-macos-0.16.0:$PATH"
zig build # libusage.a, libusage.dylib, usage.h, usage CLIzig build test # core unit tests + the C ABI smoke testzig build smoke # C ABI smoke test alonezig build # libusage.a, libusage.dylib, usage.h, usage CLIzig build test # core unit tests + the C ABI smoke testzig build smoke # C ABI smoke test alonezig build run -- providerszig build run -- parse tests/fixtures/codex-usage-live.json --now-ms 1789514000000zig build run -- advance <provider> <credential.json> <responses.json> [--now-ms N]zig build run -- claims <access-token>```
The `smoke` step is not decoration: it compiles `tests/c_smoke.c` with the toolchain,links the installed static library, and calls the ABI the way a native host does.The `smoke` step is not decoration: it compiles `tests/c_smoke.c` with the toolchain, linksthe installed static library, and calls the ABI the way a native host does.
### Parity with the TypeScript reference
```shZIG=$(command -v zig) npm --prefix packages/ai run test test/usage-zig-parity.test.tsswift build --package-path swiftswift run --package-path swift usage-smoke tests/fixtures```
Both sides compute from the same fixture at test time, so a change to eitherimplementation fails the test rather than drifting quietly. The fixtures under`tests/fixtures/` are recorded provider bodies; the live one was captured from`chatgpt.com/backend-api/wham/usage` with the `user_id`, `account_id`, and `email` fieldsremoved.For a macOS 13 host, build the library against its deployment target:
## Swift```shzig build -Dtarget=aarch64-macos.13```
`swift/` wraps this library for native macOS hosts (dawn's tray app is the first consumer):## Parity with the TypeScript reference
`prime-agent` carries the reference implementation in `packages/ai/src/utils/usage/`, andits docs in `packages/ai/docs/usage-query.md` define the JSON shapes both sides speak. Bothimplementations compute from the same fixtures and a vitest suite compares them, so a changeto either one fails the test rather than drifting quietly:
```shzig build # produces zig-out/lib/libusage.aswift build --package-path native/usage/swiftswift run --package-path native/usage/swift usage-smokeZIG=$(command -v zig) npm --prefix packages/ai run test test/usage-zig-parity.test.ts```
- `UsageCore` is the thin C bridge: `providers()`, `advance(provider:credentialJSON:responses:nowMs:)` returning either the requests or the report, and `credentialsFromAccessToken`. Every returned string is freed with `usage_free`, and a NULL return becomes a thrown error rather than a crash.- `UsageDriver` runs the host loop with `URLSession`: ask the core for requests, send them, hand the responses back, repeat. A transport failure comes back as a response with status `0`, so the feed reports it as data and the row shows the reason instead of the call throwing.- `usage-smoke` proves the path against the recorded fixtures with no network: ABI version, provider list, the codex request shape, the codex report, anthropic's ISO reset time becoming epoch milliseconds, the warning status, the plan type from the profile, and the unknown-provider error.The fixtures under `tests/fixtures/` are recorded provider bodies. The live codex one wascaptured from `chatgpt.com/backend-api/wham/usage` with its `user_id`, `account_id` and`email` fields removed.
Headers cross the ABI as ordered pairs because HTTP allows a repeated name; `UsageRequest.headers`is the dictionary view for the common case.## Swift
The library is built for the host's deployment target. For a macOS 13 app, build it with`zig build -Dtarget=aarch64-macos.13`.`swift/` wraps the library for native macOS hosts (dawn's tray app is the first consumer):
- `UsageCore` is the thin C bridge: `providers()`, `advance(provider:credentialJSON:responses:nowMs:)` returning either the requests or the report, and `credentialsFromAccessToken`. Every returned string is freed with `usage_free`, and a NULL return becomes a thrown error rather than a crash.- `UsageDriver` runs the host loop with `URLSession`: ask the core for requests, send them, hand the responses back, repeat. A transport failure comes back as status `0` so a feed reports it as data instead of the call throwing. `runCommand:` answers command requests with only the environment allowlist (`PATH`, `HOME`, locale, timezone, proxies), a timeout and a stdout cap.- `usage-smoke` proves the path against the recorded fixtures with no network.
Headers cross the ABI as ordered pairs because HTTP allows a repeated name;`UsageRequest.headers` is the dictionary view for the common case.
## Status
- One provider has a feed: `openai-codex`. Others return `{"error":"provider has no usage feed"}`, which is how a host names providers that have none instead of implying they have no limits.- Bodies carry more than the parser reads today — `credits`, `spend_control`, `code_review_rate_limit` — so new limits are a parser change plus a fixture.- No Swift glue yet: the app-side wrapper (module map, `String` marshalling) comes with the first real caller.- Eight providers have feeds. Unknown providers return `{"error":"unknown provider"}` and known providers with no feed return `{"error":"provider has no usage feed"}`, which is how a host names the gap instead of implying a provider has no limits.- Providers differ in what they can answer from. Alibaba's token-plan keys are inference-only, so its feed prefers the official `bl` CLI and falls back to a captured console session; a provider whose answer needs a second round trips the host loop again.- Bodies carry more than the parsers read today, so new limits are a parser change plus a fixture.12345567789101123 unmodified lines// swift-tools-version: 5.9import PackageDescription
// Wraps the Zig usage core for native hosts. Build the library first:// zig build (in native/usage, produces zig-out/lib/libusage.a)// zig build (repo root, produces zig-out/lib/libusage.a)// then:// swift build --package-path native/usage/swift// swift build --package-path swift//// The core does no I/O: it builds the requests and parses the responses, and UsageDriver feeds it// through URLSession. That is the whole point of the split — TLS, proxies and HTTP/2 stay with the// host, and the rules stay in one place shared with the CLI.23 unmodified lines