PhoenixKitAI.Budget (PhoenixKitAI v0.26.1)

Copy Markdown View Source

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:

settingscope
ai_daily_capevery call the module makes
ai_daily_cap_per_endpointcalls on one endpoint
ai_daily_cap_per_usercalls 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.

Summary

Types

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

Functions

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

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

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

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.

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

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

Types

amount()

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

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

scope()

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

status()

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

Functions

check(endpoint, opts)

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

@spec legacy_settings() :: %{required(scope()) => String.t()}

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

limit(scope)

@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(scope, amount)

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

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

The setting key for a scope's cap.

status(endpoint_uuid, opts)

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

@spec warn_percent() :: pos_integer()

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