Spend caps over the trailing 24 hours, checked before every provider call.
Usage rows already carry cost, endpoint and user; a cap is a sum over
the trailing day plus a refusal. Three scopes, each a module setting in
the unit of the cost_cents column (millionths of a dollar — the module
calls them nanodollars; 1_000_000 = $1) where 0 means no cap:
| setting | scope |
|---|---|
ai_daily_budget | every call the module makes |
ai_daily_budget_per_endpoint | calls on one endpoint |
ai_daily_budget_per_user | calls attributed to a user_uuid: |
ai_budget_warn_percent (default 80) marks when a scope is close: the
call still runs, but a Logger.warning and a
[:phoenix_kit_ai, :budget, :warning] telemetry event fire once per
crossing per scope (the flag clears when spend drops back under the
line).
Once a cap is reached every verb returns {:error, {:budget_exceeded, scope}} without a provider call — cached answers included: a cap is a
stop switch, so a runaway site goes quiet rather than half-quiet. The
window is rolling, not a calendar day: spend from 23:00 still counts at
09:00. The check reads the usage table; there is no reservation, so
concurrent callers can overshoot by their in-flight calls. It also fails
open — a database error reads as zero spend and no cap — because the
module would otherwise refuse everything on a blip.
The per-user cap only bites when the caller passes user_uuid:, and the
value must be a PhoenixKit user uuid (the usage row has a foreign key on
it; a row with an unknown user is not written and is logged as a
warning). Anonymous visitors cannot be capped individually; cap the
endpoint or the site instead.
Summary
Functions
:ok when every configured cap has room, {:error, {:budget_exceeded, scope}} for the first scope that is reached. Emits the warning for
scopes past the warn percent; status/2 does not.
The cap for a scope in nanodollars (0 = none).
Sets a scope's cap (nanodollars; 0 removes it).
The setting key for a scope's cap.
Spent / limit / remaining for every scope that has a cap (the user scope
only when user_uuid: is given), with no side effects — hosts show it,
check/2 acts on it. Takes an endpoint struct or uuid.
The warn threshold as a percentage of a cap (default 80).
Types
@type scope() :: :global | :endpoint | :user
@type status() :: %{ scope: scope(), spent: non_neg_integer(), limit: non_neg_integer(), remaining: integer() }
Functions
@spec check(PhoenixKitAI.Endpoint.t(), keyword()) :: :ok | {:error, {:budget_exceeded, scope()}}
:ok when every configured cap has room, {:error, {:budget_exceeded, scope}} for the first scope that is reached. Emits the warning for
scopes past the warn percent; status/2 does not.
@spec limit(scope()) :: non_neg_integer()
The cap for a scope in nanodollars (0 = none).
@spec set_limit(scope(), non_neg_integer()) :: {:ok, term()} | {:error, term()}
Sets a scope's cap (nanodollars; 0 removes it).
The setting key for a scope's cap.
@spec status(PhoenixKitAI.Endpoint.t() | String.t(), keyword()) :: [status()]
Spent / limit / remaining for every scope that has a cap (the user scope
only when user_uuid: is given), with no side effects — hosts show it,
check/2 acts on it. Takes an endpoint struct or uuid.
@spec warn_percent() :: pos_integer()
The warn threshold as a percentage of a cap (default 80).