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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ venv/
8
+ .env
@@ -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"]