An over-budget call
never runs.
Cap spend before a call executes — block, downgrade, or degrade gracefully — and get cost attribution per feature and per user, for free. No proxy, no server: a decorator.
from cendor.tokenguard import budget, track, report @budget(usd=0.50, on_exceed="block") # raises BEFORE an over-budget call def answer(q): with track(feature="support", user_id="alice"): return client.chat.completions.create(model="gpt-4o", ...) report(group_by=["feature", "user_id"]) # spend per tag — for free report().assert_under(usd=0.05, feature="search") # cost as a test assertion
Block, downgrade, truncate, raise — or your own policy.
@budget(usd=…, on_exceed=…) wraps any function. Pre-flight modes project the cost of the call about to run and act before a token is spent; post-flight modes account exactly and act on the next call.
Budgets nest — the tightest applicable cap wins; an inner downgrade never masks an outer hard cap. Attribution rides contextvars, so tags survive nested and async calls.
Watch it block, live.
A simulated agent loop under @budget(usd=0.50). Each iteration is one gpt-4o call costing $0.09. Run it past the cap and watch the mode decide what happens.
▸ each click simulates one gpt-4o call ($0.09) · enforcement happens before the call runs
Every dollar gets a feature and a user.
Wrap work in track(feature=…, user_id=…) and every call inside carries those tags — attribution rides contextvars, so tags survive nested and async calls.
unpriced_calls: 0
Every instrumented call is priced and tagged from ambient context — report(group_by=…) aggregates it; assert_under() turns cost into a CI assertion.
Where the edges are — by design.
block and downgrade project cost from offline token estimates plus an output reserve — they're approximate. Post-flight raise is exact, but stops the NEXT call, not the one that breached.
A streamed call is accounted when the stream is drained, not when it starts; a loop launching many streams eagerly can outrun post-flight modes (pre-flight still applies).
contextvars flow through async naturally; a plain threading.Thread starts fresh — use copy_context() for worker threads.
An unknown model warns (UnpricedModelWarning) and is counted in unpriced_calls; configure(on_unpriced="raise") makes block-mode reject such calls outright.
One decorator between you and a $400 surprise.
tokenguard docs → · cost-assertion cookbook → · composes with the Cendor stack