npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@maurice-frank/aiusage

v0.2.0

Published

ccusage-shaped usage and cost reporting for remote LLM platform APIs (OpenRouter, OpenAI, Anthropic)

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 figure

Capability 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 aiusage collects. The completions-only notice 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:

  1. cost per period, stacked by series;
  2. cumulative cost per series, each line labelled at its end point;
  3. tokens per period, stacked by the same series: where the volume went is rarely the same shape as where the money went;
  4. 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;
  5. 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;
  6. 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;
  7. 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 figure

The 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 done

See 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-run

Known 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 unattributedCost or 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.
  • --local can 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 aiusage reports ccusage's figure as imported without 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-canonicalized says 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 session reports a session id, its tokens and its cost, but no start time, so session duration is not obtainableccusage blocks gives fixed-width billing windows, not sessions, and measured 2026-08-04 it covers Claude Code alone (1371.50 USD against daily --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 in tags.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