Provider monetary budgets
The limits run counters bound runtime work. The SDK's monetary ledger
separately bounds spending through a host transport. Prices remain in your
configuration: Touba embeds no model tariff.
Open a durable budget
Create a private application-state directory. Use the same file and budget ID for descendants, retries and resumptions of the same operation. A new ID means a new budget, not a resumption. This example allows 10 USD: one dollar is 1,000,000 micro-units.
import { mkdirSync } from "node:fs";
import { createSqliteProviderBudget } from "@touba/sdk/budget/sqlite";
mkdirSync(".touba/private", { recursive: true, mode: 0o700 });
const budget = createSqliteProviderBudget({
filename: ".touba/private/provider-costs.sqlite",
budgetId: "invoice-batch-2026-10",
currency: "USD",
limitMicros: 10_000_000n,
});
console.log(budget.snapshot().remainingMicros.toString());
budget.close();
Python uses the same SQLite format and operations:
from pathlib import Path
from touba_sdk.budget.sqlite import create_sqlite_provider_budget
Path(".touba/private").mkdir(parents=True, exist_ok=True, mode=0o700)
budget = create_sqlite_provider_budget(
filename=".touba/private/provider-costs.sqlite",
budget_id="invoice-batch-2026-10",
currency="USD",
limit_micros=10_000_000,
)
print(budget.snapshot()["remaining_micros"])
budget.close()
JavaScript uses bigint; Python uses int. Floating-point amounts are
rejected. Convert JavaScript amounts explicitly to decimal strings when
serializing snapshots as JSON. The ledger does not convert currencies.
Reopening with a larger limit preserves the previous ceiling. A smaller limit applies to all connections; it does not cancel requests already in flight.
Connect the JavaScript transport
createProviderCostFetch, imported from @touba/sdk, accepts these options:
| Option | Purpose |
|---|---|
budget | Shared ledger, kept open throughout requests |
quote(request) | Host-established upper cost bound for the final HTTP request |
readUsage(response, quote) | Provider usage priced with the same tariff |
fetch | HTTP transport with no hidden retries; defaults to globalThis.fetch |
Pass the returned function as your provider SDK's lowest-level fetch.
Each retry then requires its own reservation. With Touba's context preflight,
use this transport for both fetch and countFetch: counting requests also
need a known cost, possibly zero if the provider contract supports that.
Keep every billable HTTP path inside the protected transport.
The quote callback must return basis: "upper_bound", maxCostMicros,
currency, tariffId and tariffDate (YYYY-MM-DD). It must check the
model, complete input, maximum output and all relevant additional charges.
An average estimate, another model's price or a model-generated token count
is not an upper bound. An absent bound prevents dispatch. Declaring a request
free also requires evidence from the provider contract.
The readUsage callback returns one of these shapes:
const reported = { kind: "reported", costMicros: 230n };
const estimated = { kind: "estimated", costMicros: 240n };
const unknown = { kind: "unknown" };
These illustrate the contract, not real prices. reported means provider-
reported usage priced with the host tariff, not a verified invoice. Read usage
metadata, never a cost generated in the model's text. Only reported can
release unused reserved funds. Missing usage, a usage-reader timeout
(5 seconds by default), an SSE response or a connection failure retains the
whole bound. usageTimeoutMs adjusts this timeout. Automatic HTTP redirects
are refused.
The Python ledger provides reserve, settle, attempt, list_attempts,
snapshot and close. The Python HTTP provider can use the same ledger
through token_cost, without copying its balance or creating another budget.
Python options use snake_case.
Tariff adapters
createProviderTokenCostAdapter prices tokens using a host-supplied
tariff. Supported protocols are openai-responses, openai-chat,
anthropic-messages and google-generate-content.
The SDK contains no vendor prices.
tariff contains id, date, currency and rates. Rates are integer
micro-units per million tokens: input, output and, where needed,
cacheRead, cacheWrite5m, cacheWrite1h. JS uses bigint; Python uses
int and cache_read, cache_write_5m, cache_write_1h.
The calculation rounds up once, after summing the exact products.
inputTokenBound receives the complete final request, including history and
schemas. It returns { basis: "upper_bound", tokens: n } with a host
justification. A counting endpoint that only estimates tokens does not establish
a bound. The exact model and maximum output must match the tariff.
import { createOpenAiDecisionProvider, createProviderTokenCostAdapter } from "@touba/sdk";
export function buildProvider({ model, tariff, inputTokenBound, budget }) {
const adapter = createProviderTokenCostAdapter({
protocol: "openai-responses", model, tariff, inputTokenBound,
});
return createOpenAiDecisionProvider({
model, maxOutputTokens: 512, maxRetries: 0,
cost: { budget, ...adapter },
});
}
Python equivalent, using the ledger opened earlier:
from touba_sdk import create_open_ai_decision_provider, create_provider_token_cost_adapter
def build_provider(*, model, tariff, input_token_bound, budget):
adapter = create_provider_token_cost_adapter(
protocol="openai-responses", model=model, tariff=tariff,
input_token_bound=input_token_bound, budget=budget,
)
return create_open_ai_decision_provider(
model=model, max_output_tokens=512, max_retries=0, token_cost=adapter,
)
In Python the bound receives a dictionary with url, method, headers
and body, rather than a JavaScript HTTP object. These are trusted host
callbacks; do not log headers, which may contain credentials.
The supported scope is text and locally handled function proposals. Media, vendor-executed tools, remote history and unpriced options are refused rather than assumed free. OpenAI reasoning tokens already included in output are not counted twice. Anthropic cache reads and writes are separate. Gemini adds thought tokens and requires an explicit thinking bound. Other options require a qualified host adapter.
Using context with cost requires cost.counting = { quote, readUsage }
in JS. Python accepts counting={"quote": ..., "read_usage": ...} in its
adapter; callback results use max_cost_micros, tariff_id, tariff_date
and cost_micros. Counting shares the same budget. A zero cost requires
support from the vendor contract.
Trace correlation
When a provider runs through the Core, the ledger links each attempt to
run_id, invocation_id, agent_ref and step_index.
provider_cost_recorded events retain the reservation, status, currency and
tariff identifier/date, without requests, raw responses or secrets.
Amounts are decimal strings in micro-units. After invocation closure, late
usage may reconcile SQLite but cannot change a terminal trace. A ceiling refusal
does not retry the provider.
Resume and diagnose
snapshot() separates the ceiling, committed amount, remaining funds,
blocked state and counts of pending, reported, estimated and
unknown attempts. Unknown cost is null (Python None), never zero.
Committed funds include unresolved reservations, not just reported costs.
After a crash, listAttempts({status: "pending"}) finds attempts whose dispatch
or outcome remains uncertain. Page with afterId and limit (default 100,
maximum 1000), after stopping workers for a complete inventory. Python uses
list_attempts(status="pending", after_id=..., limit=...).
A trusted host operator can call settle(id, usage) with provider evidence.
Elapsed time, a restart or a missing response is not proof of a free request.
| Refusal | Consequence |
|---|---|
ProviderCostBoundUnavailable | No dispatch without a known bound |
ProviderCostBudgetExceeded | Insufficient funds; no new dispatch |
ProviderCostBoundViolated | Observed cost recorded above its bound; budget blocked |
ProviderBudgetStorageUnavailable | Unavailable ledger; no new dispatch |
Scope of the guarantee
The ledger protects an application ceiling under the host tariff and bound assumptions. It does not certify those assumptions or invoices, nor control requests outside the protected transport. Qualify adapters against the actual models and options you use.
The SQLite file belongs to the trusted host, on a local disk in a private directory. Preserve it with resumption state; deleting it would reset the accounting. No prompt, credential or raw response is stored.
See the no-paid-call tutorial and autonomy budgets.