Rho.Usage (rho v0.1.0)

Copy Markdown View Source

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)

Summary

Types

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

Functions

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

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

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

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.

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

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

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

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

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

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

A provider's last 429, or nil.

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.

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

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

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.

Counters summed over every model.

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

Types

call()

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

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

Functions

by_model(state, date)

@spec by_model(map(), Date.t() | :total) :: %{required(String.t()) => map()}

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

child_spec(arg)

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

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

duration(s)

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

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

exhausted(state, provider, now \\ now())

@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(atom, body, now)

@spec from_endpoint(atom(), map(), integer()) ::
  {%{required(String.t()) => map()}, String.t() | nil}

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

from_headers(arg1, h)

@spec from_headers(atom(), %{required(String.t()) => String.t()}) :: %{
  required(String.t()) => map()
}

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

limits(state, provider)

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

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

load(dir)

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

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

percent(arg1, now)

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

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

put_endpoint(dir, provider, windows, plan, now \\ now())

@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(state, provider)

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

A provider's last 429, or nil.

record(dir, model, call, now \\ now(), date \\ Budget.today())

@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(entries)

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

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

status_text(state, provider, now \\ now())

@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()

@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(by_model)

@spec sum(%{required(String.t()) => map()}) :: map()

Counters summed over every model.

windows(state, provider)

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

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