# `Rho.Usage`
[🔗](https://github.com/GenericJam/rho/blob/v0.1.0/lib/rho/usage.ex#L1)

How much of each provider subscription is left, and what the model calls
used, for the host's usage page and status line.

The windows (percent used, reset time) come from the providers' rate-limit
headers on every model call (`record/3`, from `Rho.LLM.ReqLLM`)
and from their OAuth usage endpoints on demand (`put_endpoint/5`, with
`Rho.Usage.Fetch`), as omp reads them:

  * Claude: `anthropic-ratelimit-unified-{5h,7d,7d_oi}-utilization` (a
    fraction) and `-reset` (unix seconds); `GET /api/oauth/usage` gives
    `five_hour` / `seven_day` / `limits[]` in percent with ISO resets.
  * ChatGPT: `x-codex-{primary,secondary}-used-percent`,
    `-window-minutes`, `-reset-at` (unix seconds); `GET
    /backend-api/wham/usage` gives `rate_limit.{primary,secondary}_window`.

Everything is kept in `usage.json` in the data dir (no tokens, no
secrets; it survives restarts):

  * `"limits"`: per provider, `"windows"` (id such as `"5h"` / `"7d"` →
    `%{"label", "used" (percent), "resets_at" (unix s or nil)}`),
    `"checked_at"`, `"source"` (`"headers"` or `"endpoint"`), `"plan"`
  * `"rate_limited"`: per provider, the last 429's `"at"`, `"resets_at"`,
    `"model"` and `"message"`
  * `"days"` (`"YYYY-MM-DD"` → model → counters, the last 31 days) and
    `"total"` (model → counters): `"requests"`, `"errors"`, `"input"`,
    `"output"`, `"cacheRead"`, `"cacheWrite"`, `"cost"` (dollars,
    notional on a subscription)

# `call`

```elixir
@type call() :: %{
  :status =&gt; integer() | nil,
  :headers =&gt; [{String.t(), String.t()}] | map(),
  optional(:usage) =&gt; map() | nil,
  optional(:body) =&gt; term(),
  optional(:error) =&gt; String.t() | nil
}
```

One model call as the adapter saw it; `usage` (req_llm's) only on success.

# `by_model`

```elixir
@spec by_model(map(), Date.t() | :total) :: %{required(String.t()) =&gt; map()}
```

Counters per model for `date` (`:total` for all time).

# `child_spec`

```elixir
@spec child_spec(term()) :: Supervisor.child_spec()
```

The registry of `subscribe/0`'s processes, under `Rho.Supervisor`.

# `duration`

```elixir
@spec duration(integer()) :: String.t()
```

`2h 5m`, `3d 4h`, `12m`, `<1m`.

# `exhausted`

```elixir
@spec exhausted(map(), atom(), integer()) ::
  %{window: String.t() | nil, resets_at: integer()} | nil
```

Is `provider`'s subscription used up at `now`: a window at 100 % that
resets later (the latest such reset), else a 429 recorded with a reset
still ahead? `%{window: label | nil, resets_at: unix s}` or nil.

# `from_endpoint`

```elixir
@spec from_endpoint(atom(), map(), integer()) ::
  {%{required(String.t()) =&gt; map()}, String.t() | nil}
```

The windows and plan a usage endpoint's JSON reports: `{windows, plan}`.

# `from_headers`

```elixir
@spec from_headers(atom(), %{required(String.t()) =&gt; String.t()}) :: %{
  required(String.t()) =&gt; map()
}
```

The windows a model call's response headers report (`%{}` without any).

# `limits`

```elixir
@spec limits(map(), atom()) :: map() | nil
```

A provider's `"limits"` entry (`checked_at`, `source`, `plan`), or nil.

# `load`

```elixir
@spec load(Path.t()) :: map()
```

Everything recorded so far (an empty store when there's no readable file).

# `percent`

```elixir
@spec percent(map(), integer()) :: non_neg_integer()
```

A window's percent used now, rounded (0 once its reset time has passed).

# `put_endpoint`

```elixir
@spec put_endpoint(Path.t(), atom(), map(), String.t() | nil, integer()) :: :ok
```

Stores what a provider's usage endpoint returned (`windows` from `from_endpoint/3`).

# `rate_limited`

```elixir
@spec rate_limited(map(), atom()) :: map() | nil
```

A provider's last 429, or nil.

# `record`

```elixir
@spec record(Path.t(), String.t(), call(), integer(), Date.t()) :: :ok
```

Records one model call to `model`: a request (an error unless it carries
`usage`) with its tokens and cost, the windows its headers report, and a
429 with when it resets.

# `session`

```elixir
@spec session([map()]) :: map()
```

A session's counters from its entries: every assistant message is a
request, `stopReason: "error"` an error.

# `status_text`

```elixir
@spec status_text(map(), atom(), integer()) :: String.t() | nil
```

The status line's figure for `provider`: `"5h 42% · wk 18%"`, plus
`" · 429 2h 5m"` while its last rate limit hasn't reset; nil before any
numbers. A window whose reset has passed counts as 0%.

# `subscribe`

```elixir
@spec subscribe() :: :ok
```

Sends the caller `{:rho_usage, :changed}` after every change to the
store (a model call, an endpoint refresh), so the chat's status line
follows calls made anywhere. A no-op while `Rho.Supervisor` isn't running.

# `sum`

```elixir
@spec sum(%{required(String.t()) =&gt; map()}) :: map()
```

Counters summed over every model.

# `windows`

```elixir
@spec windows(map(), atom()) :: [{String.t(), map()}]
```

A provider's windows, `5h` then `7d` then the rest by id: `[{id, window}]`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
