PhoenixKitAI.Money (PhoenixKitAI v0.26.1)

Copy Markdown View Source

Money as the module keeps it: an amount of US dollars as a Decimal, never a float and never a scaled integer. A call's cost (Request.cost), the spend caps (PhoenixKitAI.Budget), the task limits (PhoenixKitAI.TaskLimits) and the prices per 1M tokens (PhoenixKitAI.ManualPrice) are all plain dollars.

  • to_decimal/1 reads a number from anywhere (a provider's JSON float, a stored string, a Decimal); a float goes through its shortest decimal form, so 3.0e-5 is exactly 0.00003, never Decimal.from_float/1's binary tail.
  • parse_dollars/1 reads what a person typed.
  • amount/1 is the strict reader for an amount set in code or stored as a setting: a Decimal or a dollar string. An integer is refused there, because the released versions stored those amounts as integer millionths of a dollar (5_000_000 meant $5), and reading one as dollars would loosen a cap a million times over. 0 is the one integer accepted (no amount).
  • round_cost/1 is the only rounding: a call's total cost, to the 18 decimal places of the cost column (an attodollar — far below the cheapest token).
  • to_json/1 is how an amount is written into a JSON map: a string, so it reads back as the same decimal and never as a float.

Summary

Functions

An amount set in code or stored as a setting, as dollars: a Decimal or a dollar string, from 0 to $1 000 000 000; the integer 0 is no amount. {:error, :ambiguous_amount} for any other integer or a float (see the moduledoc), :error for anything that is not an amount at all.

Whether a stored amount is in the shape the released versions wrote: an integer, or a string of digits only ("5000000"), which meant millionths of a dollar. A dollar string always has a decimal point or is "0".

Dollars typed by a person: finite, from 0 to $1 000 000 000. :error for anything else — not a number, NaN, an infinity, a negative amount.

Whether the database keeps costs in dollars: core's V223 has made phoenix_kit_ai_requests.cost and no legacy cost column (cost_cents, cost_nanodollars) is left beside it. Checked once per node and remembered once true; until then every call is refused ({:error, :money_migration_required}), because a sum of a column that is not there — or of a new empty one beside the old costs — reads as no spend.

A call's total cost rounded to the 18 decimal places of the cost column.

A sum read back from the database (a Decimal, or nil for no rows) as dollars.

A number as a Decimal: a Decimal, an integer, a finite float (through its shortest decimal form) or a decimal string (trimmed). :error for anything else, NaN and infinities included.

An amount as it is written into a JSON map or a setting: a decimal string with at least two places ("5.00", "0.10", "0.0000004"), so it reads back as the same decimal, never as a float, and never looks like the released versions' digits-only millionths (legacy_amount?/1).

Functions

amount(value)

@spec amount(term()) :: {:ok, Decimal.t()} | {:error, :ambiguous_amount} | :error

An amount set in code or stored as a setting, as dollars: a Decimal or a dollar string, from 0 to $1 000 000 000; the integer 0 is no amount. {:error, :ambiguous_amount} for any other integer or a float (see the moduledoc), :error for anything that is not an amount at all.

legacy_amount?(value)

@spec legacy_amount?(term()) :: boolean()

Whether a stored amount is in the shape the released versions wrote: an integer, or a string of digits only ("5000000"), which meant millionths of a dollar. A dollar string always has a decimal point or is "0".

parse_dollars(value)

@spec parse_dollars(term()) :: {:ok, Decimal.t()} | :error

Dollars typed by a person: finite, from 0 to $1 000 000 000. :error for anything else — not a number, NaN, an infinity, a negative amount.

ready?()

@spec ready?() :: boolean()

Whether the database keeps costs in dollars: core's V223 has made phoenix_kit_ai_requests.cost and no legacy cost column (cost_cents, cost_nanodollars) is left beside it. Checked once per node and remembered once true; until then every call is refused ({:error, :money_migration_required}), because a sum of a column that is not there — or of a new empty one beside the old costs — reads as no spend.

round_cost(cost)

@spec round_cost(Decimal.t() | nil) :: Decimal.t() | nil

A call's total cost rounded to the 18 decimal places of the cost column.

sum(sum)

@spec sum(Decimal.t() | integer() | nil) :: Decimal.t()

A sum read back from the database (a Decimal, or nil for no rows) as dollars.

to_decimal(value)

@spec to_decimal(term()) :: {:ok, Decimal.t()} | :error

A number as a Decimal: a Decimal, an integer, a finite float (through its shortest decimal form) or a decimal string (trimmed). :error for anything else, NaN and infinities included.

to_json(amount)

@spec to_json(Decimal.t()) :: String.t()

An amount as it is written into a JSON map or a setting: a decimal string with at least two places ("5.00", "0.10", "0.0000004"), so it reads back as the same decimal, never as a float, and never looks like the released versions' digits-only millionths (legacy_amount?/1).