promptpricer 0.1.0__tar.gz
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.
- promptpricer-0.1.0/.gitignore +8 -0
- promptpricer-0.1.0/PKG-INFO +18 -0
- promptpricer-0.1.0/README.md +77 -0
- promptpricer-0.1.0/llm_cost/__init__.py +0 -0
- promptpricer-0.1.0/llm_cost/cli.py +129 -0
- promptpricer-0.1.0/llm_cost/fetch.py +74 -0
- promptpricer-0.1.0/llm_cost/pricing.py +16 -0
- promptpricer-0.1.0/pyproject.toml +30 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: promptpricer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Instant token count and cost estimate across all major LLMs
|
|
5
|
+
Project-URL: Homepage, https://github.com/Sreechandh22/Prompt-cost
|
|
6
|
+
Project-URL: Repository, https://github.com/Sreechandh22/Prompt-cost
|
|
7
|
+
Author: Sreechandh
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: anthropic,cost,gemini,llm,openai,pricing,tokens
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Requires-Dist: pyperclip
|
|
17
|
+
Requires-Dist: rich
|
|
18
|
+
Requires-Dist: tiktoken
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# promptpricer
|
|
2
|
+
|
|
3
|
+
Token count and cost estimate across all major LLMs. One command, no API keys.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
$ promptpricer "You are a helpful assistant. Summarize this document in detail."
|
|
7
|
+
|
|
8
|
+
Prompt: 12 tokens Expected output: 500 tokens
|
|
9
|
+
|
|
10
|
+
Model Input tokens Output tokens Input cost Output cost Total
|
|
11
|
+
──────────────────────────────────────────────────────────────────────────────────────
|
|
12
|
+
gpt-4.1-nano 12 500 <$0.0001 $0.0002 $0.0002
|
|
13
|
+
gemini-2.0-flash 12 500 <$0.0001 $0.0002 $0.0002
|
|
14
|
+
gpt-4o-mini 12 500 <$0.0001 $0.0003 $0.0003
|
|
15
|
+
gpt-4.1-mini 12 500 <$0.0001 $0.0008 $0.0008
|
|
16
|
+
gemini-2.5-flash 12 500 <$0.0001 $0.0013 $0.0013
|
|
17
|
+
o4-mini 12 500 <$0.0001 $0.0022 $0.0022
|
|
18
|
+
claude-haiku-4 12 500 <$0.0001 $0.0025 $0.0025
|
|
19
|
+
gpt-4o 12 500 <$0.0001 $0.0050 $0.0051
|
|
20
|
+
claude-sonnet-4 12 500 <$0.0001 $0.0075 $0.0075
|
|
21
|
+
claude-opus-4 12 500 $0.0002 $0.0375 $0.0377
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Prices are fetched from [LiteLLM](https://github.com/BerriAI/litellm) and cached locally for 24 hours.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install promptpricer
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or from source:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/Sreechandh22/Prompt-cost
|
|
37
|
+
cd Prompt-cost
|
|
38
|
+
pip install -e .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# pass a prompt directly
|
|
45
|
+
promptpricer "your prompt here"
|
|
46
|
+
|
|
47
|
+
# specify expected output tokens (default: 500)
|
|
48
|
+
promptpricer "your prompt" --output 1000
|
|
49
|
+
|
|
50
|
+
# read from a file
|
|
51
|
+
promptpricer --file prompt.txt
|
|
52
|
+
|
|
53
|
+
# read from clipboard
|
|
54
|
+
promptpricer
|
|
55
|
+
|
|
56
|
+
# filter by provider
|
|
57
|
+
promptpricer "your prompt" --filter anthropic
|
|
58
|
+
promptpricer "your prompt" --filter openai
|
|
59
|
+
promptpricer "your prompt" --filter google
|
|
60
|
+
|
|
61
|
+
# show only the N cheapest models
|
|
62
|
+
promptpricer "your prompt" --top 3
|
|
63
|
+
|
|
64
|
+
# output as JSON (pipe into other tools)
|
|
65
|
+
promptpricer "your prompt" --json
|
|
66
|
+
|
|
67
|
+
# force refresh pricing cache
|
|
68
|
+
promptpricer --update
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Providers
|
|
72
|
+
|
|
73
|
+
| Provider | Models |
|
|
74
|
+
|-----------|--------|
|
|
75
|
+
| OpenAI | gpt-4o, gpt-4o-mini, gpt-4.1, gpt-4.1-mini, gpt-4.1-nano, o3, o4-mini |
|
|
76
|
+
| Anthropic | claude-opus-4, claude-sonnet-4, claude-haiku-4 |
|
|
77
|
+
| Google | gemini-2.5-pro, gemini-2.5-flash, gemini-2.0-flash |
|
|
File without changes
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import tiktoken
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich import box
|
|
8
|
+
from llm_cost.fetch import load_prices
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
PROVIDER_ALIASES = {
|
|
13
|
+
"openai": "openai",
|
|
14
|
+
"oai": "openai",
|
|
15
|
+
"anthropic": "anthropic",
|
|
16
|
+
"claude": "anthropic",
|
|
17
|
+
"google": "google",
|
|
18
|
+
"gemini": "google",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def count_tokens(text: str) -> int:
|
|
23
|
+
enc = tiktoken.get_encoding("cl100k_base")
|
|
24
|
+
return len(enc.encode(text))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def format_cost(usd: float) -> str:
|
|
28
|
+
if usd < 0.0001:
|
|
29
|
+
return "<$0.0001"
|
|
30
|
+
return f"${usd:.4f}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_rows(input_tokens: int, output_tokens: int, provider: str = None) -> list:
|
|
34
|
+
models = load_prices()
|
|
35
|
+
rows = []
|
|
36
|
+
for m in models:
|
|
37
|
+
if provider and PROVIDER_ALIASES.get(provider.lower()) != m["provider"]:
|
|
38
|
+
continue
|
|
39
|
+
in_cost = (input_tokens / 1_000_000) * m["in_price"]
|
|
40
|
+
out_cost = (output_tokens / 1_000_000) * m["out_price"]
|
|
41
|
+
total = in_cost + out_cost
|
|
42
|
+
rows.append({
|
|
43
|
+
"model": m["name"],
|
|
44
|
+
"input_tokens": input_tokens,
|
|
45
|
+
"output_tokens": output_tokens,
|
|
46
|
+
"input_cost": in_cost,
|
|
47
|
+
"output_cost": out_cost,
|
|
48
|
+
"total": total,
|
|
49
|
+
})
|
|
50
|
+
rows.sort(key=lambda r: r["total"])
|
|
51
|
+
return rows
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def run(prompt: str, output_tokens: int, as_json: bool = False,
|
|
55
|
+
provider: str = None, top: int = None) -> None:
|
|
56
|
+
input_tokens = count_tokens(prompt)
|
|
57
|
+
rows = build_rows(input_tokens, output_tokens, provider)
|
|
58
|
+
if top:
|
|
59
|
+
rows = rows[:top]
|
|
60
|
+
|
|
61
|
+
if as_json:
|
|
62
|
+
print(json.dumps(rows, indent=2))
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
table = Table(box=box.SIMPLE_HEAD, show_footer=False)
|
|
66
|
+
table.add_column("Model", style="bold white", no_wrap=True)
|
|
67
|
+
table.add_column("Input tokens", justify="right", style="dim")
|
|
68
|
+
table.add_column("Output tokens", justify="right", style="dim")
|
|
69
|
+
table.add_column("Input cost", justify="right")
|
|
70
|
+
table.add_column("Output cost", justify="right")
|
|
71
|
+
table.add_column("Total", justify="right", style="bold green")
|
|
72
|
+
|
|
73
|
+
for r in rows:
|
|
74
|
+
table.add_row(
|
|
75
|
+
r["model"],
|
|
76
|
+
f"{r['input_tokens']:,}",
|
|
77
|
+
f"{r['output_tokens']:,}",
|
|
78
|
+
format_cost(r["input_cost"]),
|
|
79
|
+
format_cost(r["output_cost"]),
|
|
80
|
+
format_cost(r["total"]),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
console.print(f"\n[dim]Prompt:[/dim] {input_tokens:,} tokens [dim]Expected output:[/dim] {output_tokens:,} tokens\n")
|
|
84
|
+
console.print(table)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main() -> None:
|
|
88
|
+
parser = argparse.ArgumentParser(
|
|
89
|
+
prog="llm-cost",
|
|
90
|
+
description="Token count and cost estimate across major LLMs.",
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument("prompt", nargs="?", help="Prompt text (omit to read from clipboard)")
|
|
93
|
+
parser.add_argument("-o", "--output", type=int, default=500, metavar="N",
|
|
94
|
+
help="Expected output tokens (default: 500)")
|
|
95
|
+
parser.add_argument("-f", "--file", metavar="FILE", help="Read prompt from file")
|
|
96
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
97
|
+
parser.add_argument("--filter", metavar="PROVIDER", help="Filter by provider: openai, anthropic, google")
|
|
98
|
+
parser.add_argument("--top", type=int, metavar="N", help="Show only the N cheapest models")
|
|
99
|
+
parser.add_argument("--update", action="store_true", help="Force refresh pricing from remote")
|
|
100
|
+
args = parser.parse_args()
|
|
101
|
+
|
|
102
|
+
if args.update:
|
|
103
|
+
load_prices(force_update=True)
|
|
104
|
+
console.print("[green]Pricing cache updated.[/green]")
|
|
105
|
+
if not args.prompt and not args.file:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
if args.file:
|
|
109
|
+
with open(args.file) as f:
|
|
110
|
+
prompt = f.read()
|
|
111
|
+
elif args.prompt:
|
|
112
|
+
prompt = args.prompt
|
|
113
|
+
else:
|
|
114
|
+
try:
|
|
115
|
+
import pyperclip
|
|
116
|
+
prompt = pyperclip.paste()
|
|
117
|
+
if not prompt.strip():
|
|
118
|
+
console.print("[red]Clipboard is empty. Pass a prompt or use --file.[/red]")
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
console.print("[dim]Reading from clipboard...[/dim]")
|
|
121
|
+
except Exception:
|
|
122
|
+
console.print("[red]No prompt given and clipboard read failed.[/red]")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
run(prompt, args.output, as_json=args.json, provider=args.filter, top=args.top)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
main()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
import urllib.request
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
PRICES_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"
|
|
7
|
+
CACHE_PATH = Path.home() / ".cache" / "llm-cost" / "prices.json"
|
|
8
|
+
CACHE_TTL = 86400 # 24 hours
|
|
9
|
+
|
|
10
|
+
FEATURED = [
|
|
11
|
+
("gpt-4o", "gpt-4o", "openai"),
|
|
12
|
+
("gpt-4o-mini", "gpt-4o-mini", "openai"),
|
|
13
|
+
("gpt-4.1", "gpt-4.1", "openai"),
|
|
14
|
+
("gpt-4.1-mini", "gpt-4.1-mini", "openai"),
|
|
15
|
+
("gpt-4.1-nano", "gpt-4.1-nano", "openai"),
|
|
16
|
+
("o3", "o3", "openai"),
|
|
17
|
+
("o4-mini", "o4-mini", "openai"),
|
|
18
|
+
("claude-opus-4", "claude-opus-4-5", "anthropic"),
|
|
19
|
+
("claude-sonnet-4", "claude-sonnet-4-5", "anthropic"),
|
|
20
|
+
("claude-haiku-4", "claude-haiku-4-5", "anthropic"),
|
|
21
|
+
("gemini-2.5-pro", "gemini/gemini-2.5-pro", "google"),
|
|
22
|
+
("gemini-2.5-flash", "gemini/gemini-2.5-flash", "google"),
|
|
23
|
+
("gemini-2.0-flash", "gemini/gemini-2.0-flash", "google"),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _fetch_remote() -> dict:
|
|
28
|
+
with urllib.request.urlopen(PRICES_URL, timeout=5) as r:
|
|
29
|
+
return json.loads(r.read())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_cache() -> dict | None:
|
|
33
|
+
if not CACHE_PATH.exists():
|
|
34
|
+
return None
|
|
35
|
+
if time.time() - CACHE_PATH.stat().st_mtime > CACHE_TTL:
|
|
36
|
+
return None
|
|
37
|
+
with open(CACHE_PATH) as f:
|
|
38
|
+
return json.load(f)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _save_cache(data: dict) -> None:
|
|
42
|
+
CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
with open(CACHE_PATH, "w") as f:
|
|
44
|
+
json.dump(data, f)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_prices(force_update: bool = False) -> list[dict]:
|
|
48
|
+
raw = None
|
|
49
|
+
|
|
50
|
+
if not force_update:
|
|
51
|
+
raw = _load_cache()
|
|
52
|
+
|
|
53
|
+
if raw is None:
|
|
54
|
+
try:
|
|
55
|
+
raw = _fetch_remote()
|
|
56
|
+
_save_cache(raw)
|
|
57
|
+
except Exception:
|
|
58
|
+
raw = {}
|
|
59
|
+
|
|
60
|
+
models = []
|
|
61
|
+
for display_name, key, provider in FEATURED:
|
|
62
|
+
entry = raw.get(key, {})
|
|
63
|
+
in_price = entry.get("input_cost_per_token", 0) * 1_000_000
|
|
64
|
+
out_price = entry.get("output_cost_per_token", 0) * 1_000_000
|
|
65
|
+
if in_price == 0 and out_price == 0:
|
|
66
|
+
continue
|
|
67
|
+
models.append({
|
|
68
|
+
"name": display_name,
|
|
69
|
+
"provider": provider,
|
|
70
|
+
"in_price": in_price,
|
|
71
|
+
"out_price": out_price,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return models
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Prices in USD per 1M tokens. Update as providers change rates.
|
|
2
|
+
# (name, input $/1M, output $/1M, tokenizer, provider)
|
|
3
|
+
MODELS = [
|
|
4
|
+
("gpt-4o", 2.50, 10.00, "cl100k_base", "openai"),
|
|
5
|
+
("gpt-4o-mini", 0.15, 0.60, "cl100k_base", "openai"),
|
|
6
|
+
("gpt-4.1", 2.00, 8.00, "cl100k_base", "openai"),
|
|
7
|
+
("gpt-4.1-mini", 0.40, 1.60, "cl100k_base", "openai"),
|
|
8
|
+
("o3", 10.00, 40.00, "cl100k_base", "openai"),
|
|
9
|
+
("o4-mini", 1.10, 4.40, "cl100k_base", "openai"),
|
|
10
|
+
("claude-opus-4", 15.00, 75.00, "cl100k_base", "anthropic"),
|
|
11
|
+
("claude-sonnet-4", 3.00, 15.00, "cl100k_base", "anthropic"),
|
|
12
|
+
("claude-haiku-3.5", 0.80, 4.00, "cl100k_base", "anthropic"),
|
|
13
|
+
("gemini-2.0-flash", 0.10, 0.40, "cl100k_base", "google"),
|
|
14
|
+
("gemini-1.5-pro", 1.25, 5.00, "cl100k_base", "google"),
|
|
15
|
+
("gemini-1.5-flash", 0.075, 0.30, "cl100k_base", "google"),
|
|
16
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "promptpricer"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Instant token count and cost estimate across all major LLMs"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Sreechandh" }]
|
|
12
|
+
keywords = ["llm", "cost", "pricing", "tokens", "openai", "anthropic", "gemini"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Topic :: Utilities",
|
|
19
|
+
]
|
|
20
|
+
dependencies = ["tiktoken", "rich", "pyperclip"]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/Sreechandh22/Prompt-cost"
|
|
24
|
+
Repository = "https://github.com/Sreechandh22/Prompt-cost"
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
promptpricer = "llm_cost.cli:main"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["llm_cost"]
|