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

@tokli/node

v0.2.0

Published

Report-only usage tracking for OpenAI, Anthropic, Gemini, DeepSeek, xAI, Mistral, Qwen, GLM, Kimi and OpenRouter — wrap your client, we read the tokens.

Readme

@tokli/node

Report-only usage tracking for tokli — a cost & usage dashboard for AI APIs. Wrap your existing provider client, keep calling it exactly as before, and tokli reports the token usage in the background. We never see your provider API key and we never sit in your request's critical path.

Supports OpenAI, Anthropic, Gemini, DeepSeek, xAI, Mistral, Qwen, GLM, Kimi and OpenRouter.

Install

pnpm add @tokli/node

openai, @anthropic-ai/sdk and @google/genai are optional peer dependencies — install whichever ones you actually use. Supported majors: openai v4–v7, @anthropic-ai/sdk 0.27+, @google/genai v1–v2.

Quickstart

import OpenAI from "openai";
import { wrapOpenAI } from "@tokli/node";

const client = wrapOpenAI(new OpenAI(), {
  ingestKey: process.env.TOKLI_INGEST_KEY, // or set the env var directly
}).withFeature("checkout");

await client.chat.completions.create({ model: "gpt-5.4", messages });

The returned client behaves exactly like the original: every property we don't instrument passes straight through, withResponse() / asResponse() keep working, and client.withOptions({ ... }) hands back a client that is still instrumented.

Without an ingest key (neither config.ingestKey nor TOKLI_INGEST_KEY), the wrapper is a silent no-op: your app keeps working exactly as before, nothing is reported.

Per-feature attribution

const checkout = client.withFeature("checkout"); // immutable and chainable
await checkout.chat.completions.create({ model: "gpt-5.4", messages });

withFeature returns a new wrapper over the same client — it never mutates the one you passed in, and tags don't nest: calling it twice replaces the tag rather than combining them.

Available wrappers

wrapOpenAI, wrapAnthropic, wrapGemini, wrapDeepSeek, wrapXai, wrapMistral, wrapQwen, wrapGlm, wrapKimi, wrapOpenRouter — all share the same (client, config) => Wrapped<T> signature and .withFeature(tag) chaining shown above. The seven OpenAI-compatible ones take the openai package pointed at the provider's base URL; each wrapper's JSDoc carries the exact URL and that provider's caching quirks.

What gets reported

We instrument the calls that bill tokens, and only those:

| Client | Reported | | --- | --- | | OpenAI & compatible (DeepSeek, xAI, Mistral, Qwen, GLM, Kimi, OpenRouter) | chat.completions.create, chat.completions.parse, responses.create, responses.parse, responses.compact, completions.create (legacy Completions API) | | Anthropic | messages.create, messages.parse | | Gemini | models.generateContent, models.generateContentStream |

Streaming and non-streaming are both covered, as is withOptions(), which re-wraps the client it returns.

What is not reported

These pass through untouched. If you use one, its spend will not appear in tokli — this list is the whole of it. For the namespaces we do instrument, a test enumerates every public method and fails the moment a provider SDK grows one nobody has classified.

| Not instrumented | Why | | --- | --- | | chat.completions.stream(), responses.stream(), messages.stream() | Helper wrappers with their own consumption surface (.finalMessage(), .text_stream, …). Use create({ stream: true }) if you want tokli to see it. | | chat.completions.runTools() | Runs a multi-call tool loop of its own. | | responses.retrieve(), .cancel(), .delete() | Idempotent or administrative — reporting retrieve would bill the same response once per call. | | chat.completions.retrieve/list/update/delete, chat.completions.messages | Manage stored completions; they bill no tokens. | | messages.countTokens(), responses.inputTokens | Counting only; they bill nothing. | | responses.inputItems | Lists what was sent; bills nothing. | | messages.batches (Anthropic) | A separate pipeline whose usage arrives out of band. | | beta.* namespaces (OpenAI and Anthropic) | Moving targets; they get instrumented once they stabilise. | | withRawResponse.* | Returns the raw HTTP response instead of a parsed body. | | client.chats (Gemini) | A stateful helper over generateContent. The official quickstart uses it, so it is the easiest one to trip over — call models.generateContent if you want it counted. | | Gemini models.list, embedContent, generateImages, generateVideos, editImage, upscaleImage | list bills nothing; the rest bill on non-token meters. |

Background responses. responses.create({ background: true }) returns immediately with no usage, so there is nothing to report at that point — the real numbers arrive later through responses.retrieve(), which we deliberately leave uninstrumented (it is idempotent, and reporting it would count the same response every time you polled). A background call that also streams is reported: its terminal event carries the usage. If you rely on non-streaming background calls, report those yourself.

Where our numbers can differ from your invoice

tokli prices your usage from a versioned table, and a few provider pricing modes are not visible in the usage object at all. These are the cases where the dashboard will read low:

| Case | Effect | | --- | --- | | Long-context premiums — OpenAI charges 2x input / 1.5x output on requests over 272K input tokens; xAI and Qwen have their own context tiers | Under-counted: we price at the base tier | | Anthropic 1-hour cache writes — billed at 2x input where 5-minute ones are 1.25x | Under-counted: the event carries a single cache-creation bucket | | Qwen explicit caching — hits bill at 10% where implicit ones bill at 20% | The response does not distinguish them, so explicit hits are priced as implicit | | Anthropic fast mode — same model ID, premium price | Not captured: speed is a request parameter, not usage | | Data residency (+10% on both providers) | Not captured: a request parameter, not usage | | Server-side tools — web search, code execution | Never counted: they bill per call or per hour, not in tokens | | Batch APIs (50% off on both providers) | Batch traffic does not go through these wrappers at all |

Everything else — the per-token prices, the cache read/write multipliers and the per-provider cache arithmetic — is modelled. If a model is missing from the table the event is still stored, flagged as unpriced rather than silently dropped.

Config

| Option | Env var | Default | Notes | | --- | --- | --- | --- | | ingestKey | TOKLI_INGEST_KEY | — | Required. Without it, the wrapper is a no-op. | | endpoint | TOKLI_ENDPOINT | https://api.tokli.dev | Ingest API base URL. | | timeoutMs | TOKLI_TIMEOUT_MS | 2000 | Timeout for the fire-and-forget report request. | | onError | — | no-op | Called if reporting fails; never throws into your code. |

An option beats the environment variable, which beats the default. An unusable value (a typo in an env var) is skipped rather than thrown — a misconfiguration must not take your app down. onError receives (error, reason), where reason is "transport", "no_usage" or "parse"; if your handler itself throws, the error is swallowed rather than surfacing anywhere in your app.

Streaming

For streaming chat completions on OpenAI, DeepSeek and Qwen, pass stream_options: { include_usage: true } so the final SSE chunk includes token usage — without it there's nothing for tokli to report. xAI, GLM, Kimi and OpenRouter send it either way. Mistral takes no stream_options at all — its request schema declares additionalProperties: false and rejects the call with a 422. Anthropic, Gemini and the Responses API need no flag.

If you abandon a stream part-way (break out of the for await), nothing is reported: we can't know what you were billed, and we would rather report nothing than guess.

How it works

Your code keeps calling the provider with your own key. After the response comes back, the SDK reads the usage object already present in it and sends those raw numbers to tokli, fire-and-forget — it never blocks your request and never touches your key. Cost is computed server-side against a versioned price table, not by the SDK.

Reporting can never break your app: if anything in our own path throws — including your onError handler — you still get the provider's response untouched.

Links