ClaudeWrapper.Budget (ClaudeWrapper v0.8.1)

Copy Markdown View Source

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_warning fires the first time the running total is at or above this value, and never again until reset/1.
  • :max_usd -- :on_exceeded fires the first time the running total is at or above this value, and check/1 returns {: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()

@type callback() :: (float() -> any())

Callback invoked when a threshold is first crossed. Receives the running total (in USD) at the moment of the crossing.

option()

@type option() ::
  {:max_usd, float() | nil}
  | {:warn_at_usd, float() | nil}
  | {:on_warning, callback() | nil}
  | {:on_exceeded, callback() | nil}
  | {:name, GenServer.name()}
  | GenServer.option()

server()

@type server() :: GenServer.server()

A running budget process: its pid or registered name.

Functions

check(server)

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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

max_usd(server)

@spec max_usd(server()) :: float() | nil

Configured ceiling in USD, or nil when none is set.

record(server, cost)

@spec record(server(), number()) :: :ok

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

@spec remaining(server()) :: float() | nil

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.

reset(server)

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

start_link(opts \\ [])

@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/1 returns {:error, %ClaudeWrapper.Error{kind: :budget_exceeded}}. nil (default) means no ceiling.
  • :warn_at_usd - Warning threshold in USD. :on_warning fires the first time the running total reaches this value. nil (default) means no warning.
  • :on_warning - 1-arity function fired once when :warn_at_usd is crossed. Receives the running total.
  • :on_exceeded - 1-arity function fired once when :max_usd is crossed. Receives the running total.
  • :name - Register the process with a name.

Also accepts standard GenServer.start_link/3 options.

total(server)

@spec total(server()) :: float()

Cumulative cost recorded so far, in USD.

warn_at_usd(server)

@spec warn_at_usd(server()) :: float() | nil

Configured warning threshold in USD, or nil when none is set.