Skip to main content

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

The first call was priced at $0.001628 — its input, plus 300 output tokens at `max_tokens` — which did not fit in $0.0005. Nothing was spent and no call was made: not even the session’s call counter moved. Alice granted $0.01, the run resumed, and the call really cost $0.001321.

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".
Counters live in the agent’s memory store, next to its run records. Every change is one atomic operation inside the store, with the limit check in the same step as the increment (one guarded 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.)
Every field, default and meter is in the budgets reference. Budgets can also live in a JSON policy file, under its "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 in policy.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 by amount, and every later call draws on it until it is used up. Without amount, the grant is exactly the shortfall, 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 an amount that 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, unrecorded and remaining.

End instead of waiting

Use 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 behind base_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):
The third run stopped before its call. 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:
The first question in 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:
A person grants (or sends "decision": "deny"), and the run is resumed:
The last line is the resumed run’s 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

A meter name that does not exist:
The window and on_exhausted are checked the same way:
An application budget needs to know which application it belongs to:
Add "application_id": "support-desk" beside the scopes.
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:
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).
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.