PhoenixKitAgent.Limits (PhoenixKitAgent v0.1.0)

Copy Markdown View Source

The spending and turn limits of a user (spec §7.3) and who is told when one is near or reached (spec §7.4).

What is counted

  • Money — PhoenixKitAgent.Spend: the user's rows of the AI request journal whose metadata.source is "agent", so a call that describes an attachment counts as well as a model call of a turn; every row with a cost, a failed call's included. Micro-dollars, $1 = 1_000_000.
  • Turns — the user's user messages with a turn_ref (one per turn), in any of their chats.

The windows are the calendar day and month of the site time zone. The limits in force are PhoenixKitAgent.UserConfig.effective/1's: the user's own, else the module's. A limit of 0 allows nothing.

The money limits apply only on an endpoint whose calls get what they cost in the journal (PhoenixKitAgent.Models.reports_cost?/2: a model call of a turn, or a picture's description on the vision endpoint, where the provider reports a cost or phoenix_kit_ai has a price for the model): on another, nothing would be counted, so only the turn limit applies there — the admin pages say so for the default model. An endpoint that cannot be found keeps the money limits.

When

check/3 runs before a turn, while the turn holds its locks (PhoenixKitAgent.Turns), so the previous turn of the user — and every journal row it wrote — is over by then. A turn that started under its limits runs to its end, even past them.

Notices

Each is sent once per limit and period: a row of phoenix_kit_agent_limit_events (user, kind, period, unique) is inserted first with ON CONFLICT DO NOTHING, and only an insert that went through sends anything.

  • 80 % of a limit (check/3) — an in-app notice to the user (PhoenixKit.Notifications.upsert_inapp/3, key agent:limit:<kind>:<period>). Event kind warn_80, period key cost:<period> or turns:<period>, so each of the three limits warns on its own.
  • A limit reached (announce_reached/2) — the in-app notice to the user (the same key, so an unread warning becomes the new text), and, only when the user's notify_on_limit is on, a notification of type "agent_limit" to the watchers (create_many/2, which respects a watcher who switched that type off). The two have their own events — the watchers' period key is w:<period>, written only when they are told — so notify_on_limit switched on later in the period still tells the watchers of that period, once.

A call refused by the phoenix_kit_ai budget is signalled by phoenix_kit_ai itself (its limit_reached event, to its watchers and to those of the task, guard_watchers/1); the turn's notice in the chat says the budget is used up.

The watchers are agent_limit_watchers (empty: every Owner) and the user's own notify_user_uuids, without the user, who is told as the user.

Notices name the user by e-mail and the limit with its numbers; nothing of a chat. A notice that cannot be sent is logged as the class of the failure.

Summary

Types

A limit of a user.

A limit as it stands: what was used of it in its period.

What a user used today and this month, and the keys of those periods.

Functions

Tells that the user of settings reached a limit (status from check/3): once per limit and period — the user in the app, and the watchers when the user's notify_on_limit is on.

Whether the user of settings (UserConfig.effective/1) may start a turn on endpoint_uuid now: :ok, or {:error, {:limit_reached, status}} for the first limit used up — the month's money, the day's money, the day's turns. On :ok, a limit at 80 % or more is announced to the user (once per period). Sends nothing about a limit reached: that is announce_reached/2.

The watchers the module adds to the AI guard's own for its tasks (watchers of PhoenixKitAgent.ai_tasks/0, called with the user's uuid or nil): agent_limit_watchers, plus the user's notify_user_uuids when their notify_on_limit is on, without the user. An empty agent_limit_watchers adds nobody: the guard tells its own watchers (every Owner by default) anyway.

Whether money counts for a call (:chat or :vision) on endpoint_uuid (see the moduledoc): an endpoint whose such calls get a cost (PhoenixKitAgent.Models.reports_cost?/2), or one that cannot be found.

What user_uuid used in the day and month of now (see the moduledoc).

Who is told about the user's limits: agent_limit_watchers (empty: every Owner) and the user's own notify_user_uuids, without the user.

Types

kind()

@type kind() :: :daily_cost | :monthly_cost | :daily_turns

A limit of a user.

status()

@type status() :: %{
  kind: kind(),
  used: non_neg_integer(),
  limit: non_neg_integer(),
  period_key: String.t()
}

A limit as it stands: what was used of it in its period.

usage()

@type usage() :: %{
  day_micro_usd: non_neg_integer(),
  month_micro_usd: non_neg_integer(),
  day_turns: non_neg_integer(),
  day_key: String.t(),
  month_key: String.t()
}

What a user used today and this month, and the keys of those periods.

Functions

announce_reached(settings, status)

@spec announce_reached(PhoenixKitAgent.UserConfig.effective(), status()) :: :ok

Tells that the user of settings reached a limit (status from check/3): once per limit and period — the user in the app, and the watchers when the user's notify_on_limit is on.

check(settings, endpoint_uuid, opts \\ [])

@spec check(PhoenixKitAgent.UserConfig.effective(), String.t() | nil, keyword()) ::
  :ok | {:error, {:limit_reached, status()}}

Whether the user of settings (UserConfig.effective/1) may start a turn on endpoint_uuid now: :ok, or {:error, {:limit_reached, status}} for the first limit used up — the month's money, the day's money, the day's turns. On :ok, a limit at 80 % or more is announced to the user (once per period). Sends nothing about a limit reached: that is announce_reached/2.

Options: :use — what the call on the endpoint is (:chat, the default, or :vision, see money_counted?/2); :now.

guard_watchers(user_uuid)

@spec guard_watchers(String.t() | nil) :: [String.t()]

The watchers the module adds to the AI guard's own for its tasks (watchers of PhoenixKitAgent.ai_tasks/0, called with the user's uuid or nil): agent_limit_watchers, plus the user's notify_user_uuids when their notify_on_limit is on, without the user. An empty agent_limit_watchers adds nobody: the guard tells its own watchers (every Owner by default) anyway.

money_counted?(endpoint_uuid, call \\ :chat)

@spec money_counted?(String.t() | nil, PhoenixKitAgent.Models.call_kind()) ::
  boolean()

Whether money counts for a call (:chat or :vision) on endpoint_uuid (see the moduledoc): an endpoint whose such calls get a cost (PhoenixKitAgent.Models.reports_cost?/2), or one that cannot be found.

usage(user_uuid, now \\ DateTime.utc_now())

@spec usage(String.t(), DateTime.t()) :: usage()

What user_uuid used in the day and month of now (see the moduledoc).

watchers(user_uuid)

@spec watchers(String.t()) :: [String.t()]

Who is told about the user's limits: agent_limit_watchers (empty: every Owner) and the user's own notify_user_uuids, without the user.