BudgetGate
Deterministic, pre-execution spend limiting for semantic actions in agent systems.
Source of Truth
The canonical source is github.com/actiongate-oss/budgetgate. PyPI distribution is a convenience mirror.
Vendoring encouraged. This is a small, stable primitive. Copy it, fork it, reimplement it. See SEMANTICS.md for the behavioral contract if you reimplement.
Quick Start
from decimal import Decimal
from budgetgate import Engine, Ledger, Budget, BudgetExceeded
engine = Engine()
@engine.guard(
Ledger("openai", "gpt-4", "user:123"),
Budget(max_spend=Decimal("10.00"), window=3600), # $10/hour
cost=Decimal("0.03"), # fixed cost per call
)
def call_gpt4(prompt: str) -> str:
return openai.chat(prompt)
try:
response = call_gpt4("Hello")
except BudgetExceeded as e:
print(f"Budget exceeded: {e.decision.spent_in_window} spent")
Two Cost Modes
Fixed Cost (pre-execution)
When cost is known before execution:
@engine.guard(
Ledger("openai", "embedding"),
Budget(max_spend=Decimal("5.00"), window=3600),
cost=Decimal("0.0001"), # fixed cost per call
)
def embed(text: str) -> list[float]:
return openai.embed(text)
Bounded Dynamic Cost (pre-execution with estimate)
When cost depends on the result but has a known upper bound:
@engine.guard_bounded(
Ledger("anthropic", "claude", "user:123"),
Budget(max_spend=Decimal("5.00"), window=3600),
estimate=Decimal("0.50"), # max possible cost (reserved before execution)
actual=lambda r: Decimal(str(r.usage.total_cost)), # actual cost (committed after)
)
def call_claude(prompt: str) -> Response:
return anthropic.messages.create(...)
The estimate is reserved before execution. If it doesn't fit the budget, the action is blocked. After execution, the actual cost is committed and unused budget is recovered.
Core Concepts
Ledger
Identifies a spend-tracked stream:
Ledger(namespace, resource, principal)
Ledger("openai", "gpt-4", "user:123") # per-user
Ledger("anthropic", "claude", "team:eng") # per-team
Ledger("infra", "compute", "global") # global
Budget
Budget(
max_spend=Decimal("10.00"), # max spend in window
window=3600, # rolling window (seconds)
mode=Mode.HARD, # HARD raises, SOFT returns result
on_store_error=StoreErrorMode.FAIL_CLOSED,
)
Decision
Every check returns a Decision with:
decision.allowed # bool
decision.spent_in_window # Decimal - current spend
decision.remaining # Decimal - budget remaining
decision.requested # Decimal - amount requested
Decorator Styles
| Decorator | Cost Mode | Returns | On Block |
|---|---|---|---|
guard |
Fixed | T |
Raises BudgetExceeded |
guard_bounded |
Dynamic | T |
Raises BudgetExceeded |
guard_result |
Fixed | Result[T] |
Returns blocked result |
guard_bounded_result |
Dynamic | Result[T] |
Returns blocked result |
# Raises on block
@engine.guard(ledger, budget, cost=Decimal("0.01"))
def fixed_action(): ...
@engine.guard_bounded(ledger, budget, estimate=Decimal("0.50"), actual=lambda r: r.cost)
def dynamic_action(): ...
# Never raises - returns Result[T]
@engine.guard_result(ledger, budget, cost=Decimal("0.01"))
def fixed_action(): ...
@engine.guard_bounded_result(ledger, budget, estimate=Decimal("0.50"), actual=lambda r: r.cost)
def dynamic_action(): ...
Relation to ActionGate
BudgetGate complements ActionGate:
| Primitive | Limits | Use case |
|---|---|---|
| ActionGate | calls/time | Rate limiting |
| BudgetGate | cost/time | Spend limiting |
Both are:
- Deterministic
- Pre-execution
- Decorator-friendly
- Store-backed
Use together:
from decimal import Decimal
@actiongate_engine.guard(Gate("api", "search"), Policy(max_calls=100))
@budgetgate_engine.guard(Ledger("api", "search"), Budget(max_spend=Decimal("1.00")), cost=Decimal("0.01"))
def search(query: str) -> list:
...
API Reference
| Type | Purpose |
|---|---|
Engine |
Core spend tracking |
Ledger |
Spend stream identity |
Budget |
Spend policy |
Decision |
Evaluation result |
Result[T] |
Wrapper for guard_result |
BudgetExceeded |
Exception from guard |
| Enum | Values |
|---|---|
Mode |
HARD, SOFT |
StoreErrorMode |
FAIL_CLOSED, FAIL_OPEN |
Status |
ALLOW, BLOCK |
BlockReason |
BUDGET_EXCEEDED, STORE_ERROR |
Numeric Precision
All spend amounts use Decimal to avoid floating-point drift. See SEMANTICS.md §9.
License
Apache License 2.0. See LICENSE for the full text.
Release files for budgetgate 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| budgetgate-0.3.1.tar.gz | 24.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| budgetgate-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.2 kB
Release files / budgetgate-0.3.1.tar.gz
| Download URL | budgetgate-0.3.1.tar.gz |
|---|---|
| Size | 24.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c0f2e08450f2ced988eb8ea9c921a785632ea1b5848ebcb224634943c77ac64d
|
|
BLAKE2b-256 checksum How to use checksums |
86ca00b2cb03e079210eb7fa1e73dd74eb78f3affea08c93cd4c9e2a40c41df7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|
Release files / budgetgate-0.3.1-py3-none-any.whl
| Download URL | budgetgate-0.3.1-py3-none-any.whl |
|---|---|
| Size | 16.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bb004c78184a01427cab682ca6e1b0cec1017fd938b5198b5495776f2c23905d
|
|
BLAKE2b-256 checksum How to use checksums |
573112aa07be403e59130134f68236abff4e4ecfb934bda6caec82ec82fd28f5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|