# usage-core Provider usage and quota, as a pure Zig core with a C ABI, a CLI, and a Swift wrapper. Hosts ask the core what to send and hand the bodies back, so the rules about what a provider's quota *means* live in one place while TLS, proxies, cookies and HTTP/2 stay with 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, Google Antigravity, Charm Hyper, OpenAI Codex, opencode-go and zai — each with its own credential kind (`oauth`, `apiKey`, `session`, or `cli` for a provider whose own CLI is already signed in) 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 core never opens a socket, reads a clock, or touches the filesystem. Providers are state machines: `advance(state, responses)` returns the requests to answer or the finished report, so a provider that needs a value from one response to build the next request simply takes another round. | function | purpose | | --- | --- | | `usage_abi_version()` | ABI version, bumped on any signature or shape change. | | `usage_providers()` | JSON array of provider ids, names and credential kinds. | | `usage_state_new()` | Allocate opaque cross-round state for multi-round feeds (e.g. Hyper). | | `usage_state_free(state)` | Free opaque cross-round state. NULL is accepted. | | `usage_credentials_from_access_token(jwt, len)` | `{"accountId"?, "email"?}` from OAuth token claims. | | `usage_advance(state, 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`, and reports failure **inside** that JSON (`{"error":"..."}`). NULL means only that allocation failed. A request is either `{"kind":"http", method, url, headers, body}` for the host to send, or `{"kind":"command", command, args, timeoutMs}` for the host to run. The core never spawns anything; a host that will not run commands answers `status: 127` and the feed routes around it. That split is deliberate. A usage query is one GET, and doing it here would mean shipping a TLS 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` and `clock` APIs in 0.16, so the version is not incidental. ```sh # hermetic install (checksum from ziglang.org/download/index.json) curl -L -o /tmp/zig.tar.xz https://ziglang.org/download/0.16.0/zig-aarch64-macos-0.16.0.tar.xz shasum -a 256 /tmp/zig.tar.xz # b23d70deaa879b5c2d486ed3316f7eaa53eacf6fc9cc747de152450d401489 mkdir -p ~/.local/share/zig && tar -xJf /tmp/zig.tar.xz -C ~/.local/share/zig export PATH="$HOME/.local/share/zig/zig-aarch64-macos-0.16.0:$PATH" zig build # libusage.a, libusage.dylib, usage.h, usage CLI zig build test # core unit tests + the C ABI smoke test zig build smoke # C ABI smoke test alone zig build run -- providers zig build run -- advance ... [--now-ms N] zig build run -- advance - # the same call as one JSON envelope on stdin zig build run -- claims ``` The `smoke` step is not decoration: it compiles `tests/c_smoke.c` with the toolchain, links the installed static library, exercises multi-round state, and asserts against recorded provider bodies. `advance -` takes the whole call as one JSON envelope line on stdin instead, so a credential never lands in argv where other local processes can read it. The envelope ends at the first newline, so a host that cannot half-close a pipe (an Erlang port) writes one line and reads the answer. It is as stateless as the argument form: the host keeps every round's responses and replays them. ```sh echo '{"provider":"anthropic","credential":{"kind":"oauth","access":"…"},"responses":[],"nowMs":0}' \ | zig build run -- advance - ``` ```sh swift build --package-path swift swift run --package-path swift usage-smoke tests/fixtures ``` For a macOS 13 host, build the library against its deployment target: ```sh zig build -Dtarget=aarch64-macos.13 ``` ## Limits A report's `limits[]` carries what a host needs to track a limit over time, beside the display text: | field | meaning | | --- | --- | | `id` | Stable machine key within the provider (`weekly_opus`, `primary`, `codex_bengalfox.secondary`). `label` is display text and can change. | | `windowSeconds` | Window length when the provider states it or the plan fixes it; absent when unknown or calendar-shaped. | | `scope` | The provider's own name for the model family or feature the limit is restricted to; absent means it counts all usage on the account. | ## Parity with the TypeScript reference `prime-agent` carries the reference implementation in `packages/ai/src/utils/usage/`, and its docs in `packages/ai/docs/usage-query.md` define the JSON shapes both sides speak. Both implementations compute from the same fixtures and a vitest suite compares them, so a change to either one fails the test rather than drifting quietly: ```sh ZIG=$(command -v zig) npm --prefix packages/ai run test test/usage-zig-parity.test.ts ``` The fixtures under `tests/fixtures/` are recorded provider bodies. The live codex one was captured from `chatgpt.com/backend-api/wham/usage` with its `user_id`, `account_id` and `email` fields removed. ## Swift `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 - Eight providers have feeds. Unknown providers return `{"error":"unknown provider"}`. - 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.