How budgets are counted
The full rules for ai: { limits: } and how each call is counted
against them. The common case is under Budgets.
-
limits:takesper_request:,per_day: { tokens: cost: },per_caller_per_day:andstore:. Token limits are whole numbers written out, 1 to a trillion.cost:is aMoneysetting, so a setting can holdMoney(What a setting can hold), read from its canonical text,"GBP 20.00".ai:withoutlimits:,limits:withoutper_day:, andper_day:withouttokens:aresemantic.ai_budget_missing, since the tokens are the hard cap and the cost only an estimate. Every other mistake in it isai.settings_invalid. -
The main store holds the counts. It is the database
store:names; elsemain(a declared one, or the one an app with none has); else the app's only database. With several databases and none of themmain,store:is required. -
The usage table,
jadpo_ai_usage, has one row per day (UTC) and window: the app's, each caller's, and each prompt and model's. The table is asked for onceai:checks cleanly, so a migration creates it; anai:that still has errors asks for none. Its rows hold counts, the prompt's name, the model's and a digest of who the caller is (a principal's key, oraddress <ip>without one). They never hold a prompt, an answer or an address. The tick, the runtime's regular housekeeping (The tick), deletes rows last changed 62 days before, sojadpo ai usagealways has this month and the last. -
Before sending (after the
too_longcheck, and after the call takes its step, so a rerun's steps line up), Jadpo works out the most the call could use: its rendered prompt's estimated tokens (four bytes a token) andmax_output:, each timesattempts:, since every attempt resends the prompt. That is checked against the request's count, which the invocation keeps (each action a test calls is a request of its own). Then it is taken, all at once, from the day's app row and, for a call with a caller or an address, the caller's row. -
How it is taken. One conditional
INSERT … ON CONFLICT DO UPDATE … WHEREstatement covers every window that applies. It admits the call only while every row's used and held tokens, and the app's cost, stay within its limit. Without a caller window it updates one row; with one it updates both together. When a row can't take it, the call isover_budget, and nothing changes: no rows or timestamps, and any tentative new rows are rolled back. A repair or a retry that needs more uses the same statement, and adds to the call's held amount only when it succeeds. A store fault isunavailable. A call refused for its budget is answeredover_budgetwith nothing sent, and its step is kept so a rerun answers the same. -
After the call, it is charged what each answer's usage says, attempts included: the built-in driver's
usage.prompt_tokens,completion_tokensandprompt_tokens_details.cached_tokens, or a package's where itsusage:says. An answer that says nothing is charged the estimate of what was sent and what came back. An attempt with no answer is charged what it sent. One statement charges the app, the caller when there is one, and the prompt and model together, and releases the app's and caller's holds. A count that can't be written leaves every hold taken, so the limit is kept more strictly, not less. Tokens are input plus output; cached tokens are part of the input. -
Every call counts: in
jadpo test(so a test can use up a limit, each test having its own database), injadpo devwhether faked, sandboxed or local, and in production. So the developmentper_daylimit on sandboxed prompts is the app's own limits, counted in the development database. A call given back on a rerun is never charged again. -
ai.embedandai.streamcount too. The most an embedding could use is its texts' estimate, with no answer and one attempt, counted under the prompt nameembed. It is charged the answer'susage.prompt_tokens, or that estimate. A stream is charged the whole of the most it could use (its prompt's estimate andmax_output:) when it is opened, since what it used is never read from it. -
jadpo ai recordisn't counted: it is the developer's own one call, made outside any running app and its store. -
models: { prices: }names a constant of the package's libraries listing, for each model:model:, its name after its provider;currency:, aCurrency;input:,cached:andoutput:, each a price per million tokens in the currency's major unit, to six places;from:, the day the price holds from.
A call uses its model's row with the latest
from:on or before its day. Cost is counted in millionths of the currency, each kind of token rounded up. A model with no price, the built-in driver's included, adds nothing to the estimate, though its tokens still count. A price in another currency thancost:'s can't be compared with it, so the call ismisconfigured, and nothing is sent.ai: { prices: }overrides a package's price (Streams, embeddings and the OpenAI and Anthropic packages). -
jadpo ai usagereadsjadpo_ai_usagefrom the store's database: its connection's setting, a runningjadpo dev's, or--url. It shows the day, today's tokens againstper_dayand its calls, and today's cost. Then a table of each prompt and model, with this month's calls, today's tokens, the month's tokens and the month's cost (–without a price). The footer has today's tokens and share of the limit, or a failure footer once the day's tokens are spent.--jsongives the same with input, output and cached tokens. A database that can't be read isai.usage_unavailable.
Markdown for agents: /docs/how-budgets-are-counted.md.Built from Jadpo main at a707efb.