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

@bydtech/ai-cost-meter

v0.1.1

Published

Know what each LLM call costs, per request, per user and per feature, with budgets, without sending usage data anywhere.

Readme

ai-cost-meter

Know what each LLM call costs, per request, per user and per feature, with budgets, without sending usage data anywhere.

npm version CI License: MIT

Why it exists

Provider dashboards show what you spent yesterday, per API key. They do not show which user, which feature or which prompt spent it, and they cannot stop a runaway loop before the bill arrives. ai-cost-meter prices every call in your own process from the token counts the response already carries, tags it, adds it to daily and monthly budgets, and can refuse the next call when a budget is used up. Nothing leaves your machine: no service, no telemetry, no API key.

Prices change. The bundled prices are list prices read from each provider's own pricing page on the date in data/prices.json (updated, and checked per model). Every entry has a source URL: check it before relying on a number. Results are estimates of list price, not your invoice: discounts, credits, taxes, regional uplifts, priority or flex tiers, tool fees and rounding are not included.

Install

npm install @bydtech/ai-cost-meter

Node.js 20 or later. No runtime dependencies.

Quick start

From the command line, a quick estimate:

$ npx @bydtech/ai-cost-meter price gpt-5 --in 1200 --out 300 --cached 4096 --date 2026-10-16
openai/gpt-5  (2026-10-16, standard rates)
  input         1,200 tokens  x $1.25/MTok   = $0.001500
  cached input  4,096 tokens  x $0.125/MTok  = $0.000512
  output          300 tokens  x $10/MTok     = $0.003000
  total                                        $0.005012
source: https://developers.openai.com/api/docs/pricing
List-price estimate; your invoice may differ.

In code, record each call with tags and a budget:

import { createMeter, fromOpenAI } from '@bydtech/ai-cost-meter';

const meter = createMeter({
  budgets: [{ tag: 'user:*', window: 'day', limitUsd: 1 }],
  onBudget: (e) => console.log(`${e.tag} is at ${e.threshold * 100}% of $${e.limitUsd} today`),
});

// Use the object your OpenAI client returned; this one is a saved example.
const response = {
  model: 'gpt-5-mini',
  usage: {
    prompt_tokens: 1834,
    completion_tokens: 412,
    prompt_tokens_details: { cached_tokens: 1536 },
  },
};

const tags = ['user:42', 'feature:search'];
if ((await meter.check(tags)).ok) {
  const { costUsd, breakdown } = await meter.record({ ...fromOpenAI(response), tags });
  console.log(costUsd); // 0.0009369
  console.log(breakdown.lines);
  // {
  //   inputTokens: { tokens: 298, ratePerMtok: 0.25, usd: 0.0000745 },
  //   cachedInputTokens: { tokens: 1536, ratePerMtok: 0.025, usd: 0.0000384 },
  //   outputTokens: { tokens: 412, ratePerMtok: 2, usd: 0.000824 }
  // }
}

fromAnthropic(message) and fromGemini(response) work the same way. A fuller example with a file-backed store and a JSONL log is in examples/budgets.mjs.

CLI reference

ai-cost-meter price <model> [options]    estimate the list price of one call
ai-cost-meter report <log.jsonl> [opts]  summarize a log written by jsonlLogger()
ai-cost-meter models [options]           list the models with prices

price

| Flag | Default | Meaning | |---|---|---| | --in <n> | 0 | Uncached input tokens | | --out <n> | 0 | Output tokens, reasoning included | | --cached <n> | 0 | Input tokens read from the prompt cache | | --cache-write <n> | 0 | Input tokens written to the prompt cache (Anthropic: the 5-minute cache) | | --cache-write-1h <n> | 0 | Input tokens written to Anthropic's 1-hour cache | | --audio <n>, --cached-audio <n> | 0 | Audio input tokens, for models that price audio separately | | --provider <name> | none | Needed only when a model ID is listed by more than one provider | | --batch | off | Price at batch rates | | --date <YYYY-MM-DD> | today (UTC) | Price as of this date, which matters for announced price changes |

Counts accept 1,200 and 1_200. When the model is unknown, the CLI prints the closest known IDs and exits 1.

report

Reads the JSONL file written by jsonlLogger() and prints cost, calls and tokens per group.

$ npx ai-cost-meter report ai-cost.jsonl --group-by model
ai-cost.jsonl: 5 calls, $0.035050 total, 1 without a price (not in the total)

  COST        CALLS   INPUT  CACHED  OUTPUT  MODEL
  $0.027975       2  18,298  15,000     803  anthropic/claude-sonnet-4-6
  $0.006138       1   5,000   3,000   2,000  google-gemini-api/gemini-2.5-flash
  $0.000937       1   1,834   1,536     412  openai/gpt-5-mini
  $0.000000*      1     800       0     100  my-finetune

* some calls in this group have no price and are not in its cost.

| Flag | Default | Meaning | |---|---|---| | --group-by <model\|tag\|day> | model | How to group | | --tag-prefix <prefix> | none | With --group-by tag, only tags starting with this, for example user: | | --since <YYYY-MM-DD> | none | Calls on or after this UTC day | | --until <YYYY-MM-DD> | none | Calls on or before this UTC day |

The report sums the cost stored with each call; it does not re-price old calls.

models

Lists every model with its base input, output and cached rates, and whether it has tiers, batch prices or announced changes. --provider <name> narrows it.

Common flags

| Flag | Default | Meaning | |---|---|---| | -f, --format <text\|json> | text | Output format | | --prices <file> | none | Merge a custom price file over the bundled one. Repeatable; later files win. | | --no-default-prices | off | Use only the --prices files | | -h, --help, --version | | |

Exit codes: 0 ok, 1 no price for that model or mode, 2 usage error.

API reference

createMeter(options?) → Meter

| Method | Returns | What it does | |---|---|---| | record({ provider?, model, usage, tags?, batch?, at? }) | Promise<CostResult> | Prices the call, keeps it in the store, adds it to every budget it falls under, fires onBudget and onRecord. | | estimate({ provider?, model, usage, batch?, at? }) | CostResult | Prices a call without recording it. | | check(tags?, { estimateUsd?, at? }) | Promise<{ ok, budgets }> | The budgets that apply to a call with these tags. ok is false when one is used up, or when estimateUsd would push it over. Call it before the request to block it. | | summary({ groupBy?, tagPrefix?, since?, until? }) | Promise<Summary> | Totals over the calls in the store, grouped by model (default), tag or day. | | prices | PriceDataset | The merged prices in use. |

CostResult is { provider, model, matchedModel, costUsd, breakdown, warnings }. costUsd is a number in USD, or null when the price is unknown: never a silent 0. breakdown.lines has { tokens, ratePerMtok, usd } for each usage bucket with tokens, plus promptTokens, tierAboveInputTokens (the tier applied, or null), batch, priceEffectiveFrom and source.

Usage

usage is a set of token counts in buckets that do not overlap. Every field defaults to 0.

| Field | Tokens | |---|---| | inputTokens | Uncached input (text, image, video, documents) | | cachedInputTokens | Input read from the prompt cache | | cacheWriteTokens | Input written to the prompt cache (Anthropic: 5-minute) | | cacheWrite1hTokens | Input written to Anthropic's 1-hour cache | | audioInputTokens, cachedAudioInputTokens | Audio input, uncached and cached | | outputTokens | Output, including reasoning or thinking tokens |

If you build usage by hand, do not count a token in two buckets: OpenAI's prompt_tokens already includes cached tokens, for example. The adapters do this subtraction for you.

Adapters

They read the response object (an SDK's return value or the raw JSON body) without importing any SDK, and return { provider, model, usage, batch, warnings? } to spread into record(). Each takes { batch?, model? } to override what the response says.

| Adapter | Reads | Verified against | |---|---|---| | fromOpenAI(response) | Chat Completions: usage.prompt_tokens, usage.prompt_tokens_details.cached_tokens, usage.prompt_tokens_details.cache_write_tokens, usage.completion_tokens. Responses API: usage.input_tokens, usage.input_tokens_details.cached_tokens, usage.input_tokens_details.cache_write_tokens, usage.output_tokens. Uncached input is the total minus cached minus cache writes, as in OpenAI's own cost example. service_tier other than default is flagged. | Chat Completions reference, Responses reference, Prompt caching guide | | fromAnthropic(message) | usage.input_tokens, usage.cache_read_input_tokens, usage.cache_creation_input_tokens split by usage.cache_creation.ephemeral_5m_input_tokens / ephemeral_1h_input_tokens, usage.output_tokens. usage.service_tier: "batch" sets batch; other tiers and a non-global usage.inference_geo are flagged. | Messages API reference | | fromGemini(response) | usageMetadata.promptTokenCount (includes cached), cachedContentTokenCount, toolUsePromptTokenCount (counted as input), candidatesTokenCount + thoughtsTokenCount (output), the AUDIO entries of promptTokensDetails, toolUsePromptTokensDetails and cacheTokensDetails, serviceTier, and modelVersion for the model. Batch is not marked in the response: pass { batch: true }. | generateContent reference, UsageMetadata |

For streams: pass OpenAI's last chunk with stream_options: { include_usage: true } (or the response of response.completed), Anthropic's final message, or Gemini's last chunk.

Other exports

  • estimateCost(input, { prices?, defaultPrices? }): price one call with no meter. Never throws for a missing price; the reason is in warnings.
  • jsonlLogger(file): an onRecord callback that appends each record as one JSON line (synchronous append). readJsonl(file) and parseJsonl(text) read it back; summarize(records, options) is what summary() and the CLI use.
  • memoryStore({ maxRecords }): the default store.
  • defaultPrices(), validatePrices(value), loadPricesFile(file), mergePrices(...sets), createPriceBook(dataset), buildPriceBook(prices?, useDefaults?), priceCall(book, input), normalizeUsage(usage).
  • Errors: UsageError (bad arguments or data), UnknownPriceError (missing price with unknownModel: 'throw').

All types are exported.

Configuration

createMeter options, all optional:

| Option | Default | Meaning | |---|---|---| | prices | none | Your own entries (a dataset or an array), merged over the bundled ones. Same provider and model replaces the bundled entry. | | defaultPrices | true | false uses only prices. | | budgets | [] | Spending limits; see below. | | onBudget(event) | none | Called once when a budget first reaches 80% and once at 100% in each window. event is { budgetId, tag, window, period, threshold, spentUsd, limitUsd }. | | onRecord(record) | none | Called with every recorded call. Use jsonlLogger(file) to write a log. | | store | memoryStore() | Where counters and records live; see below. | | unknownModel | 'warn' | When a price is missing (unknown model, or no batch or long-context price listed): 'warn' returns costUsd: null and warns; 'throw' throws UnknownPriceError. | | onWarning(message) | process.emitWarning | Receives each distinct warning once. | | now() | () => new Date() | The clock, for tests and replays. |

Budgets

budgets: [
  { window: 'month', limitUsd: 500 },                       // everything
  { tag: 'feature:search', window: 'day', limitUsd: 20 },  // calls tagged feature:search
  { tag: 'user:*', window: 'day', limitUsd: 0.5 },          // each user:<id> separately
]

window is day (resets at 00:00 UTC) or month (the 1st, 00:00 UTC). A tag ending in :* creates one budget per value. id defaults to <tag or "global">/<window>. Calls without a price are not counted. Budgets are checked when you call check(): the meter never blocks a call on its own, since it only sees the call after it was made.

Stores

The default store keeps counters and up to 100,000 records in memory, so budgets reset when the process restarts and are not shared between processes. To share or persist them, pass an object with four methods (each may return a promise):

interface MeterStore {
  increment(key: string, amount: number): number | Promise<number>; // atomic; returns the new total
  get(key: string): number | Promise<number>;
  append(record: MeterRecord): void | Promise<void>;
  records(): MeterRecord[] | Promise<MeterRecord[]>;
}

increment must be atomic (Redis INCRBYFLOAT, a SQL UPDATE ... RETURNING), so exactly one caller sees a threshold being crossed. examples/json-file-store.mjs is a single-process example that keeps everything in one JSON file; it is not part of the package.

Custom prices

A price file has the shape of data/prices.json; only provider, model, input_per_mtok and output_per_mtok are required. Use it for negotiated rates, your gateway's model names, or a dated snapshot ID that responses report. See examples/custom-prices.json.

The price data

data/prices.json holds 69 models, each read on 2026-10-16 from the provider's official pricing page:

| Provider key | Models | Source | |---|---|---| | openai | 43 (the text models in the flagship table, plus chat-latest, gpt-5.3-codex, gpt-5-search-api) | developers.openai.com/api/docs/pricing | | anthropic | 16 (every model on the page that is available on the Claude API) | platform.claude.com/docs/en/about-claude/pricing | | google-gemini-api | 10 (the text-generation models) | ai.google.dev/gemini-api/docs/pricing |

Each entry has rates in USD per million tokens and only the rates the page lists:

  • input_per_mtok, output_per_mtok, and where listed cached_input_per_mtok, cache_write_per_mtok, cache_write_1h_per_mtok, audio_input_per_mtok, cached_audio_input_per_mtok.
  • tiers: rates that replace the base rates when the prompt is longer than above_input_tokens (OpenAI above 272K, Claude Haiku 5.5 above 100,000, some Gemini models above 200K). Prompt length counts cached and cache-write tokens.
  • batch_discount where the page states one flat batch discount (Anthropic: 50%, with cache prices discounted too), or batch with the batch table's own numbers where the page lists them per model (OpenAI, Gemini). Models with neither have no batch price.
  • changes: price changes the page announces with a date. Gemini 4 Argon, Gemini 3.8 Flash and Gemini 3.6 Flash list higher prices from December 1, 2026 and January 1, 2027; the meter uses the rates in effect on the UTC date of each call.
  • notes, source, checked.

npm run check-data validates the file against data/schema.json and checks sort order, duplicate IDs (across providers too), that every number is finite, non-negative and under $1,000 per million, that cached rates do not exceed input rates, that tiers are in order and that announced changes are still in the future. A weekly workflow fails when updated is more than 30 days old, as a reminder to re-read the pages; it never edits the data.

Left out on purpose: models priced per minute, per image or per second, realtime and audio-generation models, embeddings, image and video models, fine-tuned model inference, Anthropic models retired from the Claude API, OpenAI's limited-access cyber and life-sciences models, and Gemini Live, TTS and transcription models. Add any of them with prices.

How it works, and its limits

Each call is priced as tokens × rate / 1,000,000 per usage bucket, summed, at the rates in effect on the call's UTC date, at the tier its prompt length falls in, at batch rates when asked. Records go to the store; budgets are counters keyed by budget, tag and window. The JSONL logger and the CLI use only the Node standard library.

When a rate is not listed, the meter does not guess low: cached tokens without a cached rate, and cache writes without a cache-write rate, are charged at the input rate, and the result carries a warning. When a whole price is missing (unknown model, a batch request on a model with no batch price, a long prompt with no long-context batch price), costUsd is null.

Not modeled, so not in the number:

  • Discounts, credits, committed-use pricing, taxes and currency conversion.
  • Regional or data-residency uplifts (OpenAI and Anthropic list 10% for some endpoints), Anthropic's US-only inference multiplier, priority, flex and fast modes. Adapters warn when a response shows one of these.
  • Tool fees (web search, code execution containers, grounding), context-cache storage billed per hour, and free tiers.
  • Gemini toolUsePromptTokenCount is counted as input; whether a given tool's tokens are billed is up to that tool's pricing.
  • Model IDs are matched exactly. A dated snapshot such as gpt-4o-2024-08-06 is unknown unless you add it, because the page may price it differently from the alias.
  • Budget windows are UTC. Totals are floating-point sums, rounded to 1e-12 USD.

Contributing

Corrections to the price data are the most useful contribution; see CONTRIBUTING.md. Issues and pull requests are welcome at github.com/BYDTechSolutions/ai-cost-meter.

License

MIT


Maintained by BYD Tech, a software studio that builds MVPs, AI agents, and web & mobile apps.