Local Models
How usertrust classifies local endpoints and meters Ollama, vLLM, and LM Studio at nominal rates.
usertrust governs local (self-hosted) inference with the same two-phase spend, audit chain, and receipts as cloud calls. The endpoint -- not the model string -- picks the settlement regime: a call to Ollama on localhost settles at nominal local rates, a call to a paid API settles at cloud rates. Renaming a local model (ollama cp llama3.2 gpt-4o) cannot change the regime in either direction.
Free inference stays inside governance, not exempt from it.
Endpoint Classification
Every governed client is classified local or cloud by inspecting its baseURL. Classification runs in a fixed order — the first rule that applies wins:
- Explicit override — the
endpointoption oftrust()(trust(client, { endpoint: { class: "local", runtime: "vllm" } })) wins over everything onceclassis set;runtimedefaults to"unknown"when omitted. - No readable
baseURL— absent or malformed URLs classify as cloud. Never throws. endpoints[]matchers — first matching config entry wins.- Loopback autodetect —
localhost,127.0.0.1,::1, and the IPv4-mapped IPv6 loopback classify as local whenlocal.autoDetectLoopbackistrue(the default). The port hints the runtime:11434= ollama,1234= lmstudio,8000= vllm, anything else = openai-compat. - Cloud default — anything unmatched is cloud.
The default is fail-expensive by design: over-charging free inference at cloud rates is recoverable; under-charging a paid endpoint is not.
Endpoint Matchers
Each endpoints[] entry matches the client's baseURL in exactly one of three forms -- never raw string prefixing:
| Form | Example | Semantics |
|---|---|---|
| Scheme URL | http://gpu-box:8000 | Origin equality; path, query, and trailing slash ignored |
| Leading star | *.gpu.internal | Hostname suffix; matches a.gpu.internal, not gpu.internal itself |
| Bare hostname | gpu-box | Case-insensitive hostname equality, any port |
// .usertrust/usertrust.config.json
{
"budget": 500000,
"endpoints": [
{ "match": "http://gpu-box:8000", "class": "local", "runtime": "vllm" },
{ "match": "*.gpu.internal", "class": "local" }
]
}Origin equality is deliberate: prefix matching would let http://gpu-box:8000.evil.com or http://gpu-box:8000@evil.com classify as local. Origin comparison kills both shapes.
Nominal Metering
Local-scope rate resolution never touches the cloud pricing table:
local.modelsexact match- Longest trailing-star glob (
"llama3.3*","*") local.defaultRate—{ inputPer1k: 0, outputPer1k: 0 }by default
Every call cost is floored at 1 usertoken — zero-amount ledger transfers are invalid — so a {0,0}-rate local call settles at exactly 1 nominal usertoken. Real token counts are still metered: for local OpenAI-compatible chat.completions streams (not the Responses API, which reports usage on its own), stream_options: { include_usage: true } is auto-injected (local.injectUsageOptions, default true) so settlement uses server-truth counts instead of estimates; an explicit include_usage is respected, and a server that rejects the field gets one retry without it.
Why Budgets Still Bind
Nominal metering is not an exemption. Every governed local call still places a PENDING hold, settles at least 1 usertoken, and is recorded on the audit chain (or, if that write fails, its receipt carries auditDegraded) — so runaway loops burn budget, anomaly governance still fires, and every governed call has a receipt. The receipt carries the provenance:
| Receipt field | Local value |
|---|---|
endpoint | { class: "local", runtime: "ollama" | "vllm" | "lmstudio" | "openai-compat" | "unknown" }; "unknown" when an endpoints[] entry or override omits runtime |
meter.costBasis | "nominal" (or "usd-proxy" with rateClass: "amortized-usd") |
meter.rateSource | "local-model" or "local-default" |
usageSource | "provider" or "estimated" |
For GPU showback, set local.rateClass: "amortized-usd" and per-model rates in local.models — local calls then settle at your amortized chargeback rate (1 usertoken = $0.0001, as usual) instead of the nominal floor.
unknownModelPolicy
Only cloud-scope calls can hit an unknown model — local scope always resolves to local rates. When a model misses customRates (consulted only when pricing is "custom") and the pricing table (an exact id, or the id with one trailing -YYYYMMDD / -YYYY-MM-DD date stripped; there is no prefix matching), unknownModelPolicy decides:
| Policy | Behavior |
|---|---|
"fallback" | Silent dearest-known fallback rate (the pricing-table maximum); the receipt still carries meter.rateSource: "fallback" |
"warn" (default) | Same rate, one-time console warning per model per process, meter.rateSource: "fallback" on the receipt |
"deny" | PolicyDeniedError thrown before any PENDING hold is placed |
Security Posture
Endpoint classification -- config matchers, overrides, and loopback autodetect -- is a trusted-operator decision. Never wire it to end-user or request input. This is the same trust boundary as budget and customRates: the config author already controls billing entirely.
In server and multi-tenant deployments, set local.autoDetectLoopback: false and classify via explicit endpoints[] config. Loopback inside a container can be a forwarding sidecar to a paid API.
A compromised local server can under-report usage. Receipts expose usageSource and meter.rateSource precisely so that this is auditable.
Try It
The examples/ollama-local-governance demo shows the before/after -- frontier fallback billing vs. nominal local settlement -- plus the model-spoofing defense. It works against a live Ollama on localhost:11434 or an inline mock server, in dryRun mode against throwaway temp vaults. No TigerBeetle needed.
npm install
cd examples/ollama-local-governance
npx tsx run.ts