Budgets
A budget caps what an agent may spend — dollars, tokens, model calls, tool calls, sandbox seconds — for one run, a session, the agent, or the whole application. By the end of this page you will have a run that stops before a model call it cannot afford, waits for a person to top it up, and carries on.Every output on this page is what the code printed when it was run. Prices and
the model’s wording will differ on your run.
A run that waits for money
How it works
1
Every scope that covers the run is charged
A run charges its own
request budget, its session’s, its agent’s, and the
application’s. Any one of them running out stops the work, and the run says
which.2
A model call is held before it is made
The runtime counts the input it is about to send and prices the most the
call could return:
model_config["max_tokens"], or 4,096 tokens when none
is set (sent to the provider as the call’s ceiling, so an answer that
reaches it ends with termination_reason length). That amount is
held, and the call is counted against model_calls. If it does not
fit, the call is not made. A hold larger than the whole limit never fits:
raise the limit (or lower max_tokens), since topping up by the
shortfall covers only that one call.3
The real cost replaces the hold
When the call returns, the hold is settled at what it really cost and its
tokens are counted. The trace records a
budget_warning when a meter
reaches warn_at (80%) of its limit plus anything a person granted, and
budget_exhausted when it runs out.4
At the wall: pause or end
With
on_exhausted: "pause" (the default) the run returns
status: "awaiting_budget" with a budget_request, keeping the work done
so far. With "terminate" it ends with status: "error" and
termination_reason: "budget_exhausted".UPDATE in SQL, one script in Redis,
one filtered update in MongoDB), so workers sharing a Redis, SQL or MongoDB
store share a budget, two of them cannot both spend the last dollar, and a
hundred runs charging one application budget at once do not fail each other.
Each hold is a record of its own, removed when the call settles. With the
default in-memory store, a budget lasts as long as the process. A finished
run’s own request counter is removed and its spend kept on the run’s record;
session, agent and application counters stay.
Counters written by 0.5.x are read and moved into this layout the first time
their key is touched; nothing is lost, and nothing needs to be run.
A charge for work already done never fails the run
A hold before a call can refuse the call: that is the budget doing its job. But once a model call or a tool call has happened, recording its charge cannot undo it, so a failed write does not fail the run. The write is tried again for up to 15 seconds; if the store is still unreachable, the charge is kept as unrecorded: on the run’s record (unrecorded_charges), in the trace as a
budget_charge_unrecorded event, and as unrecorded in budget_status and
GET /runs/{run_id}/budget. The counters then under-count by that amount, and
the record is where to find out by how much. Any hold the run left standing is
released when it ends.
Scopes, meters and windows
A window resets a counter:
total (never), day (00:00 UTC) or month (the
1st, 00:00 UTC).
A call’s output is known only when it returns, so a call can cross a token
limit: what it used is still counted in full, and the next call is stopped
before it is made (its input would not fit). Leave a call’s worth of headroom
if the limit must never be passed. (In 0.4.1 the call that crossed the limit
was not counted, and resuming made it again.)
"budgets" key
(policy reference); set them in one place or the other, not both: both is refused when the agent is built (for a policy file, at its first run, when the file is read).
On a policy object
A policy you build in code carries its budgets inpolicy.budgets, a
PolicyBudgets with one list of BudgetLimits per scope. They become part of
the policy and are hashed with it, like budgets in a policy file:
BudgetLimit takes the same fields as a dict limit (meter, limit,
window, warn_at, on_exhausted), and PolicyBudgets the same keys as
governance_config["budgets"] (request, session, agent, application,
application_id). A policy that has budgets takes no governance_config["budgets"]
as well.
When a budget runs out
Pause, then grant or deny
budget_request says what stopped the run:
Then, from this process or any other sharing the memory store:
await agent.grant_budget(run_id, approver="alice", amount=..., note=...)adds to that one budget. A grant belongs to the run, not to the call that stopped: it raises the run’s limit byamount, and every later call draws on it until it is used up. Withoutamount, the grant is exactly theshortfall, enough for the call that stopped and nothing more, so a run that stops one call at a time (a limit of one tool call, say) asks again at each. Name anamountthat covers the work still to do, and the run asks once. The policy is not changed; the grant is recorded with the approver’s name.await agent.deny_budget(run_id, approver="alice", note=...)refuses it.await agent.resume(run_id)then continues a granted run, or ends a denied one cleanly.await agent.budget_status(run_id)lists every budget covering the run:limit,granted,spent,reserved,unrecordedandremaining.
End instead of waiting
terminate where no person is watching: batch jobs, evaluations, a
background worker. A budget a person has already denied for this run is not
asked about again: the run ends.
A model with no published price
A model behindbase_url (vLLM, LM Studio, a gateway) usually has no
published price. Its calls are made and recorded, but a model_cost_usd
budget cannot govern them: it would never trip. Budget its calls and tokens
instead. Here against a local OpenAI-compatible server (a small stand-in that
gives the same answer to every question):
cost_complete: False says the 0.0
is not a real price, and the trace has a budget_cost_incomplete event for
each such call.
Over HTTP
OmniServe has the same flow. This server (on port 8123, beside the stand-in model on 8000) allows one model call per session:demo was answered; the second returned
"status": "awaiting_budget" with its run_id and budget_request:
GET /runs/{run_id}/budget returns budgets (as budget_status) and
requests (each time the run ran out). Its budget entry and the request’s
status:
"decision": "deny"), and the run is resumed:
status and response. Leave out
amount to grant the shortfall.
Budgets and the other limits
Budgets are not the only thing that stops a run; they are the only limit in dollars, and the only one across runs.
More in which limit to use.
When things go wrong
ValueError: budget meter must be one of …
ValueError: budget meter must be one of …
A meter name that does not exist:The window and
on_exhausted are checked the same way:ValueError: budgets.application_id is required to budget an application
ValueError: budgets.application_id is required to budget an application
An Add
application budget needs to know which application it belongs to:"application_id": "support-desk" beside the scopes.Run … is not waiting for a budget decision
Run … is not waiting for a budget decision
grant_budget or deny_budget on a run that is not awaiting_budget —
already granted, or never stopped — raises LookupError; over HTTP it is
a 404:A dollar budget never stops a local model
A dollar budget never stops a local model
The model has no published price, so
model_cost_usd has nothing to
count; cost_complete is False in the run’s totals. Budget
model_calls and model_tokens instead (above).The run keeps stopping after every grant
The run keeps stopping after every grant
A grant without
amount covers only the call that stopped. Grant more
headroom: amount=0.50.Next
Budgets reference
Every meter, window, scope and field.
Approvals
Pausing for a person, for tools as well as money.
Policies
Budgets can live in a policy file, with its rules.
Sandboxes and execution
Where sandbox seconds are spent.
Durable runs
Resume a waiting run from another process, days later.
Security model
What is enforced, and where.