Skip to main content
Version: 1.0.0

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:

OptionPurpose
budgetShared 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
fetchHTTP 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.

RefusalConsequence
ProviderCostBoundUnavailableNo dispatch without a known bound
ProviderCostBudgetExceededInsufficient funds; no new dispatch
ProviderCostBoundViolatedObserved cost recorded above its bound; budget blocked
ProviderBudgetStorageUnavailableUnavailable 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.