stella usage

Report on, replicate, and prune the cross-project usage hub at ~/.stella/usage.db, the same cost numbers as stella stats, across every project.

stella stats answers "what did this project cost." stella usage answers the same question across every project on this machine, by reading the usage hub, a single SQLite file per developer at ~/.stella/usage.db. It needs no provider and no API key, and nothing it does sends any data anywhere.

Synopsis

stella usage
stella usage report [--format <text|json|csv>] [--org <id>] [--by-tool]
stella usage sync [--all]
stella usage prune [--older-than <AGE>] [--max-rows <N>] [--gc-deleted] [--force] [--vacuum] [--dry-run]

A bare stella usage runs stella usage report with its defaults. Flags belong to a specific subcommand, so spell out report whenever you pass one.

What it does

Every project keeps its own record at <workspace>/.stella/private/store.db. That's the file stella stats and stella inspect read from. The hub is built from those project files: each time a turn finishes, its numbers get copied into ~/.stella/usage.db, so you can answer a question across all your projects without opening each project's database one by one.

This design gives you two things:

  • It still works when a project's own database is busy. Reading from the hub does not compete with a live session that's using store.db.
  • It outlives the project folder. A project you deleted six months ago still counts toward your totals, because its rows were already copied into the hub before the folder was removed.

Data only ever flows one way, from store.db to usage.db. Nothing here writes back into a project's own file, and if the hub is missing or briefly can't be opened, it never fails your turn. Copying data into the hub is always best-effort.

The hub stores summaries and metadata only, never your source code or tool output. Prompts are reduced to a short digest and preview. Registering with stella cloud changes which ids your rows carry, not what's in them, and it does not upload anything on its own.

Subcommands

report

Totals grouped by org, provider, and model, across every project copied into the hub: calls, tokens, cache reads, cost, and how many different projects contributed. This is the default.

sync

Copies this project's usage data into the hub, picking up where it left off. Safe to run again; --all repairs every project the hub has ever seen.

prune

Keeps the hub from growing forever, by age, by a row limit, or by clearing out projects whose folder is gone.

stella usage report

Groups every row in the hub by org, provider, and model, and totals them up. Rows are sorted by cost, highest first.

--format <text|json|csv>

text prints lined-up columns with a TOTAL row at the bottom (table also works as another name for the same thing). json (wrapped in a versioned envelope) and csv are meant for piping into other tools. The three text columns in CSV follow the RFC-4180 escaping rule, so an org id or model name containing a comma can never shift the columns after it.

Default text

--org <id>

Only shows rows recorded under this org id. Rows written before you registered have no org attached, so an --org filter always leaves those out.

--by-tool

Shows tool reliability instead of spend: calls, errors, and the error rate for each (tool, surface) pair, across every project, read from the hub's tool summary. Each row also breaks its errors down by ErrorClass (errors_by_class in JSON/CSV, CLASSES in the table view; unclassified covers calls recorded before their error type was tracked), so a question like "bash's error rate, not counting invalid_input" is simple math, not text matching. This gives you the same numbers behind the Observatory leaderboard, but with no time window and in a form you can pipe to other tools. It cannot be combined with --org, because this summary is keyed by project, not by org, and silently ignoring the filter would give you a wrong answer. It's called --by-tool rather than --tools, because --tools is already used for the session-wide tool-policy setting.

stella usage
stella usage report --format json
stella usage report --org acme --format csv > acme-spend.csv
stella usage report --by-tool --format json | jq '.rows[] | select(.error_rate > 0.03)'

Sample table output (for illustration only):

ORG            PROVIDER   MODEL                           CALLS        INPUT       OUTPUT   CACHE-READ       COST  PROJECTS
(local)        anthropic  claude-fable-5                    412      8214003       411220      6120884    31.4188         7
(local)        zai        glm-5.2                           268      3901556       288104      1902330     4.9071         4
TOTAL                                                       680     12115559       699324      8023214    36.3259

An installation that isn't registered shows its rows under (local), which is what stella prints for a missing org id. An empty hub tells you so and points you to stella usage sync.

stella usage sync

Normally, copying into the hub just happens on its own: each finished turn ships its data over best-effort. sync is the manual and the repair option. It moves each project's saved position forward, so running it again is always safe and only sends rows the hub doesn't already have.

--all

Walks every project the hub already knows about, instead of just the current folder. Use this to repair things: if a project's background sync failed partway through, its position falls behind, and --all fixes every project like that in one pass. A registered project whose local store.db is missing is reported as skipped, not as an error.

# This project only.
stella usage sync

# Every project the hub knows about — the repair pass.
stella usage sync --all

Syncing one project reports a single number. --all reports each project first:

acme-api                 128 row(s)
acme-web                 0 row(s)
old-prototype            skipped (no local store)
replicated 128 telemetry row(s) across 3 project(s)

stella usage prune

The hub keeps one row per model call, from every project, forever, including projects that no longer exist. prune controls how much it keeps. It refuses to run unless you give it at least one of --older-than, --max-rows, or --gc-deleted, so a bare stella usage prune can never accidentally delete anything.

--older-than <AGE>

Deletes rows older than this window. Use a number followed by d, w, h, mo, or y, for example 90d, 12w, 720h, 3mo, 1y. A bare number means days. The value must be positive; 0 would delete everything up to right now, which is almost always a mistake.

--max-rows <N>

A hard cap on how many rows the hub keeps. The oldest deletable rows are removed until the hub is at or under this number.

--gc-deleted

Also removes everything the hub holds for a project whose folder is gone and that was never registered to an org: its usage data, its summaries, its saved sync position, and its entry in the hub. "Gone" means the project's own folder is missing while its parent folder still exists, since that's what a real deletion looks like. A whole drive being unavailable, like an unplugged drive or an offline network share, is deliberately not treated as a deletion, so a temporarily disconnected disk never wipes out your local history. A project that comes back gets re-added on its next sync.

--dry-run

Shows what would be deleted, without deleting anything. Worth running first.

--vacuum

Runs SQLite's VACUUM afterward, giving the freed disk space back to your filesystem, and resets the cloud sync positions to match. This also runs automatically after a large prune.

--force

Also deletes rows that haven't been confirmed as sent to the cloud yet, which breaks a pending upload. Off by default.

# See what would be removed, without touching anything.
stella usage prune --older-than 1y --dry-run

# A year of history, plus everything held for projects you've since deleted.
stella usage prune --older-than 1y --gc-deleted --vacuum

# Or limit by row count instead of age.
stella usage prune --max-rows 100000

Prune is safe for cloud users by default. A row that belongs to a registered org and that the cloud upload hasn't confirmed yet is never deleted, even if it falls inside your age window. Those rows are reported back to you as kept. --force is the deliberate way to override that, and it permanently breaks the pending upload. Summary rows never get uploaded, so they age out and get deleted no matter what.

If a row limit can't be reached because the extra rows are all unconfirmed uploads, prune tells you that directly instead of quietly leaving the hub over its limit. Either wait for the upload to finish, or run again with --force.

Relationship to stella stats

stella stats

One project. Reads <workspace>/.stella/private/store.db, the original source of the data, including the per-run cache-cost columns. Its prune only affects that file.

stella usage

Every project. Reads ~/.stella/usage.db, which is built from the file above. Its prune only affects the hub. Pruning one never touches the other.

Both commands use the same --older-than format on purpose, so --older-than 3mo always works the same way on either command.

Examples

Your five most expensive models across every project, since rows already come back sorted by cost:

stella usage report --format json | jq '.rows[0:5] | .[] | {provider, model, cost_usd}'

Find the models you use across the most projects:

stella usage report --format json | jq '.rows[] | select(.projects > 2) | {provider, model, projects, cost_usd}'

Fix every out-of-date project after a stretch of interrupted sessions, then report:

stella usage sync --all
stella usage report

Keep the hub bounded on a schedule, safely:

stella usage prune --older-than 6mo --gc-deleted --vacuum

The hub is a real SQLite file at ~/.stella/usage.db (set STELLA_DATA_DIR to use a different folder). Beyond this command, you can query it with any SQLite client. See Telemetry & budget.