# 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 <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.

```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.
