Skip to main content
Version: 2.x

Expression reference

Bursar uses a safe expression language for pricing formulas. The same syntax works identically in Python and TypeScript.

All arithmetic is exact decimal (Python decimal.Decimal, TypeScript decimal.js) — never binary floating point. So input_tokens * 0.1 + output_tokens * 0.2 with both equal to 1 evaluates to exactly 0.3, not 0.30000000000000004, and the result is byte-identical across both SDKs. The engine quantizes the final cost to 6 decimal places with ROUND_HALF_UP; it never truncates a sub-credit cost to zero.

When to use expressions

Rate cards can express most pricing with per_unit and sum charges — the canonical standard card prices gpt-4o and gpt-4o-mini entirely that way. Expressions handle the non-linear cases: formulas combining several measures, or prices that need tier(), clamp(), or percentile(). Every expression is validated at config load, so an invalid formula fails validation instead of reaching a charge; a wrong function argument count surfaces as an ExpressionError when the formula is first evaluated.

Arithmetic

OperatorExampleDescription
+input_tokens * 0.01 + output_tokens * 0.03Addition
--cache_read_tokens * 0.001Subtraction / negation
*input_tokens * 0.01Multiplication
/output_tokens * (0.03 / 1000)Division
//input_tokens // 1000Floor division
%input_tokens % 1000Modulo

:::warning Exponentiation (**) is not allowed The ** / exponentiation operator is rejected at config-load time in both SDKs (it raises ExpressionError). This is deliberate DoS hardening: an unbounded exponent such as 9 ** 9 ** 9 could allocate gigabytes and hang the process. There is no constant-exponent carve-out — use repeated multiplication (x * x) if you need a power. :::

:::warning Division / modulo by zero raises x / 0, x // 0 and x % 0 raise ExpressionError in both SDKs. They do not silently produce inf/NaN and never flow into a charge. Any expression that evaluates to a non-finite result (inf/NaN) is also rejected as an ExpressionError. :::

Comparisons

OperatorExample
==output_tokens == 0
!=output_tokens != 0
<output_tokens < 1000
<=output_tokens <= 1000
>output_tokens > 1000
>=output_tokens >= 1000
in"gpt-4" in model
not in"batch" not in job_type

in / not in are substring containment checks (both sides are coerced to strings) so the two engines agree byte-for-byte.

:::warning Chained comparisons (a < b < c) are not supported Both SDKs reject chained comparisons at parse time with an ExpressionError. Python's chaining semantics (a < b and b < c) cannot be reproduced by the TypeScript left-associative parser, so neither engine allows them — this keeps the two engines byte-identical. Write the explicit and form instead: tool_calls > 0 and tool_calls <= 10. :::

Boolean

OperatorExample
andtool_calls > 0 and tool_calls <= 10
ortool_calls == 0 or cache_read_tokens > 0
not5 if not (tool_calls > 10) else 10

Ternary

Python-style conditional expression:

output_tokens * 0.5 if output_tokens > 1000 else output_tokens * 0.3

Functions

FunctionArityDescriptionExample
ceil(x)1Round upceil(input_tokens * 0.001)
floor(x)1Round downfloor(output_tokens / 1000)
round(x) / round(x, n)1–2Round half-up to nearest integer (or n decimals)round(input_tokens * 0.001)
min(a, b, ...)≥ 1Minimum of valuesmin(cost_a, cost_b)
max(a, b, ...)≥ 1Maximum of valuesmax(0, model_cost - allowance)
if(cond, then, else)exactly 3Conditionalif(input_tokens > 1000, cost_a, cost_b)
tier(val, t1, r1, [t2, r2, ...], default)even, ≥ 4Tiered pricingtier(input_tokens, 10000, 5, 100000, 10, 20)
clamp(x, lo, hi)exactly 3Range clampclamp(tool_calls, 0, 100)
percentile(p, v1, v2, ...)≥ 2, 0 ≤ p ≤ 100Percentile of valuespercentile(95, model_cost_1, model_cost_2)

The function set is intentionally small: no sum, abs, or arbitrary math-library access. A wrong arity (and percentile's p outside 0..100) raises ExpressionError when the formula is evaluated, so a typo fails loudly instead of silently mispricing.

round() uses ROUND_HALF_UP in both SDKs (it deliberately diverges from Python's built-in banker's rounding) so the two engines agree to the last digit. These are expression-level helpers a config author can call — the engine never implicitly rounds the total beyond the final 6dp quantization.

tier() details

Form: tier(value, t1, r1, [t2, r2, ...], default) — a value, one or more (threshold, rate) pairs, and a trailing default. The argument count must therefore be even and at least 4 (value + N≥1 pairs + default). Odd counts (3, 5, 7, …) and fewer than 4 arguments raise ExpressionError.

Returns r_i for the first threshold where value < t_i, else default:

tier(input_tokens, 10000, 5, 100000, 10, 20)
# value < 10000 → 5
# value < 100000 → 10
# otherwise → 20 (default)

percentile() details

Sorts values, computes p-th percentile (0 ≤ p ≤ 100) via linear interpolation. Requires at least 2 arguments; a p outside 0..100 raises ExpressionError.

percentile(50, input_tokens, output_tokens, tool_calls) # median of 3 values
percentile(0, a, b, c) # min
percentile(100, a, b, c) # max

Available variables

An expression charge may reference the measures declared by its operation, and must reference at least one — a constant formula like 1 + 1 is rejected. For example:

pricing:
operations:
completion:
measures:
input_tokens: { unit: token }
output_tokens: { unit: token }
rate_cards:
standard:
operations:
completion:
unmatched:
action: charge
charge:
type: expression
formula: input_tokens * 0.01 + output_tokens * 0.03

Dimensions such as model are selected through rate-card rules and are not expression variables. An undeclared or misspelled measure name is rejected when the configuration is loaded.

Safety

  • Python: AST-based validator with a strict node allowlist. The ** (ast.Pow) node is intentionally excluded, as are attribute access, subscripts, lambdas, comprehensions, f-strings, and imports. There is no exec(); the only eval runs a pre-validated AST in a namespace with no builtins.
  • TypeScript: Recursive-descent parser with a strict allowlist. No eval(), no Function() constructor. Variable lookup uses own-property checks (not the in operator), so prototype-chain identifiers (__proto__, constructor, prototype, toString, hasOwnProperty) are rejected as undefined variables rather than resolving to inherited members.
  • Variable-name validation: at config-load time every identifier must be a known metric variable or an allowed function; an unknown name (e.g. a typo like inputtokens) raises ExpressionError then, not at first runtime use.
  • Exact decimal, finite results: all math runs in Decimal; ** is rejected; division/modulo by zero raises; the final result is asserted finite. Non-finite results raise ExpressionError and never reach a charge.