# `PhoenixKitAI.Budget`
[🔗](https://github.com/BeamLabEU/phoenix_kit_ai/blob/v0.26.1/lib/phoenix_kit_ai/budget.ex#L1)

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 US
dollars (a decimal string, `"5"` or `"0.10"`) where `0` or blank means no
cap; the usage rows' `cost` is summed and compared as it is:

| setting | scope |
|---|---|
| `ai_daily_cap` | every call the module makes |
| `ai_daily_cap_per_endpoint` | calls on one endpoint |
| `ai_daily_cap_per_user` | calls attributed to a `user_uuid:` |

The released versions kept these caps under `ai_daily_budget*` in integer
millionths of a dollar (`"5000000"` was $5). The module's migration chain
copies them to the keys above in dollars and leaves the old keys alone. A
scope whose old key is still set while its new key is missing (the migration
has not run) refuses every call with `{:error, {:budget_unmigrated, scope}}`
rather than read millionths as dollars, which would loosen the cap a million
times over.

Beside the money, `ai_daily_calls_per_user` caps how many calls one user
may make in the same trailing day (a count, `0` = no cap; scope
`:user_calls`). A cheap model makes a money cap meaningless as a limit on
how much one person can use, and a count says it directly.

It counts **successful** calls (cached answers included — each writes a
zero-cost success row); failed provider calls cost nothing and do not
count. Like the money cap it reads the usage rows by `user_uuid`, so a
`user_uuid:` that is not a PhoenixKit user (its rows are written
unattributed, with a warning) is never capped — pass real user uuids.

`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). Its `spent` / `limit` measurements are in the scope's unit: dollars
(floats — a measurement must be a number), or a call count for
`:user_calls` — check the `scope` metadata before adding them up.

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 written without it, with a warning, so
it counts toward no user). Anonymous visitors cannot be capped individually; cap the
endpoint or the site instead.

# `amount`

```elixir
@type amount() :: Decimal.t() | non_neg_integer()
```

Dollars (a `Decimal`) for a money scope, a call count for `:user_calls`.

# `scope`

```elixir
@type scope() :: :global | :endpoint | :user | :user_calls
```

# `status`

```elixir
@type status() :: %{
  scope: scope(),
  spent: amount(),
  limit: amount(),
  remaining: Decimal.t() | integer()
}
```

# `check`

```elixir
@spec check(PhoenixKitAI.Endpoint.t(), keyword()) ::
  :ok | {:error, {:budget_exceeded | :budget_unmigrated, scope()}}
```

`:ok` when every configured cap has room, `{:error, {:budget_exceeded,
scope}}` for the first scope that is reached, `{:error, {:budget_unmigrated,
scope}}` for a cap still in the released versions' settings only. Emits the
warning for scopes past the warn percent; `status/2` does not.

# `legacy_settings`

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

The settings keys the released versions kept the money caps under (millionths).

# `limit`

```elixir
@spec limit(scope()) :: amount()
```

The cap for a scope: dollars (`Decimal`) for a money scope, a call count for
`:user_calls`; `0` = none (also for a cap not migrated yet).

# `set_limit`

```elixir
@spec set_limit(scope(), Decimal.t() | String.t() | non_neg_integer()) ::
  {:ok, term()} | {:error, term()}
```

Sets a scope's cap: dollars for a money scope — a `Decimal` or a dollar
string (`"5"`, `"0.10"`) — or a call count for `:user_calls`; `0` removes
it. A money cap given as any other integer or as a float is refused with
`{:error, :ambiguous_amount}`: the released API took integer millionths of a
dollar, and `set_limit(:global, 5_000_000)` must not become a $5,000,000 cap.

# `setting`

```elixir
@spec setting(scope()) :: String.t()
```

The setting key for a scope's cap.

# `status`

```elixir
@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. Money is dollars
(`Decimal`), `:user_calls` a count. A scope still in the released versions'
settings only is left out (`check/2` refuses on it).

# `warn_percent`

```elixir
@spec warn_percent() :: pos_integer()
```

The warn threshold as a percentage of a cap (default 80).

---

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