Cumulative USD budget tracker with threshold callbacks.
A budget process accumulates cost across turns and fires
caller-supplied callbacks when configurable thresholds are first
crossed. When a :max_usd ceiling is set, check/1 returns
{:error, %ClaudeWrapper.Error{kind: :budget_exceeded}} once the
running total is at or above the ceiling, giving callers a hard stop
before the next CLI invocation.
This is the Elixir port of the Rust crate's BudgetTracker /
BudgetBuilder. The Rust type is an Arc<Mutex<...>> handle shared
across clones; the idiomatic Elixir equivalent is a GenServer whose
pid (or registered name) is the shared handle. It mirrors the
process-based style of ClaudeWrapper.SessionServer and
ClaudeWrapper.DuplexSession.
It is self-contained: ClaudeWrapper.Session and
ClaudeWrapper.SessionServer are not aware of it. Drive it yourself
from a multi-turn loop -- record each turn's total_cost_usd, then
check/1 before the next turn.
Threshold semantics
:warn_at_usd--:on_warningfires the first time the running total is at or above this value, and never again untilreset/1.:max_usd--:on_exceededfires the first time the running total is at or above this value, andcheck/1returns{:error, %ClaudeWrapper.Error{kind: :budget_exceeded}}from then on.
Both thresholds are "at or above" (>=), so a record that lands the
total exactly on the threshold crosses it. Non-positive and
non-finite costs are ignored. reset/1 clears the total and re-arms
both callbacks so they can fire again.
Usage
{:ok, budget} =
ClaudeWrapper.Budget.start_link(
max_usd: 5.00,
warn_at_usd: 4.00,
on_warning: fn total -> IO.puts("warning: $#{total} spent") end,
on_exceeded: fn total -> IO.puts("budget hit: $#{total}") end
)
# In a multi-turn loop, after each turn:
:ok = ClaudeWrapper.Budget.record(budget, result.total_cost_usd)
case ClaudeWrapper.Budget.check(budget) do
:ok -> :keep_going
{:error, %ClaudeWrapper.Error{kind: :budget_exceeded, reason: %{total_usd: total, max_usd: max}}} -> {:halt, total, max}
end
ClaudeWrapper.Budget.total(budget)
#=> 4.32
Summary
Types
Callback invoked when a threshold is first crossed. Receives the running total (in USD) at the moment of the crossing.
A running budget process: its pid or registered name.
Functions
Check the budget against the configured ceiling.
Returns a specification to start this module under a supervisor.
Configured ceiling in USD, or nil when none is set.
Record an additional cost in USD.
Remaining budget in USD.
Clear the running total and re-arm both callbacks.
Start a budget tracker.
Cumulative cost recorded so far, in USD.
Configured warning threshold in USD, or nil when none is set.
Types
Callback invoked when a threshold is first crossed. Receives the running total (in USD) at the moment of the crossing.
@type option() :: {:max_usd, float() | nil} | {:warn_at_usd, float() | nil} | {:on_warning, callback() | nil} | {:on_exceeded, callback() | nil} | {:name, GenServer.name()} | GenServer.option()
@type server() :: GenServer.server()
A running budget process: its pid or registered name.
Functions
@spec check(server()) :: :ok | {:error, ClaudeWrapper.Error.t()}
Check the budget against the configured ceiling.
Returns {:error, %ClaudeWrapper.Error{kind: :budget_exceeded}} (with
:reason %{total_usd: total, max_usd: max}) when the running total
is at or above :max_usd, and :ok otherwise (including when no
ceiling is set).
Returns a specification to start this module under a supervisor.
See Supervisor.
Configured ceiling in USD, or nil when none is set.
Record an additional cost in USD.
Fires :on_warning the first time the running total reaches
:warn_at_usd, and :on_exceeded the first time it reaches
:max_usd. Non-positive and non-finite costs are ignored.
Returns :ok.
Remaining budget in USD.
Returns nil when no :max_usd ceiling is set. Otherwise returns
max - total, clamped at 0.0 once the ceiling is reached.
@spec reset(server()) :: :ok
Clear the running total and re-arm both callbacks.
After a reset the total is 0.0 and :on_warning / :on_exceeded
can fire again the next time their thresholds are crossed.
Returns :ok.
@spec start_link([option()]) :: GenServer.on_start()
Start a budget tracker.
Options
:max_usd- Hard ceiling in USD. Once the running total reaches this value,check/1returns{:error, %ClaudeWrapper.Error{kind: :budget_exceeded}}.nil(default) means no ceiling.:warn_at_usd- Warning threshold in USD.:on_warningfires the first time the running total reaches this value.nil(default) means no warning.:on_warning- 1-arity function fired once when:warn_at_usdis crossed. Receives the running total.:on_exceeded- 1-arity function fired once when:max_usdis crossed. Receives the running total.:name- Register the process with a name.
Also accepts standard GenServer.start_link/3 options.
Cumulative cost recorded so far, in USD.
Configured warning threshold in USD, or nil when none is set.