Halton Meter Docs
Docs /Concepts /SQLite schema
CONCEPTS · 03 macOS Stable

SQLite schema

What lands on disk for every captured request, the requests table, the projects join, body capture storage, pricing rates, and the millicent integer money convention.

macOS 12+ · Python 3.11+ ·6 min read ·Updated Jun 8, 2026

Halton Meter's primary store is a single SQLite database at ~/.halton-meter/db.sqlite, opened in WAL mode so the proxy hot path (write-mostly) and the FastAPI app (read-only) can share it without locking. The schema is owned by SQLAlchemy models under daemon/halton_meter/storage/models/. This page is the on-disk contract, what columns exist, what they mean, what conventions apply.

The five core tables

TableOwner modelWhat it holds
projectsproject.py::ProjectOne row per project slug
requestsrequest.py::RequestOne row per intercepted LLM call
request_bodiesrequest_body.py::RequestBodyOptional captured request/response bodies
project_settingsproject_settings.py::ProjectSettingsPer-project overrides (e.g. body capture toggle)
pricing_ratespricing_rate.py::PricingRateProvider × model × period rate cards

Plus three operational tables: audit_events, policies, and reconciliation_records, covered in their own contexts.

requests: the row that matters

Every metered call lands here. The columns, from storage/models/request.py:

ColumnTypeMeaning
idUUID4 stringPrimary key
project_idFK → projects.id (ON DELETE RESTRICT)Result of the Smart Attribution chain
providerstringanthropic, openai, gemini, xai
modelstringThe model id the provider returned (e.g. claude-opus-4-7)
modestringTransport mode; default standard
request_kindstringThe classifier column: chat, embedding, image, audio
input_tokensintProvider-reported input usage
output_tokensintProvider-reported output usage
thinking_tokensintAnthropic extended-thinking budget consumed
cache_read_tokensintCache hit input tokens
cache_write_tokensintCache creation input tokens
cost_usd_minor_unitsbigint, nullableCost in millicents; NULL if model unknown to the rate card
fx_ratenumeric, nullableUSD→other-currency rate used at cost time
rates_sourcestring, nullablebundled-YYYY-MM-DD, override, or NULL
latency_msfloat, nullableWall-clock latency
time_to_first_token_msint, nullableStreaming time to first token
tokens_completeboolFalse for streaming rows where the response stream was truncated mid-flight
statusstringsuccess, error, blocked_by_policy
http_status_codeint, nullableProvider HTTP status on error
provider_error_codestring, nullableProvider-reported error code
error_classstring, nullableNormalised error class (see Error classification)
error_message_hashstring, nullableHash of the error message (local-only)
retryablebool, nullableWhether the error is retryable (v0.3.0 Phase 2)
prompt_hashstring, nullableHash of the prompt for dedupe
idempotency_keystring, nullableProvider idempotency key
source_machinestring, nullableOriginating machine
source_workdirstring, nullableOriginating working directory
attribution_methodstring, nullableWhich attribution tier won
requested_attimestamptzUTC timestamp; indexed
recorded_attimestamptzUTC write time; part of the cloud-cursor index

The error-taxonomy columns http_status_code, provider_error_code, error_class, error_message_hash, and retryable arrived in v0.3.0.

cache_read_tokens and cache_write_tokens are intentionally separate columns. A single cached_tokens column was renamed on 2026-04-30 once cache_creation_input_tokens (the write side) became billable on a different schedule from cache_read_input_tokens.

Money is integer millicents

cost_usd_minor_units stores millicents, 1 USD = 100_000 minor units, as a BIGINT. Display-time conversion:

usd = cost_usd_minor_units / 100_000

The reasons are operational, not theoretical: REAL columns drift on sums, and a half-cent rounding error compounded across a month of captures will drive a reconciliation engineer mad. Storing integers and dividing at display time keeps every aggregation exact.

projects: the slug join

id           String(36) primary key (UUID4)
name         Text NOT NULL (the human-readable name)
slug         String(255) UNIQUE NOT NULL (normalised project slug)
created_at   first-sighting timestamp
archived_at  timestamptz, nullable

The proxy hot path auto-creates a projects row on first sighting via services.ingest._get_or_create_project. You never have to insert manually; just attribute correctly (see Project tagging) and the row appears.

request_bodies: opt-out body capture

When bodies.enabled = true (the default in config.toml), the daemon captures request and response bodies into a sibling table, separately from the metering row, so a body-capture failure cannot drop the cost attribution. The schema:

id            UUID4
request_id    FK → requests.id
direction     'request' | 'response'
content_type  application/json | text/event-stream | …
body          BLOB (redacted, up to bodies.max_body_bytes)
captured_at   timestamptz

Defaults from config.toml:

  • bodies.enabled = true
  • bodies.max_body_bytes = 524288 (512 KiB per body)
  • bodies.retention_days = 90
  • bodies.cache_ttl_seconds = 300

Redaction passes strip Bearer tokens, API keys, and other recognised secret shapes before the body is written. Per-category counts are visible via:

$ halton-meter bodies stats

To turn off body capture for a single project (overriding the daemon-wide switch):

$ halton-meter project set <slug> body-capture off

To purge old bodies manually:

$ halton-meter bodies purge --older-than 30d --vacuum

pricing_rates: temporal rate cards

id           UUID4
provider     string
model        string
mode         string (standard, batch, streaming, …)
input_rate_per_million_usd_minor_units        integer (millicents per 1M tokens)
output_rate_per_million_usd_minor_units       integer
thinking_rate_per_million_usd_minor_units     integer
cache_read_rate_per_million_usd_minor_units   integer, nullable
is_seed_default   bool (true = bundled seed row, false = operator override)
effective_from   timestamptz
effective_to     timestamptz, nullable (NULL = current)

The effective_from / effective_to half-open interval lets the rate card change without rewriting history. halton-meter recompute-costs walks every row in requests, looks up the active rate at requested_at, and rewrites cost_usd_minor_units, useful after a rate-card correction.

Migrations

The local install has no Alembic. On first start it runs metadata.create_all(), then applies ALTER TABLE migrations gated on PRAGMA user_version in storage/repo.py. The schema version is tracked by user_version, not a marker-row table: CURRENT_USER_VERSION = 13. Each step bumps user_version once its ALTER TABLE has applied, so a connect only runs the steps a given DB has not yet seen.

Reading the data

Every row is reachable via the local API (loopback only):

~: read the captures
$ curl -sS http://127.0.0.1:8765/v1/stats
$ curl -sS "http://127.0.0.1:8765/v1/projects/<slug>/requests?limit=50"
$ curl -sS -OJ "http://127.0.0.1:8765/v1/projects/<slug>/requests.csv"

Or directly with sqlite if you prefer:

$ sqlite3 ~/.halton-meter/db.sqlite '.schema requests'

For teams that want longer history without changing the on-disk shape, Cloud applies the same schema with a 90-day retention window on top of the local store.

What's next