zig library for building requests to query inference providers usage limits
README.md

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.

# 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 <provider> <credential.json> <responses.json>... [--now-ms N]
zig build run -- advance -      # the same call as one JSON envelope on stdin
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, 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.

echo '{"provider":"anthropic","credential":{"kind":"oauth","access":"…"},"responses":[],"nowMs":0}' \
  | zig build run -- advance -
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:

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:

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.