llm-tally 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. This project follows [Semantic Versioning](https://semver.org/).
4
+
5
+ ## [0.1.0] - 2026-10-17
6
+
7
+ ### Added
8
+
9
+ - Prices for 138 models from 14 providers: OpenAI, Anthropic, Google, xAI, DeepSeek, Mistral, Alibaba Qwen, Moonshot Kimi, Z.ai GLM, MiniMax, Perplexity, Cohere, Amazon Nova and AI21, with cache, long-prompt and per-request prices (as of 2026-10-17).
10
+ - Reasoning effort (`none`, `low`, `medium`, `high`, `max`) in the library (`effort`, `reasoningTokens`), the CLI (`--effort`) and the web calculator; models that don't reason are not charged thinking tokens.
11
+ - `estimateCost`, `compareCosts`, `costFromUsage` and `countTokens`, with one-line shorthands such as `estimateCost(text, "sonnet")`.
12
+ - Short model names (`opus`, `sonnet`, `grok`, `kimi`, `glm`, ...) and support for router, cloud and dated IDs (`x-ai/grok-4.7`, `amazon.nova-pro-v1:0`, `mistral-large-latest`).
13
+ - `llm-tally` CLI with `--model`, `--provider`, `--all`, `--effort`, `--output-tokens`, `--requests`, `--cached`, `--file`, `--json` and a searchable `--list`.
14
+ - A web calculator: `npx llm-tally ui` opens a self-contained page that works offline, also hosted at https://acus9345.github.io/llm-tally/.
15
+ - `LlmTallyError` with error codes and fix-it messages.
16
+ - Works in Node.js 22+, Bun, Deno and browsers; CI covers Windows, macOS and Linux.
17
+
18
+ [0.1.0]: https://github.com/acus9345/llm-tally/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 acus9345
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,153 @@
1
- # Temporary Holding Version
1
+ # llm-tally
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Estimate what a prompt costs on 138 LLMs from 14 providers (OpenAI, Anthropic, Google, xAI, DeepSeek, Mistral, Qwen, Kimi, GLM, MiniMax, Perplexity, Cohere, Amazon Nova and AI21) in your browser, on the command line, or from code.
4
+
5
+ - **Reasoning-aware.** Pick a reasoning effort (none → max) and thinking tokens are added only to models that actually reason.
6
+ - **Easy names.** API IDs (`claude-sonnet-5-5`), short names (`sonnet`, `grok`, `kimi`, `glm`) or router/cloud IDs (`x-ai/grok-4.7`, `amazon.nova-pro-v1:0`).
7
+ - **Zero dependencies, typed, offline.** Runs in Node.js, Bun, Deno and browsers.
8
+
9
+ ## Web calculator
10
+
11
+ Open **https://acus9345.github.io/llm-tally/**: paste a prompt (or drop a text file), pick a reasoning effort and a number of requests, and compare every model, cheapest first. Nothing you type is uploaded.
12
+
13
+ The same page works offline from the command line:
14
+
15
+ ```sh
16
+ npx llm-tally ui
17
+ ```
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ npm install llm-tally
23
+ ```
24
+
25
+ Run it in your project folder (in a brand-new empty folder, run `npm init -y` first). Requires Node.js 22+, Bun or Deno.
26
+
27
+ ## Command line
28
+
29
+ ```sh
30
+ npx llm-tally "Summarize the plot of Hamlet in three sentences." -o 300 -e medium
31
+ ```
32
+
33
+ ```text
34
+ MODEL PROVIDER INPUT TOKENS OUTPUT TOKENS COST (USD)
35
+ qwen3.8-flash alibaba 9 4,300 $0.00202
36
+ gpt-6-luna openai 9 4,300 $0.00215
37
+ claude-haiku-5-5 anthropic 12 4,300 $0.00215
38
+ ...
39
+ kimi-k3 moonshot 9 4,300 $0.0645
40
+ claude-opus-5-5 anthropic 12 4,300 $0.0860
41
+
42
+ Cheapest: qwen3.8-flash ($0.00202), 43x cheaper than claude-opus-5-5.
43
+ Token counts are estimates (usually within 15%). Prices as of 2026-10-17.
44
+ ```
45
+
46
+ `-o 300` is the visible answer; `-e medium` adds about 4,000 thinking tokens to models that reason.
47
+
48
+ | Option | Meaning |
49
+ | --- | --- |
50
+ | `-m, --model <id>` | Model(s) to price, by ID or short name. Repeat or comma-separate. |
51
+ | `-p, --provider <name>` | Compare every model from a provider. `--all` compares everything. |
52
+ | `-e, --effort <level>` | Reasoning effort: `none` (default), `low`, `medium`, `high`, `max`. |
53
+ | `-o, --output-tokens <n>` | Length of the visible answer: `500`, `2k`. Default `0`. |
54
+ | `-n, --requests <n>` | Also show the total for this many requests: `10k`, `1m`. |
55
+ | `-c, --cached <n>` | Input tokens served from the prompt cache. |
56
+ | `-f, --file <path>` | Read the prompt from a file (UTF-8 or UTF-16). Text can also be piped in. |
57
+ | `-l, --list [filter]` | List models, prices and short names; filter by provider or name. |
58
+ | `--json` | Machine-readable output. |
59
+ | `ui` | Open the web calculator (`--no-open` prints its path instead). |
60
+
61
+ ```sh
62
+ npx llm-tally -f prompt.txt -m sonnet,gpt-5.5 -e high -o 1k
63
+ npx llm-tally -f prompt.txt -p anthropic -e medium -n 10k
64
+ Get-Content prompt.txt | npx llm-tally -m kimi,glm,qwen3.8-max # cat on macOS/Linux
65
+ npx llm-tally --list qwen
66
+ ```
67
+
68
+ Exit codes: `0` success, `1` unknown model, provider or value, `2` bad command-line usage.
69
+
70
+ ## Library
71
+
72
+ Save as `tally.mjs` and run `node tally.mjs`:
73
+
74
+ ```js
75
+ import { compareCosts, estimateCost, formatUsd } from "llm-tally";
76
+
77
+ const prompt = "Summarize the plot of Hamlet in three sentences.";
78
+
79
+ // One model, one line:
80
+ console.log(formatUsd(estimateCost(prompt, "sonnet").totalCost));
81
+
82
+ // Several models: a 300-token answer with medium reasoning effort, cheapest first.
83
+ const models = ["gpt-5.5", "sonnet", "gemini-flash", "grok", "deepseek-flash", "kimi", "gpt-4.1"];
84
+ for (const c of compareCosts(prompt, { models, outputTokens: 300, effort: "medium" })) {
85
+ console.log(c.model.padEnd(18), formatUsd(c.totalCost));
86
+ }
87
+ ```
88
+
89
+ ```text
90
+ $0.0000240
91
+ gpt-4.1 $0.00242
92
+ deepseek-flash $0.00516
93
+ gemini-3.8-flash $0.0161
94
+ grok-4.7 $0.0258
95
+ claude-sonnet-5-5 $0.0430
96
+ kimi-k3 $0.0645
97
+ gpt-5.5 $0.129
98
+ ```
99
+
100
+ `gpt-4.1` doesn't reason, so it only pays for the 300-token answer. CommonJS works too: `const { estimateCost } = require("llm-tally");`
101
+
102
+ ### API
103
+
104
+ All amounts are US dollars. Token counts from `countTokens`, `estimateCost` and `compareCosts` are estimates; `costFromUsage` is exact because it uses your provider's reported counts.
105
+
106
+ - **`estimateCost(input, model | options)`**: price one request. `input` is a string or chat messages (`{ role, content }`). Options: `model`, `outputTokens` (answer length), `effort` (`"none"`…`"max"`) or `reasoningTokens` (exact), `cachedInputTokens`. Returns `{ model, provider, inputTokens, outputTokens, reasoningTokens, inputCost, cachedInputCost, outputCost, requestCost, totalCost, longContext, warnings, ... }`.
107
+ - **`compareCosts(input, models | options)`**: the same for several models, cheapest first. Options add `models` or `providers`.
108
+ - **`costFromUsage({ model, inputTokens, outputTokens?, cachedInputTokens? })`**: exact cost from an API response's usage numbers. `inputTokens` is the total input including cached tokens (for Anthropic: `input_tokens + cache_read_input_tokens`).
109
+ - **`countTokens(input, model?)`**: estimated tokens, imitating that model's tokenizer.
110
+ - **`getModel(id)`**, **`findModel(id)`**, **`listModels(provider?)`**: look up prices and metadata (`reasoning: false` marks models that never think).
111
+ - **`registerModel({ id, provider, input, output, cachedInput?, reasoning?, requestFee?, aliases?, ... })`**: add a model or override a price (USD per 1M tokens).
112
+ - **`EFFORT_REASONING_TOKENS`**, **`PRICES_AS_OF`**, **`formatUsd(amount)`**.
113
+
114
+ Mistakes throw an `LlmTallyError` with a `code` (`UNKNOWN_MODEL`, `INVALID_INPUT`, `INVALID_OPTION`) and a message that says how to fix it, e.g. `Unknown model "gpt-55". Did you mean "gpt-5.5", "gpt-5", "gpt-5.4"?`.
115
+
116
+ ## Providers and short names
117
+
118
+ | Provider | Models | Short names |
119
+ | --- | --- | --- |
120
+ | OpenAI | 41 (GPT-6, GPT-5.x, GPT-4.x, o-series, legacy) | — use the API ID, e.g. `gpt-5.5` |
121
+ | Anthropic | 16 (Claude Fable, Opus, Sonnet, Haiku) | `fable`, `opus`, `sonnet`, `haiku` |
122
+ | Google | 10 (Gemini 4, 3.x, 2.5) | `gemini-pro`, `gemini-flash`, `gemini-flash-lite` |
123
+ | xAI | 8 (Grok 4.x) | `grok` |
124
+ | DeepSeek | 2 (V4 Pro, Flash) | `deepseek-pro` |
125
+ | Mistral | 8 (Large, Medium, Small, Ministral, Codestral) | `mistral-large-latest` and other `-latest` IDs |
126
+ | Alibaba Qwen | 13 (Qwen 3.x Max, Plus, Flash, Coder) | — |
127
+ | Moonshot Kimi | 4 (K3, K2.x) | `kimi` |
128
+ | Z.ai GLM | 16 (GLM-5.x, GLM-4.x) | `glm` |
129
+ | MiniMax | 6 (M3, M2.x) | `minimax` |
130
+ | Perplexity | 3 (Sonar, Sonar Pro, Sonar Reasoning Pro) | — |
131
+ | Cohere | 4 (Command A, Command R) | `command-a`, `command-r` |
132
+ | Amazon | 5 (Nova 2 Lite, Premier, Pro, Lite, Micro) | Bedrock IDs like `amazon.nova-pro-v1:0` |
133
+ | AI21 | 2 (Jamba Large, Mini) | — |
134
+
135
+ Short names point at the newest model in that line. `npx llm-tally --list` shows everything. IDs with provider prefixes (OpenRouter, LiteLLM, Vertex AI, Bedrock) and date suffixes (`gpt-4o-2024-08-06`, `claude-sonnet-4-5-20250929`) also work.
136
+
137
+ ## How accurate is it?
138
+
139
+ - **Prices** come from each provider's public pricing page (standard tier, as of `PRICES_AS_OF`), including long-prompt tiers. Not included: batch discounts, cache-write surcharges, image/audio pricing. DeepSeek uses peak-hour rates (off-peak is half), Mistral Large its current sale price, Qwen the International region, Perplexity the low search-context request fee.
140
+ - **Thinking tokens** per effort level are typical values (low 1k, medium 4k, high 12k, max 32k); real usage varies a lot by task. Pass `reasoningTokens` when you know yours.
141
+ - **Token counts** are tuned against OpenAI's o200k tokenizer: usually within ~10% for English, code and JSON, ~20% for other languages, and rougher for providers other than OpenAI, Anthropic and Google. For exact counts, use your provider's token-counting endpoint and `costFromUsage`.
142
+
143
+ ## Troubleshooting
144
+
145
+ - **`Unknown model "..."`**: check `npx llm-tally --list <part of the name>`. New models arrive in new versions; until then use `registerModel()`.
146
+ - **`No input`**: pass the prompt in quotes, use `--file`, or pipe text in.
147
+ - **`$` in PowerShell text**: use single quotes (`'costs $5'`) or `--file`.
148
+ - **Non-English text piped in Windows PowerShell 5.1 becomes `?`**: use `--file`, or run `$OutputEncoding = [Text.Encoding]::UTF8` first.
149
+ - **`ERR_REQUIRE_ESM` or syntax errors**: upgrade to Node.js 22 or newer.
150
+
151
+ ## License
152
+
153
+ [MIT](LICENSE)
@@ -0,0 +1,21 @@
1
+ import { type ModelInfo, type Provider } from "./models.js";
2
+ /**
3
+ * Looks up a model by ID or short name. Accepts aliases (`sonnet`, `grok`, `gemini-flash`),
4
+ * provider prefixes (`openai/gpt-5.5`, `x-ai/grok-4.7`, `models/gemini-2.5-pro`),
5
+ * Bedrock IDs (`anthropic.claude-opus-5-5`) and dated snapshots (`gpt-4o-2024-08-06`, `mistral-large-latest`).
6
+ *
7
+ * @throws {LlmTallyError} `UNKNOWN_MODEL` with the closest known IDs when nothing matches.
8
+ */
9
+ export declare function getModel(id: string): ModelInfo;
10
+ /** Like {@link getModel} but returns `undefined` instead of throwing. */
11
+ export declare function findModel(id: string): ModelInfo | undefined;
12
+ /** Lists known models, optionally only those from one provider. */
13
+ export declare function listModels(provider?: Provider): ModelInfo[];
14
+ /**
15
+ * Adds a model or replaces a built-in one (for example to apply a negotiated discount).
16
+ * Prices are USD per 1 million tokens.
17
+ *
18
+ * @example
19
+ * registerModel({ id: "my-finetune", provider: "openai", input: 3, output: 12 });
20
+ */
21
+ export declare function registerModel(model: ModelInfo): void;
@@ -0,0 +1,132 @@
1
+ import { describe, LlmTallyError } from "./errors.js";
2
+ import { BUILTIN_MODELS } from "./models.js";
3
+ const registry = new Map();
4
+ // Lower-case alias ("sonnet", "mistral-small-2603") -> model ID.
5
+ const aliases = new Map();
6
+ for (const model of BUILTIN_MODELS)
7
+ add(model);
8
+ function add(model) {
9
+ registry.set(model.id, model);
10
+ // Lookups are case-insensitive, so "minimax-m3" finds "MiniMax-M3".
11
+ for (const alias of [model.id, ...(model.aliases ?? [])])
12
+ aliases.set(alias.toLowerCase(), model.id);
13
+ }
14
+ function lookup(key) {
15
+ const viaAlias = aliases.get(key);
16
+ return registry.get(key) ?? (viaAlias === undefined ? undefined : registry.get(viaAlias));
17
+ }
18
+ // Prefixes that other platforms (Vertex, Bedrock, OpenRouter, LiteLLM) put in front of the same model ID.
19
+ const PREFIXES = [
20
+ /^models\//,
21
+ /^(openai|anthropic|google|gemini|vertex_ai|bedrock|xai|x-ai|deepseek|mistral|mistralai)\//,
22
+ /^(qwen|alibaba|dashscope|moonshot|moonshotai|zai|z-ai|zhipu|minimax|perplexity|cohere|amazon|ai21)\//,
23
+ /^(us\.|eu\.|apac\.|global\.)?(anthropic|amazon|cohere|ai21)\./,
24
+ ];
25
+ // Dated snapshots: "gpt-4o-2024-08-06", "claude-haiku-4-5-20251001", "claude-opus-4-5@20251101", "-latest".
26
+ const SUFFIXES = [/-\d{4}-\d{2}-\d{2}$/, /[-@]\d{8}$/, /-latest$/, /-v\d+(:\d+)?$/, /-preview-\d{2}-\d{4}$/];
27
+ function normalize(id) {
28
+ let key = id.trim().toLowerCase();
29
+ for (const prefix of PREFIXES)
30
+ key = key.replace(prefix, "");
31
+ return key;
32
+ }
33
+ /**
34
+ * Looks up a model by ID or short name. Accepts aliases (`sonnet`, `grok`, `gemini-flash`),
35
+ * provider prefixes (`openai/gpt-5.5`, `x-ai/grok-4.7`, `models/gemini-2.5-pro`),
36
+ * Bedrock IDs (`anthropic.claude-opus-5-5`) and dated snapshots (`gpt-4o-2024-08-06`, `mistral-large-latest`).
37
+ *
38
+ * @throws {LlmTallyError} `UNKNOWN_MODEL` with the closest known IDs when nothing matches.
39
+ */
40
+ export function getModel(id) {
41
+ if (typeof id !== "string" || id.trim() === "") {
42
+ throw new LlmTallyError("INVALID_OPTION", `model must be a model ID string like "gpt-5.5", but got ${describe(id)}. Run \`npx llm-tally --list\` to see known models.`);
43
+ }
44
+ const found = findModel(id);
45
+ if (found)
46
+ return found;
47
+ const suggestions = closestIds(normalize(id), 3);
48
+ throw new LlmTallyError("UNKNOWN_MODEL", `Unknown model "${id}". Did you mean ${suggestions.map((s) => `"${s}"`).join(", ")}? ` +
49
+ "Run `npx llm-tally --list` to see all models, or add your own with registerModel().");
50
+ }
51
+ /** Like {@link getModel} but returns `undefined` instead of throwing. */
52
+ export function findModel(id) {
53
+ if (typeof id !== "string")
54
+ return undefined;
55
+ const exact = registry.get(id);
56
+ if (exact)
57
+ return exact;
58
+ let key = normalize(id);
59
+ const direct = lookup(key);
60
+ if (direct)
61
+ return direct;
62
+ // Strip one suffix at a time ("...-20240620-v1:0" needs two passes) until something matches.
63
+ let changed = true;
64
+ while (changed) {
65
+ changed = false;
66
+ for (const suffix of SUFFIXES) {
67
+ const stripped = key.replace(suffix, "");
68
+ if (stripped !== key) {
69
+ key = stripped;
70
+ changed = true;
71
+ const hit = lookup(key);
72
+ if (hit)
73
+ return hit;
74
+ }
75
+ }
76
+ }
77
+ return undefined;
78
+ }
79
+ /** Lists known models, optionally only those from one provider. */
80
+ export function listModels(provider) {
81
+ const all = [...registry.values()];
82
+ return provider === undefined ? all : all.filter((m) => m.provider === provider.toLowerCase());
83
+ }
84
+ /**
85
+ * Adds a model or replaces a built-in one (for example to apply a negotiated discount).
86
+ * Prices are USD per 1 million tokens.
87
+ *
88
+ * @example
89
+ * registerModel({ id: "my-finetune", provider: "openai", input: 3, output: 12 });
90
+ */
91
+ export function registerModel(model) {
92
+ if (model === null || typeof model !== "object") {
93
+ throw new LlmTallyError("INVALID_INPUT", `registerModel() needs a model object, but got ${describe(model)}.`);
94
+ }
95
+ if (typeof model.id !== "string" || model.id.trim() === "") {
96
+ throw new LlmTallyError("INVALID_OPTION", 'registerModel(): "id" must be a non-empty string, e.g. "my-model".');
97
+ }
98
+ if (typeof model.provider !== "string" || model.provider === "") {
99
+ throw new LlmTallyError("INVALID_OPTION", `registerModel("${model.id}"): "provider" must be a string, e.g. "openai".`);
100
+ }
101
+ for (const field of ["input", "output", "cachedInput"]) {
102
+ const value = model[field];
103
+ if (value === undefined && field === "cachedInput")
104
+ continue;
105
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
106
+ throw new LlmTallyError("INVALID_OPTION", `registerModel("${model.id}"): "${field}" must be a price in USD per 1M tokens (a number >= 0), but got ${describe(value)}.`);
107
+ }
108
+ }
109
+ if (model.aliases !== undefined && !Array.isArray(model.aliases)) {
110
+ throw new LlmTallyError("INVALID_OPTION", `registerModel("${model.id}"): "aliases" must be an array of strings.`);
111
+ }
112
+ add({ ...model, provider: model.provider.toLowerCase() });
113
+ }
114
+ function closestIds(key, count) {
115
+ return [...aliases.keys()]
116
+ .map((id) => ({ id, d: distance(key, id) }))
117
+ .sort((a, b) => a.d - b.d)
118
+ .slice(0, count)
119
+ .map((x) => x.id);
120
+ }
121
+ /** Levenshtein edit distance. */
122
+ function distance(a, b) {
123
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
124
+ for (let i = 1; i <= a.length; i++) {
125
+ const cur = [i];
126
+ for (let j = 1; j <= b.length; j++) {
127
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
128
+ }
129
+ prev = cur;
130
+ }
131
+ return prev[b.length];
132
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "./run.js";
3
+ process.exitCode = await main(process.argv.slice(2));
@@ -0,0 +1,16 @@
1
+ /** Machine-readable reason for an {@link LlmTallyError}. */
2
+ export type LlmTallyErrorCode = "UNKNOWN_MODEL" | "INVALID_INPUT" | "INVALID_OPTION";
3
+ /**
4
+ * The only error type llm-tally throws for caller mistakes.
5
+ *
6
+ * `message` says what went wrong and how to fix it; `code` lets you branch on the cause.
7
+ */
8
+ export declare class LlmTallyError extends Error {
9
+ /** Why the call failed. */
10
+ readonly code: LlmTallyErrorCode;
11
+ constructor(code: LlmTallyErrorCode, message: string);
12
+ }
13
+ /** Throws unless `value` is a finite number >= 0. */
14
+ export declare function assertTokenCount(name: string, value: unknown): asserts value is number;
15
+ /** Short, safe description of a value for error messages. */
16
+ export declare function describe(value: unknown): string;
package/dist/errors.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The only error type llm-tally throws for caller mistakes.
3
+ *
4
+ * `message` says what went wrong and how to fix it; `code` lets you branch on the cause.
5
+ */
6
+ export class LlmTallyError extends Error {
7
+ /** Why the call failed. */
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.name = "LlmTallyError";
12
+ this.code = code;
13
+ }
14
+ }
15
+ /** Throws unless `value` is a finite number >= 0. */
16
+ export function assertTokenCount(name, value) {
17
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
18
+ throw new LlmTallyError("INVALID_OPTION", `${name} must be a number of tokens >= 0, but got ${describe(value)}. Pass a whole number like 500.`);
19
+ }
20
+ }
21
+ /** Short, safe description of a value for error messages. */
22
+ export function describe(value) {
23
+ if (value === null)
24
+ return "null";
25
+ if (typeof value === "string")
26
+ return value.length > 40 ? `"${value.slice(0, 40)}…"` : `"${value}"`;
27
+ if (Array.isArray(value))
28
+ return "an array";
29
+ if (typeof value === "object")
30
+ return "an object";
31
+ return String(value);
32
+ }
@@ -0,0 +1,136 @@
1
+ import { type ReasoningEffort } from "./models.js";
2
+ /** One part of a multi-part message. Only text parts are counted. */
3
+ export interface ContentPart {
4
+ type: string;
5
+ text?: string;
6
+ }
7
+ /** A chat message in the OpenAI / Anthropic / Gemini-style shape. */
8
+ export interface ChatMessage {
9
+ role: string;
10
+ content: string | ContentPart[];
11
+ }
12
+ /** What you can measure: plain text, or a list of chat messages. */
13
+ export type TallyInput = string | readonly ChatMessage[];
14
+ /** Options for {@link countTokens}. */
15
+ export interface CountOptions {
16
+ /** Model whose tokenizer to imitate. Defaults to OpenAI's o200k (GPT-4o and newer). */
17
+ model?: string;
18
+ }
19
+ /** Reply-related options shared by {@link estimateCost} and {@link compareCosts}. */
20
+ export interface ReplyOptions {
21
+ /** Length of the visible answer, in tokens. Defaults to 0. */
22
+ outputTokens?: number;
23
+ /**
24
+ * How hard reasoning models think: `"none"`, `"low"`, `"medium"`, `"high"` or `"max"`.
25
+ * Adds the typical thinking tokens for that level ({@link EFFORT_REASONING_TOKENS}); models that
26
+ * cannot think get none. Defaults to `"none"`.
27
+ */
28
+ effort?: ReasoningEffort;
29
+ /** Exact thinking tokens per request, instead of `effort`. */
30
+ reasoningTokens?: number;
31
+ /** How many of the input tokens are read from the prompt cache. Defaults to 0. */
32
+ cachedInputTokens?: number;
33
+ }
34
+ /** Options for {@link estimateCost}. */
35
+ export interface EstimateOptions extends ReplyOptions {
36
+ /** Model ID to price, e.g. `"gpt-5.5"` or `"sonnet"`. */
37
+ model: string;
38
+ }
39
+ /** Options for {@link compareCosts}. */
40
+ export interface CompareOptions extends ReplyOptions {
41
+ /** Models to compare. Defaults to a few current models from each provider. */
42
+ models?: readonly string[];
43
+ /** Compare every known model from these providers instead, e.g. `["anthropic", "xai"]`. */
44
+ providers?: readonly string[];
45
+ }
46
+ /** Token counts reported by a provider's API, for exact pricing with {@link costFromUsage}. */
47
+ export interface Usage {
48
+ /** Model ID that served the request. */
49
+ model: string;
50
+ /**
51
+ * All input tokens, including any read from cache.
52
+ * (Anthropic reports cache reads separately: pass `input_tokens + cache_read_input_tokens`.)
53
+ */
54
+ inputTokens: number;
55
+ /** Output tokens, including reasoning/thinking tokens. Defaults to 0. */
56
+ outputTokens?: number;
57
+ /** Input tokens served from the prompt cache. Defaults to 0. */
58
+ cachedInputTokens?: number;
59
+ }
60
+ /** A priced request. All costs are in US dollars. */
61
+ export interface CostEstimate {
62
+ /** Model ID as found in the catalog. */
63
+ model: string;
64
+ /** Provider that serves the model. */
65
+ provider: string;
66
+ /** Total input tokens (including cached ones). */
67
+ inputTokens: number;
68
+ /** Input tokens priced at the cache rate. */
69
+ cachedInputTokens: number;
70
+ /** Billed output tokens: the visible answer plus any thinking. */
71
+ outputTokens: number;
72
+ /** The part of `outputTokens` spent thinking (0 for models that don't reason). */
73
+ reasoningTokens: number;
74
+ /** Cost of the uncached input tokens. */
75
+ inputCost: number;
76
+ /** Cost of the cached input tokens. */
77
+ cachedInputCost: number;
78
+ /** Cost of the output tokens, including thinking. */
79
+ outputCost: number;
80
+ /** Fixed per-request fee (for example a web-search fee); usually 0. */
81
+ requestCost: number;
82
+ /** `inputCost + cachedInputCost + outputCost + requestCost`. */
83
+ totalCost: number;
84
+ /** Always `"USD"`. */
85
+ currency: "USD";
86
+ /** `true` when the input token count is an estimate rather than reported by the API. */
87
+ approximate: boolean;
88
+ /** `true` when the long-context price tier was applied. */
89
+ longContext: boolean;
90
+ /** Things worth knowing about this estimate (for example, input larger than the context window). */
91
+ warnings: string[];
92
+ }
93
+ /**
94
+ * Estimates how many tokens `input` uses.
95
+ *
96
+ * @param options A model ID (`"sonnet"`) or `{ model }`. Without a model, counts like GPT-4o and newer.
97
+ * @example
98
+ * countTokens("Hello, world!"); // 4
99
+ * countTokens(messages, "claude-opus-5-5");
100
+ */
101
+ export declare function countTokens(input: TallyInput, options?: CountOptions | string): number;
102
+ /**
103
+ * Estimates what sending `input` to `model` costs, in US dollars.
104
+ *
105
+ * @param options A model ID (`"gpt-5.5"`, `"sonnet"`) or `{ model, outputTokens?, effort?, cachedInputTokens? }`.
106
+ * @example
107
+ * estimateCost("Summarize this article...", "gpt-5.5").totalCost;
108
+ * estimateCost("Summarize this article...", { model: "sonnet", outputTokens: 300, effort: "medium" });
109
+ */
110
+ export declare function estimateCost(input: TallyInput, options: EstimateOptions | string): CostEstimate;
111
+ /**
112
+ * Prices the same request on several models, cheapest first.
113
+ *
114
+ * @param options A list of model IDs, or `{ models?, providers?, outputTokens?, effort?, cachedInputTokens? }`.
115
+ * @example
116
+ * compareCosts(prompt, ["gpt-5.5", "sonnet", "grok"]);
117
+ * compareCosts(prompt, { providers: ["anthropic"], outputTokens: 500 });
118
+ */
119
+ export declare function compareCosts(input: TallyInput, options?: CompareOptions | readonly string[]): CostEstimate[];
120
+ /**
121
+ * Exact cost of a finished request, from the token counts in the API response.
122
+ *
123
+ * @example
124
+ * costFromUsage({ model: "gpt-5.5", inputTokens: res.usage.input_tokens, outputTokens: res.usage.output_tokens });
125
+ */
126
+ export declare function costFromUsage(usage: Usage): CostEstimate;
127
+ /**
128
+ * Formats a dollar amount for display: cents from $1 up, three significant digits below,
129
+ * so small costs stay comparable ($0.0100 vs $0.0201).
130
+ *
131
+ * @example
132
+ * formatUsd(12.5); // "$12.50"
133
+ * formatUsd(0.010048); // "$0.0100"
134
+ * formatUsd(0.000123); // "$0.000123"
135
+ */
136
+ export declare function formatUsd(amount: number): string;