@maurice-frank/aiusage
v0.2.0
Published
ccusage-shaped usage and cost reporting for remote LLM platform APIs (OpenRouter, OpenAI, Anthropic)
Maintainers
Readme
aiusage
ccusage for the platforms.
ccusage reads local agent logs and tells you what
your coding CLIs cost. aiusage asks the platform billing APIs the same question, so
the number covers everything your organisation ran through them (apps, pipelines,
notebooks, agents), split by user account, API key, model and workspace, priced from
what the platform actually billed.
npx aiusage # daily usage across every configured platform
npx aiusage --json # ccusage-shaped JSON (see "JSON contract")
npx aiusage keys # which API key spent what
npx aiusage accounts --days 7 # which person spent what, last 7 days
npx aiusage monthly -b # months, with per-model rows
npx aiusage --local # platforms *and* local agents, via ccusage
npx aiusage report --out spend.svg # the report figureCapability matrix
Each platform answers a different subset of the question. aiusage reports what it could
actually get and leaves the differences intact. Run aiusage providers to see this
for your own credentials.
| | OpenRouter | OpenAI Platform | Claude Platform | Local (ccusage) |
|---|---|---|---|---|
| Token usage | yes | yes | yes | yes (local logs) |
| Cost | reported per row | per project-day | per model-day | imported |
| Split by model | yes | yes | yes | yes |
| Split by API key | with a management key | yes | yes | no |
| Split by user account | derived from key ownership | yes | yes | no |
| Split by workspace/project | yes | yes | yes | no |
| Split by agent | — | — | — | yes |
| Cache tokens reported | no | read + write | read + 5m/1h write | read + write |
| Request counts | yes | yes | no | no |
| Sub-daily (hourly) buckets | no | yes | yes | no |
| Lookback | 30 days | unlimited | unlimited | as far as the logs go |
| Live unit prices | /api/v1/models | LiteLLM table | LiteLLM table | ccusage's own |
Several OpenRouter workspaces. An OpenRouter management key is scoped to one
workspace, so the credential is a list: set OPENROUTER_MANAGEMENT_KEY_<LABEL> once per
workspace (or a comma-separated list in one variable). The label names that workspace in
reports when the key sees exactly one, tagged workspaceNameSource: "credential-label",
because OpenRouter has no workspace-name API. Every key is verified with
GET /api/v1/key first, so a management key pasted into OPENROUTER_API_KEY is still used
as one, and a key visible to two management keys is collected once.
Local agents (--local). ccusage reads the
coding agents' own logs on this machine and prices them itself. aiusage --local runs it
and fuses its rows in as a fourth source, split by agent (aiusage agents). Two things
this changes, both stated on every run: its cost is labelled imported
(see below), distinct from reported, and an agent billed through an API key is also inside
that platform's total, so the fused number can count the same traffic twice.
local-overlap-possible says so whenever both are present. Subscription-billed agents
(Claude Max, Codex plans) do not overlap.
ccusage collection deliberately uses daily rather than session: session also exposes
which project a session ran in, but its bucketing is coarser (a session's tokens land
entirely on its last-activity day). Measured on one machine, that shifted a 27-day
window's total by about 16% for no real gain, since ccusage only reports project paths for
some agents in the first place. daily's exact day-bucketed totals win.
Together AI is not supported. It publishes no usage or cost API — cost analytics are dashboard-only, and the public API reference contains no usage, cost, billing or audit endpoint (checked 2026-07-26). An earlier version shipped a Together provider that could only verify the key and contribute unit prices, never a single usage row; it was removed rather than kept as a source that answers nothing. Spend on Together has to be read from Together's own dashboard.
Credentials
Read from the environment; see .env.example for the full list. A platform without credentials is skipped and said so.
| Variable | Platform | Notes |
|---|---|---|
| OPENROUTER_API_KEY | OpenRouter | An inference key sees only its own activity. |
| OPENROUTER_MANAGEMENT_KEY | OpenRouter | Needed to split by key and account. OPENROUTER_PROVISIONING_KEY is accepted too. |
| OPENROUTER_MANAGEMENT_KEY_<LABEL> | OpenRouter | Repeatable: one per workspace. |
| OPENAI_ADMIN_KEY | OpenAI | Must be an admin key; project keys get 401. |
| OPENAI_ORG_ID | OpenAI | Only for multi-org admin keys. |
| ANTHROPIC_ADMIN_KEY | Claude | Admin API key (sk-ant-admin…) or org OAuth token. |
| AIUSAGE_CCUSAGE_CMD | Local | How to run ccusage for --local; discovered otherwise. |
| AIUSAGE_REPORT_DIR | — | Where report saves its figure; the working directory otherwise. |
OpenAI and Anthropic remain one credential each: their admin keys are org-scoped, and multi-org reporting is not implemented.
Where the money comes from
Every cost in the output is labelled with how it was established. This is the point of the tool: a per-key figure that was derived must not look like one the platform billed.
| costSource | Meaning |
|---|---|
| reported | The platform billed this exact row (OpenRouter activity rows). |
| allocated | The platform billed a coarser bucket (a project-day, a model-day) and that real amount was distributed across the rows inside it, in proportion to their derived cost. Platform totals stay equal to the invoice. |
| imported | Restated from another tool's own calculation (ccusage, pricing local agent logs from the LiteLLM table). No platform billed it; for a subscription-billed agent it is the API-equivalent of those tokens, distinct from money spent. |
| calculated | No billed figure was available: tokens × published unit price. |
| unavailable | Neither a billed figure nor a price could be found. Reported as 0 in JSON with costSource: "unavailable", which is distinct from free usage. |
| mixed | A row aggregates several of the above. |
Allocation exists because no platform attributes money to an API key or a person, while all of them attribute tokens that far. Two consequences worth knowing:
- Charges that are not token consumption (web search, code execution, session fees) are
not spread over token counts. They appear in
meta.unattributedCost. - OpenAI cost line items are opaque prose, so if your org also uses embeddings, images or
audio, that billed cost is included in the OpenAI total and allocated across the
completions usage
aiusagecollects. Thecompletions-onlynotice says so on every run.
Unit prices come from the platform's own catalogue where one exists (OpenRouter) and
otherwise from LiteLLM's price table, the same
source ccusage prices Claude Code with. meta.priceSources records which was used;
aiusage pricing shows the per-model prices and the key each was matched on.
Model identity across surfaces. Platforms spell the same model differently: OpenRouter
prefixes a vendor (anthropic/claude-opus-5), a platform's own API may carry a trailing
pinned-snapshot date (claude-opus-5-20260315), and ccusage's local logs use neither. A
--split model view (and the report figure's own model breakdowns) groups by the canonical
id (vendor prefix and pinned-snapshot date stripped (src/models.ts)), the
same transform LiteLLM price matching already trusts, so "the same model" reads as one row
instead of three. When this actually merges two or more distinct raw ids, a
model-id-canonicalized notice says so, naming them; merges are always disclosed.
modelsUsed still lists every raw spelling seen, unmerged, for anyone who needs the exact
strings a platform reported.
JSON contract
aiusage --json mirrors ccusage --json field for field on the shared parts, so anything
already parsing ccusage output keeps working:
{
"daily": [ // or "weekly" / "monthly"
{
"agent": "anthropic", // the contributing platform, or "all"
"cacheCreationTokens": 1500,
"cacheReadTokens": 200,
"inputTokens": 1500,
"metadata": {
"agents": ["anthropic"],
"providers": ["anthropic"],
"costSource": "allocated",
"requests": null,
"reasoningTokens": 0
},
"modelBreakdowns": [
{ "modelName": "claude-opus-4-6", "inputTokens": 1500, "outputTokens": 500,
"cacheCreationTokens": 1500, "cacheReadTokens": 200, "cost": 2.5,
"costSource": "allocated", "provider": "anthropic", "requests": null }
],
"modelsUsed": ["claude-opus-4-6"],
"outputTokens": 500,
"period": "2026-07-25",
"totalCost": 2.5,
"totalTokens": 3700,
// aiusage additions, present only for the splits you asked for:
"apiKeyBreakdowns": [ /* { id, name, …tokens, cost, costSource, providers, models } */ ],
"accountBreakdowns": [ /* … */ ]
}
],
"totals": { "cacheCreationTokens": 1500, "cacheReadTokens": 200, "inputTokens": 1500,
"outputTokens": 500, "totalCost": 2.5, "totalTokens": 3700,
"requests": null, "costSource": "allocated" },
// aiusage addition: derived shape, never a total. Each part is null when the
// collected grain cannot support it — see "Derived statistics" below.
"statistics": {
"timeOfDay": {
"hours": [ /* 24 entries: { hour, cost, tokens, requests, activeDays } */ ],
"week": [ /* only busy cells: { weekday (1=Mon), hour, cost, tokens } */ ],
"sources": ["anthropic"], // sources that reported sub-daily buckets
"coarseSources": ["openrouter"], // whole-day sources, excluded from the above
"excludedTokens": 48000, "excludedCost": 27.0,
"peakHour": 9, "measure": "cost"
},
"concentration": { "unit": "daily", "measure": "cost", "activePeriods": 27,
"topShare": 0.19, "periodsForHalf": 5,
"topDecileShare": 0.36, "topDecilePeriods": 3 },
"diagnostics": [ /* also folded into meta.notices */ ]
},
"meta": {
"tool": "aiusage", "version": "0.1.0", "generatedAt": "…",
"granularity": "daily", "range": { "since": "…", "until": "…" }, "timezone": "UTC",
"costIncluded": true, "priceSources": ["litellm@2026-07-26"],
"providers": [ /* per platform: status, capabilities, identity, recordCount, totalCost */ ],
"unattributedCost": [ /* billed money that is not token consumption */ ],
"notices": [ /* every diagnostic: code, level, provider, message */ ]
}
}Additive only: aiusage adds keys; it leaves every ccusage key in place with its original meaning. The compatibility contract is pinned by tests/report.test.ts.
Commands and flags
aiusage [daily] usage grouped by day (default)
aiusage weekly | monthly ISO weeks (Monday-labelled) / calendar months
aiusage models grouped by model, across the window
aiusage keys | accounts | workspaces
aiusage agents grouped by agent: ccusage agent names, with --local
aiusage providers capability matrix for your credentials
aiusage pricing [--model <id>] unit prices with their source
aiusage report the report figure: 90-day window, --local implied-j/--json · -s/--since <date> · -u/--until <date> · --days <n> · -z/--timezone <tz>
· -p/--provider <list> · --split <model,apiKey,account,workspace,provider,agent> ·
-b/--breakdown · --local · --hourly · -O/--offline · --no-cost · --compact ·
--color/--no-color. report also takes --out <file>, --format svg|html, --print and
--granularity daily|weekly|monthly.
report fuses local agent usage by default (it's usually a person looking at their own
machine's whole picture) and defaults to writing an HTML file instead of stdout, named
<today>-aiusage-report-<since>-to-<until>.html in $AIUSAGE_REPORT_DIR (the working
directory when that is unset; a leading ~/ is expanded). Missing directories are created.
--no-local drops the local fusion; --print opts back out to stdout; an explicit --out
always wins; --json is unaffected either way.
Dates accept YYYY-MM-DD or YYYYMMDD. The default window is the trailing 30 days
(OpenRouter's hard lookback limit, so the default is a window every platform can answer),
except for report, which defaults to 90 days so a daily figure has enough of a trend to draw.
Exit codes: 0 success, 1 a platform failed (its rows are missing and a notice says so),
2 bad invocation.
Timezones. Platforms bucket usage in UTC. With --timezone set to anything else,
OpenAI and Anthropic are queried in hourly buckets so a local day is grouped correctly;
OpenRouter only reports whole UTC days and emits a timezone-approximation warning.
--hourly. Asks OpenAI and Anthropic for hourly buckets whatever the timezone, which is
what the time-of-day statistics need. It costs roughly 24× the buckets and several times the
pages of the same window in days, so only report turns it on by default; --no-hourly opts
back out, and any other command can opt in. OpenRouter and ccusage report whole days no
matter what is asked, and say so in capabilities.hourly.
Derived statistics
statistics in the JSON, and two panels plus two summary cards in the report, answer
questions the time series cannot. Both are shape, never a total, and both are null when the
collected grain cannot support them.
Time of day — the window's cost (or tokens) summed by hour of your clock, plus the
weekday × hour grid behind it. Built only from sources that reported sub-daily buckets. A
whole-day bucket says nothing about when inside the day its tokens were spent, so it is
excluded rather than spread across 24 hours: excludedTokens / excludedCost state how much,
coarseSources names which sources, and a time-of-day-partial warning says it out loud.
These panels are therefore smaller than the report's own totals whenever a whole-day source
is in the run — by design. With no hourly source at all the statistic is null and a
time-of-day-unavailable notice gives the reason.
In the weekday × hour heatmap, colour is a cell's rank among the busy cells, not its magnitude: hourly spend is heavy-tailed enough that linear bins put nearly every cell in the lightest step. Read it for pattern and the hour panel above it for size.
Concentration — whether the window is spiky or steady: the share of the measure in its
single busiest period, the fewest periods that together reach half of it, and the share in its
busiest tenth (rounded up to at least one period, with topDecilePeriods saying how many).
Computed from the same period rows the figure draws, so every source is included.
Projects are platform workspaces — an OpenAI project, an Anthropic or OpenRouter workspace. Usage whose platform reported no workspace keeps its own row rather than being dropped or folded into a named one. No source collected here reports the repository an agent ran in, so none is shown.
The report figure
aiusage report draws the same numbers as stacked panels on one shared time axis:
- cost per period, stacked by series;
- cumulative cost per series, each line labelled at its end point;
- tokens per period, stacked by the same series: where the volume went is rarely the same shape as where the money went;
- top models, ranked by cost (or tokens) as a dot chart: position along a shared scale (area and colour ramps are avoided), coloured by the provider that served each model rather than a per-model hue; the tail beyond the top few folds into a disclosed "Other N models" row, and a model billed under more than one provider gets the neutral mark instead of either provider's colour;
- projects and workspaces, ranked the same way, with unattributed usage kept as its own disclosed row — drawn only when a platform actually named a workspace;
- cost by hour of day on a clock axis, with the busiest hour labelled directly, and weekday × hour as a heatmap: see Derived statistics for which sources these cover and what they leave out;
- token mix, the share of each period that was uncached input, output, cache write and cache read, so a change in caching shows up on its own axis.
With --no-cost the two cost panels are dropped, and the token panels
take both the composition and the accumulation. Each series carries a vendor mark as well
as a colour, so no series depends on hue alone; the marks are original glyphs, distinct from
vendor logos, and a name that does not identify a vendor gets a neutral one.
Output is a self-contained SVG (no fonts, no scripts, no network), or a printable white page
with --format html, which adds a summary strip, the period table with a cost bar and a
token-mix bar per row, what every source actually answered, and every notice.
aiusage report --days 30 --out spend.svg # series = provider
aiusage report --local --split agent --out by-agent.svg
aiusage report --granularity monthly --days 365 --format html --out year.html
aiusage report --json # the numbers behind the figureThe caption is part of the deliverable: it carries the window, the cost provenance of what is plotted, the price sources, any source that did not fully report, and billed cost that is not token consumption. A figure that gets forwarded without its table should still state its limits.
Library use
The pipeline is plain functions over plain data, and the HTTP client is injectable:
import { applyCosts, collectUsage, loadConfig, loadPriceBook } from 'aiusage';
const config = loadConfig();
const collection = await collectUsage({ config, range, timeZone: 'UTC' });
const { priceBook } = await loadPriceBook({ /* … */ });
const { records, unattributed } = applyCosts(collection.results, priceBook);Development
mise run setup # cold start: toolchain, frozen deps, git hooks, verify
mise run check # lint + format + typecheck + tests: the definition of doneSee AGENTS.md for the working agreement.
Releasing
Publishing runs from CI (not from a laptop): bump version in package.json, merge, then
publish a GitHub release tagged v<version>.
.github/workflows/release.yml
re-runs the full check, refuses a tag that disagrees with the manifest, and publishes with
npm provenance. It needs one
repository secret, NPM_TOKEN (an npm automation token with publish rights).
The tarball is dist/ plus src/: the sources ship so the emitted source maps and
declaration maps resolve to something. To see exactly what a publish would send:
npm pack --dry-runKnown limits
- Together AI is not a source at all: it exposes no usage API (see above), so Together spend is absent from every total here rather than reported as unknown.
- OpenAI: only completions usage is collected; other products' cost lands in
unattributedCostor is allocated across completions (flagged per run). - OpenRouter: no cache-token split, 30-day lookback, and account attribution is derived
from key ownership (
tags.accountAttribution: "key-creator") because OpenRouter does not attribute an activity row to the member who made the request. --localcan double-count: local agent rows and a platform's rows may describe the same traffic, and nothing in the logs says which key a session used. Warned per run; never silently deduplicated.- Local agent rows are not re-priced: an agent log names a model but not the vendor that
served it, so
aiusagereports ccusage's figure asimportedwithout guessing a vendor to look the price up under. - Canonical model grouping (above) only strips a vendor prefix and a pinned-snapshot date;
it does not catch a platform's own internal version string that has no mechanical relation
to another surface's spelling. Two distinct dated snapshots of a model you wanted to tell
apart will read as the same row;
model-id-canonicalizedsays when a merge happened, not which snapshot won. - The time-of-day statistics never cover OpenRouter or local agent usage, because neither reports a bucket finer than a day. On a run that includes either, those panels total less than the report does; the excluded figures are stated beside them, but the two numbers are not meant to reconcile.
- No session, per-request or per-prompt statistics exist here, and none can from these
sources. No billing API has a session concept or a per-request row;
ccusage sessionreports a session id, its tokens and its cost, but no start time, so session duration is not obtainable —ccusage blocksgives fixed-width billing windows, not sessions, and measured 2026-08-04 it covers Claude Code alone (1371.50 USD againstdaily --by-agent's 2057.26 over the same 30 days, with Codex and other agents absent). Likewise no source reports a request's context size: Anthropic reports only which pricing tier (0-200k/200k-1M) a bucket fell in, which is intags.contextWindow, not a maximum. Any of these would mean parsing the agents' own transcripts, which this tool does not do. - "Project" means a platform workspace, never a repository or directory. No collected source reports the working directory an agent ran in.
Licence
MIT
