# Usage feeds

How `/usage`, `prime-agent usage --json`, the Zig core, and extension providers share one
definition of "what is this account's quota".

## The shape

A provider with a quota feed is a module exporting a `UsageFeed`. A feed is driven by the
host, never the other way round: the host resolves a credential, asks the feed for the next
requests, sends them with its own HTTP stack, hands the responses back, and repeats until
the feed reports.

```ts
interface UsageFeed {
  provider: string;                       // "anthropic", "alibaba", ...
  providerName: string;                   // "Anthropic"
  credentialKinds: readonly CredentialKind[];   // which credentials this feed accepts
  advance(state: UsageState, responses: readonly UsageResponse[]): UsageStep;
}

type UsageStep =
  | { requests: UsageRequest[] }          // answer these, then call advance again
  | { report: UsageReportBody };          // finished

type UsageRequest =
  | { kind: "http"; method: "GET" | "POST"; url: string;
      headers?: Record<string, string>;   // includes any provider-mandated User-Agent
      body?: string }                     // already encoded; POSTs carry their own body
  | { kind: "command"; command: string; args: string[];
      stdin?: string; timeoutMs?: number };  // the host runs it; exit status is the status

interface UsageResponse { status: number; body: string }
```

A **command request** exists because some providers ship a supported CLI that is a better interface
than their scraped endpoint: alibaba's token plan prefers `bl usage token-plan --console-region … 
--console-site … --output json` and only falls back to a captured browser session. The core never
spawns anything — it asks, and the host decides:

- a host that runs commands answers with the exit status and stdout (`coding-agent` runs only
  allowlisted command names, with a narrowed environment, a timeout and an output cap);
- a host that will not run commands answers `status: 127`, and the feed either routes around it (the
  console-session path) or reports that as the reason it has no feed.

`advance` is called first with no responses, then once per round of responses, in order.
Single-request providers ignore `responses` on the first call and report on the second;
providers that need a value from one response to build the next request (hyper: teams, then
team billing) use the loop the way it is meant to be used.

## The report

```ts
interface UsageReportBody {
  accountId?: string; email?: string; planType?: string;
  limits: UsageLimit[];      // may be empty for balance-only providers
  balance?: string;          // "11.83" — dollars or credits, already formatted
  notes?: string[];          // extra lines the UI shows
  error?: string | null;     // a failed fetch is data, not an exception
}
interface UsageLimit {
  label: string;             // "Primary", "Weekly", "Gemini", "Claude", ...
  usedPercent?: number;      // 0..100, absent when the provider reports no percentage
  windowLabel?: string;      // "5h window", "7d window"
  resetsAt?: number;         // epoch milliseconds
  status: "ok" | "warning" | "exhausted" | "unknown";
  detail?: string;           // e.g. "3 reset cards"
}
```

Rules that exist because providers break them:

- **Units differ per provider.** codex and opencode send percent; anthropic sends percent in
  `limits[].percent`; alibaba sends a *fraction* in `per1WeekPercentage`; antigravity sends a
  *remaining* fraction. Convert at the edge and store percent.
- **Timestamps differ too.** codex sends epoch seconds *or* milliseconds in `reset_at`;
  anthropic, antigravity, hyper and opencode send ISO-8601 strings, with and without
  fractional seconds. Normalise to epoch milliseconds.
- **Provider identity can be gated.** antigravity returns `403 SUBSCRIPTION_REQUIRED` unless
  the request carries an `antigravity/...` User-Agent, so a feed's request headers are part
  of its definition.
- **A missing window is not a zero.** `null`/absent means "this provider does not report that
  window"; the limit is omitted, and `status: "unknown"` is only for a limit that exists with
  no percentage.
- **No feed is a first-class answer.** `alibaba` without a captured console session, or a
  provider that has no quota API at all, must say so (`skipped` with a reason) rather than
  report zero usage.

## Credentials

Credentials are provider-defined, because reality is not a union of two shapes:

| kind | shape | providers |
| --- | --- | --- |
| `oauth` | `{ access, refresh?, accountId?, email?, ... }` | codex, anthropic, antigravity |
| `apiKey` | `{ apiKey }` | opencode-go, zai, deepseek, hyper |
| `session` | `{ session }` — opaque, provider-defined | alibaba (`{ url, region, secToken?, headers }`) |
| `cli` | `{ config? }` — no secret; the host runs the provider's own signed-in CLI | alibaba |

A feed declares which kinds it accepts; a host that cannot supply one reports the provider as
skipped and says why. Alibaba's session is captured from a browser HAR of the console
(`scripts/capture-alibaba-session.mjs`) because token-plan API keys are inference-only.

## Two implementations, one definition

| | typescript | zig |
| --- | --- | --- |
| feed modules | `packages/ai/src/utils/usage/providers/<provider>.ts` | `native/usage/src/providers/<provider>.zig` |
| driver | `advance` | `usage_advance(provider, credential_json, responses_json, now_ms)` |
| discovery | `usageProviders()` | `usage_providers()` |

The Zig ABI speaks the same JSON: `usage_advance` returns either `{"requests":[…]}` or
`{"report":{...}}`, where a request is `{"kind":"http","method","url","headers":[["k","v"]],"body"?}`
to send or `{"kind":"command","command","args","timeoutMs"}` for the host to run, so a native
host drives the identical loop with NSURLSession. `usage_supported_providers()` returns
`[{"provider","providerName","credentialKinds"}]`.

The Zig core lives in its own repository and is consumed here at `native/usage/` as a
submodule, so its lifecycle (toolchain pin, releases) is separate from this package while the
contract stays pinned by the parity test.

Adding a provider means one module in each language, one fixture, and a parity case. The
parity test (`packages/ai/test/usage-zig-parity.test.ts`) computes both implementations from
the same recorded bodies, so a change on either side fails the build instead of drifting.
