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

tokentally

v0.1.7

Published

Token usage in, dollar totals out.

Readme

tokentally 🧮 — Count the tokens. Mind the tab.

CI npm Node.js License

tokentally is a TypeScript library for normalizing LLM provider token usage and estimating USD cost. Its core API works in browsers and Node.js; optional Node helpers load pricing and model limits from LiteLLM or OpenRouter.

import { estimateUsdCost, normalizeTokenUsage, pricingFromUsdPerMillion } from "tokentally";

const usage = normalizeTokenUsage({ prompt_tokens: 1_000, completion_tokens: 250 });
const pricing = pricingFromUsdPerMillion({ inputUsdPerMillion: 1.75, outputUsdPerMillion: 14 });
console.log(estimateUsdCost({ usage, pricing })?.totalUsd);
// 0.00525

Install

pnpm add tokentally

tokentally requires Node.js 24 or newer when used in Node.js projects. The package is ESM-only.

Quick start

Save the example above as cost.mjs, then run it:

$ node cost.mjs
0.00525

normalizeTokenUsage() accepts common snake_case and camelCase provider fields. It returns null when it cannot find a recognized token count, so unknown payloads do not silently become zero-cost calls.

Normalize usage

The normalizer understands OpenAI-style prompt_tokens and completion_tokens, Anthropic-style input_tokens and output_tokens, camelCase variants, cached input details, and reasoning token details. Missing recognized counts become zero, and a missing total is inferred from the fields that are present.

import { normalizeTokenUsage } from "tokentally";

const usage = normalizeTokenUsage({
  input_tokens: 120,
  output_tokens: 30,
  cache_read_input_tokens: 80,
});
// { inputTokens: 120, outputTokens: 30, uncachedInputTokens: 120,
//   cachedInputTokens: 80, totalTokens: 230 }

Price and tally calls

Pricing is expressed as USD per token. Use pricingFromUsdPerMillion() for the rates commonly published by providers, or pricingFromUsdPerToken() when the source already uses per-token values. Both helpers accept optional cache-read and cache-creation rates. When a catalog does not publish a cache rate, cache tokens fall back to the ordinary input rate rather than being treated as free. CostBreakdown.inputUsd includes all three input categories.

| API | Purpose | | --------------------------------------- | ---------------------------------------------------- | | normalizeTokenUsage(raw) | Normalize common provider usage shapes | | pricingFromUsdPerMillion(rates) | Convert published per-million rates | | pricingFromUsdPerToken(rates) | Validate per-token rates | | resolvePricingFromMap(map, modelId) | Resolve exact and common provider-prefixed model IDs | | estimateUsdCost({ usage, pricing }) | Price one normalized call | | tallyCosts({ calls, resolvePricing }) | Aggregate calls and cost by model |

tallyCosts() accepts a synchronous or asynchronous pricing resolver. Calls without usage still count in the per-model breakdown; models without pricing retain their usage but have a null cost and do not contribute to the total. Model maps use their own entries only; IDs such as constructor and __proto__ are treated as ordinary model IDs.

Cache ambiguity and strict validation

When constructing usage manually, supply uncachedInputTokens whenever either cachedInputTokens or cacheCreationInputTokens is present, including a zero cache count. The uncached count excludes both cache reads and cache creation. OpenAI's prompt count includes cache reads; Anthropic's input count excludes cache reads and writes. A flattened object alone cannot tell the estimator which interpretation you intended.

For example, with input at $1, output at $2, and cache reads at $0.10 per million tokens:

| Usage interpretation | Input | Cached | Uncached | Total for 50 output tokens | | ------------------------ | ----- | ------ | -------- | -------------------------- | | OpenAI inclusive input | 1,000 | 400 | 600 | $0.00074 | | Anthropic additive input | 1,000 | 400 | 1,000 | $0.00114 |

The default still treats inputTokens as uncached when uncachedInputTokens is missing. The ambiguous manual object { inputTokens: 1000, outputTokens: 50, cachedInputTokens: 400 } therefore retains its $0.00114 estimate. The result now includes a single structured warning: warnings: [{ code: "AMBIGUOUS_CACHED_INPUT", message: "..." }]. Inspect cost?.warnings from estimateUsdCost() or result.warnings from tallyCosts(). Warnings are omitted for unambiguous usage and deduplicated to one entry per result, even when a tally contains many ambiguous calls. They are returned on each affected invocation, never written to the console or stderr.

Opt into rejection with requireExplicitUncachedInputTokens: true on either function:

import {
  estimateUsdCost,
  normalizeTokenUsage,
  pricingFromUsdPerMillion,
  tallyCosts,
} from "tokentally";

const pricing = pricingFromUsdPerMillion({
  inputUsdPerMillion: 1,
  outputUsdPerMillion: 2,
  cachedInputUsdPerMillion: 0.1,
});
const usage = normalizeTokenUsage({
  prompt_tokens: 1000,
  completion_tokens: 50,
  prompt_tokens_details: { cached_tokens: 400 },
});
const cost = estimateUsdCost({ usage, pricing, requireExplicitUncachedInputTokens: true });
// cost.totalUsd is 0.00074; no warnings.
const result = await tallyCosts({
  calls: [{ model: "example/model", usage }],
  resolvePricing: () => pricing,
  requireExplicitUncachedInputTokens: true,
});
// result.total.totalUsd is 0.00074; no warnings.

Strict estimation throws a TypeError for ambiguous input; strict tallying rejects its promise. The error explains how to normalize the original provider payload or pass an explicit uncached count. Validation checks every original call before aggregation and pricing resolution, so mixing normalized and ambiguous manual calls cannot hide the ambiguity. It also applies when pricing is missing. Default estimation with missing usage or pricing still returns null; a default tally can return warnings even when its total is null because no pricing was found.

Normalize the original provider payload, not an already flattened ambiguous object: lost provider semantics cannot be recovered. For manual OpenAI usage in the table above, passing uncachedInputTokens: 600 is sufficient; manual Anthropic usage needs uncachedInputTokens: 1000.

Ambiguous manual usage is deprecated. Strict validation is planned to become the default in a future compatibility-changing release; its major/minor version and migration timing remain undecided. This release keeps strict validation opt-in and preserves existing numeric totals.

Load catalog pricing in Node.js

Import catalog helpers from tokentally/node. The LiteLLM loader uses a seven-day disk cache at $HOME/.tokentally/cache; set TOKENTALLY_CACHE_DIR to put it elsewhere. Disk caching is optional: when no cache directory is configured or saving fails, a successful fetch still returns network pricing. Failed refreshes fall back to a readable cached catalog.

import { loadLiteLlmCatalog, resolveLiteLlmPricing } from "tokentally/node";

const { catalog, source } = await loadLiteLlmCatalog({ env: process.env, fetchImpl: fetch });
const pricing = catalog ? resolveLiteLlmPricing(catalog, "openai/gpt-5.2") : null;
console.log({ source, pricing });

OpenRouter requires an API key supplied by your application. Its loader rejects malformed response collections before caching, skips malformed model rows, and caches validated results for five minutes from fetch completion:

import { resolvePricingFromMap } from "tokentally";
import { fetchOpenRouterPricingMap } from "tokentally/node";

const apiKey = process.env.OPENROUTER_API_KEY;
if (!apiKey) throw new Error("Set OPENROUTER_API_KEY");
const map = await fetchOpenRouterPricingMap({
  apiKey,
  fetchImpl: fetch,
});
const pricing = resolvePricingFromMap(map, "openai/gpt-5.2");

Catalog prices and limits can change. tokentally estimates cost from the source you provide; it does not reconcile provider invoices.

Development

pnpm install
pnpm check

pnpm check runs formatting, linting, type checks, tests with coverage, the package build, and a smoke test of the built public exports. CI runs the full gate on Node.js 24 and 26.

License

MIT