aitc 1.0.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.
aitc-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: aitc
3
+ Version: 1.0.0
4
+ Summary: Add your description here
5
+ Author-email: Arian Omrani <arian24b@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/arian24b/aitc
8
+ Project-URL: Repository, https://github.com/arian24b/aitc
9
+ Project-URL: Issues, https://github.com/arian24b/aitc/issues
10
+ Keywords: throne,cli,installer,hotspot,proxy
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: requests>=2.34.2
23
+ Requires-Dist: tiktoken>=0.13.0
24
+
25
+ # aitc
26
+
27
+ **AITC** — AI Token Counter. A CLI tool for counting tokens in text/files and estimating API costs across popular LLM models. Supports a diff mode comparing two files.
28
+
29
+ ## Features
30
+
31
+ - Count tokens in inline text, files, or piped stdin
32
+ - Estimate API costs for GPT-4o, GPT-4o mini, Claude Sonnet 4.5, Claude Haiku 4.5, Gemini 1.5 Pro, and Gemini 1.5 Flash
33
+ - Live pricing via OpenRouter API (`--live-prices`)
34
+ - Diff mode to compare token counts between two files (`--diff`)
35
+
36
+ ## Installation
37
+
38
+ ### Via pip
39
+
40
+ ```bash
41
+ pip install aitc
42
+ ```
43
+
44
+ ### Via uv
45
+
46
+ ```bash
47
+ uv tool install aitc
48
+ ```
49
+
50
+ ### From source
51
+
52
+ ```bash
53
+ # Clone the repository
54
+ git clone https://github.com/arian24b/aitc.git
55
+ cd aitc
56
+
57
+ # Sync dependencies with uv
58
+ uv sync
59
+
60
+ # Run directly
61
+ uv run aitc --text "Hello, world!"
62
+
63
+ # Or install in development mode
64
+ uv tool install -e .
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ ### Count tokens from inline text
70
+
71
+ ```bash
72
+ aitc --text "Your text here"
73
+ ```
74
+
75
+ ### Count tokens from a file
76
+
77
+ ```bash
78
+ aitc --file README.md
79
+ ```
80
+
81
+ ### Pipe text via stdin
82
+
83
+ ```bash
84
+ echo "Your text here" | aitc
85
+ cat somefile.txt | aitc
86
+ ```
87
+
88
+ ### Compare two files (diff mode)
89
+
90
+ ```bash
91
+ aitc --diff file1.txt file2.txt
92
+ ```
93
+
94
+ ### Fetch live pricing
95
+
96
+ ```bash
97
+ aitc --text "Hello" --live-prices
98
+ ```
99
+
100
+ ## Development
101
+
102
+ ```bash
103
+ # Clone
104
+ git clone https://github.com/arian24b/aitc.git
105
+ cd aitc
106
+
107
+ # Set up environment with uv
108
+ uv sync
109
+
110
+ # Run linting
111
+ uv run ruff check .
112
+
113
+ # Build
114
+ uv build
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT
aitc-1.0.0/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # aitc
2
+
3
+ **AITC** — AI Token Counter. A CLI tool for counting tokens in text/files and estimating API costs across popular LLM models. Supports a diff mode comparing two files.
4
+
5
+ ## Features
6
+
7
+ - Count tokens in inline text, files, or piped stdin
8
+ - Estimate API costs for GPT-4o, GPT-4o mini, Claude Sonnet 4.5, Claude Haiku 4.5, Gemini 1.5 Pro, and Gemini 1.5 Flash
9
+ - Live pricing via OpenRouter API (`--live-prices`)
10
+ - Diff mode to compare token counts between two files (`--diff`)
11
+
12
+ ## Installation
13
+
14
+ ### Via pip
15
+
16
+ ```bash
17
+ pip install aitc
18
+ ```
19
+
20
+ ### Via uv
21
+
22
+ ```bash
23
+ uv tool install aitc
24
+ ```
25
+
26
+ ### From source
27
+
28
+ ```bash
29
+ # Clone the repository
30
+ git clone https://github.com/arian24b/aitc.git
31
+ cd aitc
32
+
33
+ # Sync dependencies with uv
34
+ uv sync
35
+
36
+ # Run directly
37
+ uv run aitc --text "Hello, world!"
38
+
39
+ # Or install in development mode
40
+ uv tool install -e .
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ### Count tokens from inline text
46
+
47
+ ```bash
48
+ aitc --text "Your text here"
49
+ ```
50
+
51
+ ### Count tokens from a file
52
+
53
+ ```bash
54
+ aitc --file README.md
55
+ ```
56
+
57
+ ### Pipe text via stdin
58
+
59
+ ```bash
60
+ echo "Your text here" | aitc
61
+ cat somefile.txt | aitc
62
+ ```
63
+
64
+ ### Compare two files (diff mode)
65
+
66
+ ```bash
67
+ aitc --diff file1.txt file2.txt
68
+ ```
69
+
70
+ ### Fetch live pricing
71
+
72
+ ```bash
73
+ aitc --text "Hello" --live-prices
74
+ ```
75
+
76
+ ## Development
77
+
78
+ ```bash
79
+ # Clone
80
+ git clone https://github.com/arian24b/aitc.git
81
+ cd aitc
82
+
83
+ # Set up environment with uv
84
+ uv sync
85
+
86
+ # Run linting
87
+ uv run ruff check .
88
+
89
+ # Build
90
+ uv build
91
+ ```
92
+
93
+ ## License
94
+
95
+ MIT
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.4
2
+ Name: aitc
3
+ Version: 1.0.0
4
+ Summary: Add your description here
5
+ Author-email: Arian Omrani <arian24b@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/arian24b/aitc
8
+ Project-URL: Repository, https://github.com/arian24b/aitc
9
+ Project-URL: Issues, https://github.com/arian24b/aitc/issues
10
+ Keywords: throne,cli,installer,hotspot,proxy
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: requests>=2.34.2
23
+ Requires-Dist: tiktoken>=0.13.0
24
+
25
+ # aitc
26
+
27
+ **AITC** — AI Token Counter. A CLI tool for counting tokens in text/files and estimating API costs across popular LLM models. Supports a diff mode comparing two files.
28
+
29
+ ## Features
30
+
31
+ - Count tokens in inline text, files, or piped stdin
32
+ - Estimate API costs for GPT-4o, GPT-4o mini, Claude Sonnet 4.5, Claude Haiku 4.5, Gemini 1.5 Pro, and Gemini 1.5 Flash
33
+ - Live pricing via OpenRouter API (`--live-prices`)
34
+ - Diff mode to compare token counts between two files (`--diff`)
35
+
36
+ ## Installation
37
+
38
+ ### Via pip
39
+
40
+ ```bash
41
+ pip install aitc
42
+ ```
43
+
44
+ ### Via uv
45
+
46
+ ```bash
47
+ uv tool install aitc
48
+ ```
49
+
50
+ ### From source
51
+
52
+ ```bash
53
+ # Clone the repository
54
+ git clone https://github.com/arian24b/aitc.git
55
+ cd aitc
56
+
57
+ # Sync dependencies with uv
58
+ uv sync
59
+
60
+ # Run directly
61
+ uv run aitc --text "Hello, world!"
62
+
63
+ # Or install in development mode
64
+ uv tool install -e .
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ ### Count tokens from inline text
70
+
71
+ ```bash
72
+ aitc --text "Your text here"
73
+ ```
74
+
75
+ ### Count tokens from a file
76
+
77
+ ```bash
78
+ aitc --file README.md
79
+ ```
80
+
81
+ ### Pipe text via stdin
82
+
83
+ ```bash
84
+ echo "Your text here" | aitc
85
+ cat somefile.txt | aitc
86
+ ```
87
+
88
+ ### Compare two files (diff mode)
89
+
90
+ ```bash
91
+ aitc --diff file1.txt file2.txt
92
+ ```
93
+
94
+ ### Fetch live pricing
95
+
96
+ ```bash
97
+ aitc --text "Hello" --live-prices
98
+ ```
99
+
100
+ ## Development
101
+
102
+ ```bash
103
+ # Clone
104
+ git clone https://github.com/arian24b/aitc.git
105
+ cd aitc
106
+
107
+ # Set up environment with uv
108
+ uv sync
109
+
110
+ # Run linting
111
+ uv run ruff check .
112
+
113
+ # Build
114
+ uv build
115
+ ```
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ tokencount.py
4
+ aitc.egg-info/PKG-INFO
5
+ aitc.egg-info/SOURCES.txt
6
+ aitc.egg-info/dependency_links.txt
7
+ aitc.egg-info/entry_points.txt
8
+ aitc.egg-info/requires.txt
9
+ aitc.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aitc = tokencount:main
@@ -0,0 +1,2 @@
1
+ requests>=2.34.2
2
+ tiktoken>=0.13.0
@@ -0,0 +1 @@
1
+ tokencount
@@ -0,0 +1,83 @@
1
+ [project]
2
+ name = "aitc"
3
+ version = "1.0.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ authors = [
8
+ {name = "Arian Omrani", email = "arian24b@gmail.com"}
9
+ ]
10
+ license = "MIT"
11
+ classifiers = [
12
+ "Development Status :: 5 - Production/Stable",
13
+ "Intended Audience :: Developers",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ ]
22
+ keywords = ["throne", "cli", "installer", "hotspot", "proxy"]
23
+ dependencies = [
24
+ "requests>=2.34.2",
25
+ "tiktoken>=0.13.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ aitc = "tokencount:main"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/arian24b/aitc"
33
+ Repository = "https://github.com/arian24b/aitc"
34
+ Issues = "https://github.com/arian24b/aitc/issues"
35
+
36
+ [build-system]
37
+ requires = ["setuptools"]
38
+ build-backend = "setuptools.build_meta"
39
+
40
+ [tool.semantic_release]
41
+ commit_parser = "conventional"
42
+ commit_author = "Arian Omrani <arian24b@gmail.com>"
43
+ branch = "main"
44
+ version_toml = ["pyproject.toml:project.version"]
45
+ upload_to_vcs_release = false
46
+ build_command = "uv build"
47
+
48
+ [tool.ruff]
49
+ line-length = 120
50
+ indent-width = 4
51
+ respect-gitignore = true
52
+ fix = true
53
+ show-fixes = true
54
+
55
+ [tool.ruff.format]
56
+ docstring-code-format = true
57
+ docstring-code-line-length = "dynamic"
58
+
59
+ [tool.ruff.lint]
60
+ select = [
61
+ "E", # pycodestyle errors
62
+ "W", # pycodestyle warnings
63
+ "F", # Pyflakes
64
+ "I", # isort
65
+ "N", # pep8-naming
66
+ "UP", # pyupgrade
67
+ "S", # bandit
68
+ "B", # flake8-bugbear
69
+ "A", # flake8-builtins
70
+ "C4", # flake8-comprehensions
71
+ "DTZ", # flake8-datetimez
72
+ "T20", # flake8-print
73
+ "SIM", # flake8-simplify
74
+ "LOG", # flake8-logging
75
+ "RUF", # Ruff-specific rules
76
+ ]
77
+ ignore = [
78
+ "S101", # Use of assert detected (needed for tests)
79
+ "T20", # print statements (CLI tool)
80
+ "A003", # Class attribute shadows builtin
81
+ "B008", # Function calls in arg defaults (Typer/Click idiom)
82
+ "SIM115", # Open without context manager (intentional in follow_log)
83
+ ]
aitc-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env python3
2
+ """Token counting and cost estimation CLI tool.
3
+
4
+ Counts tokens in text/files and estimates API cost across popular LLM models.
5
+ Supports a diff mode comparing two files.
6
+ """
7
+
8
+ import argparse
9
+ import os
10
+ import sys
11
+ from typing import Any
12
+
13
+ import requests
14
+ import tiktoken
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Model definitions
18
+ # ---------------------------------------------------------------------------
19
+
20
+ MODELS: list[dict[str, Any]] = [
21
+ {
22
+ "display_name": "GPT-4o",
23
+ "openrouter_id": "openai/gpt-4o",
24
+ "hardcoded_price_per_1m": 2.50,
25
+ },
26
+ {
27
+ "display_name": "GPT-4o mini",
28
+ "openrouter_id": "openai/gpt-4o-mini",
29
+ "hardcoded_price_per_1m": 0.15,
30
+ },
31
+ {
32
+ "display_name": "Claude Sonnet 4.5",
33
+ "openrouter_id": "anthropic/claude-sonnet-4-5",
34
+ "hardcoded_price_per_1m": 3.00,
35
+ },
36
+ {
37
+ "display_name": "Claude Haiku 4.5",
38
+ "openrouter_id": "anthropic/claude-haiku-4-5",
39
+ "hardcoded_price_per_1m": 0.80,
40
+ },
41
+ {
42
+ "display_name": "Gemini 1.5 Pro",
43
+ "openrouter_id": "google/gemini-pro-1.5",
44
+ "hardcoded_price_per_1m": 1.25,
45
+ },
46
+ {
47
+ "display_name": "Gemini 1.5 Flash",
48
+ "openrouter_id": "google/gemini-flash-1.5",
49
+ "hardcoded_price_per_1m": 0.075,
50
+ },
51
+ ]
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Tokenizer
56
+ # ---------------------------------------------------------------------------
57
+
58
+ _ENCODING = "cl100k_base"
59
+
60
+
61
+ def count_tokens(text: str) -> int:
62
+ """Count the number of tokens in a text string using cl100k_base encoding.
63
+
64
+ Args:
65
+ text: The input text to tokenize.
66
+
67
+ Returns:
68
+ The number of tokens in the text.
69
+ """
70
+ encoder = tiktoken.get_encoding(_ENCODING)
71
+ return len(encoder.encode(text))
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Pricing
76
+ # ---------------------------------------------------------------------------
77
+
78
+ _OPENROUTER_URL = "https://openrouter.ai/api/v1/models"
79
+ _LIVE_TIMEOUT_SECONDS = 3
80
+
81
+
82
+ def fetch_live_prices() -> dict[str, float] | None:
83
+ """Fetch live model pricing from the OpenRouter API.
84
+
85
+ Returns:
86
+ A dict mapping openrouter_id -> price_per_1m_tokens, or None if the
87
+ fetch fails for any reason (timeout, network error, bad JSON, etc.).
88
+ """
89
+ try:
90
+ response = requests.get(_OPENROUTER_URL, timeout=_LIVE_TIMEOUT_SECONDS)
91
+ response.raise_for_status()
92
+ data = response.json()
93
+ except Exception:
94
+ return None
95
+
96
+ if not isinstance(data, dict):
97
+ return None
98
+ models_list = data.get("data")
99
+ if not isinstance(models_list, list):
100
+ return None
101
+
102
+ prices: dict[str, float] = {}
103
+ for entry in models_list:
104
+ model_id = entry.get("id")
105
+ pricing = entry.get("pricing")
106
+ if not isinstance(model_id, str) or not isinstance(pricing, dict):
107
+ continue
108
+ prompt_price = pricing.get("prompt")
109
+ # OpenRouter pricing.prompt is price-per-token; convert to per-1M.
110
+ if isinstance(prompt_price, (int, float)):
111
+ prices[model_id] = prompt_price * 1_000_000
112
+ return prices
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # Formatting helpers
117
+ # ---------------------------------------------------------------------------
118
+
119
+
120
+ def format_number(n: int) -> str:
121
+ """Format an integer with comma separators.
122
+
123
+ Args:
124
+ n: The integer to format.
125
+
126
+ Returns:
127
+ A string like "1,234".
128
+ """
129
+ return f"{n:,}"
130
+
131
+
132
+ def format_cost(cost: float) -> str:
133
+ """Format a cost value to 6 decimal places.
134
+
135
+ Args:
136
+ cost: The cost value to format.
137
+
138
+ Returns:
139
+ A string like "0.003085".
140
+ """
141
+ return f"{cost:.6f}"
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Input reading
146
+ # ---------------------------------------------------------------------------
147
+
148
+
149
+ def read_file(path: str) -> str:
150
+ """Read text content from a file.
151
+
152
+ Args:
153
+ path: Path to the file.
154
+
155
+ Returns:
156
+ The file contents as a string.
157
+
158
+ Raises:
159
+ FileNotFoundError: If the file does not exist.
160
+ """
161
+ if not os.path.isfile(path):
162
+ raise FileNotFoundError(f"File not found: {path}")
163
+ with open(path) as fh:
164
+ return fh.read()
165
+
166
+
167
+ def read_stdin() -> str | None:
168
+ """Read all text from standard input (non-interactive / pipe).
169
+
170
+ Returns:
171
+ The piped text, or None if stdin is a terminal (tty).
172
+ """
173
+ if sys.stdin.isatty():
174
+ return None
175
+ return sys.stdin.read()
176
+
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Output
180
+ # ---------------------------------------------------------------------------
181
+
182
+
183
+ def _price_label(prices_live: bool, live_prices: dict | None) -> str | None:
184
+ """Build the bracketed price-source annotation line.
185
+
186
+ Args:
187
+ prices_live: Whether --live-prices was requested.
188
+ live_prices: The result of the live fetch (None means failed).
189
+
190
+ Returns:
191
+ A label string when --live-prices was used, None otherwise.
192
+ """
193
+ if not prices_live:
194
+ return None
195
+ if live_prices is None:
196
+ return "[prices: fallback (live fetch failed)]"
197
+ return "[prices: live via OpenRouter]"
198
+
199
+
200
+ def _get_prices(
201
+ prices_live: bool,
202
+ ) -> tuple[dict[str, float], dict[str, float] | None]:
203
+ """Resolve per-model prices.
204
+
205
+ Args:
206
+ prices_live: Whether to attempt a live fetch.
207
+
208
+ Returns:
209
+ Tuple of (price_per_1m_by_openrouter_id, raw_prices_dict_or_None).
210
+ """
211
+ live_prices: dict[str, float] | None = None
212
+ if prices_live:
213
+ live_prices = fetch_live_prices()
214
+ price_map: dict[str, float] = {}
215
+ for m in MODELS:
216
+ oid = str(m["openrouter_id"])
217
+ if live_prices is not None and oid in live_prices:
218
+ price_map[oid] = live_prices[oid]
219
+ else:
220
+ price_map[oid] = float(m["hardcoded_price_per_1m"])
221
+ return price_map, live_prices
222
+
223
+
224
+ def output_basic(
225
+ token_count: int,
226
+ prices_live: bool,
227
+ ) -> None:
228
+ """Print basic mode output (tokens + cost estimates)."""
229
+ price_map, live_prices = _get_prices(prices_live)
230
+
231
+ print(f"Tokens: {format_number(token_count)}")
232
+ label = _price_label(prices_live, live_prices)
233
+ if label is not None:
234
+ print(label)
235
+ print()
236
+ print("Cost estimates (input tokens):")
237
+
238
+ for m in MODELS:
239
+ oid = str(m["openrouter_id"])
240
+ ppm = price_map[oid]
241
+ cost = (token_count / 1_000_000) * ppm
242
+ print(f" {m['display_name']:<20} ${format_cost(cost)}")
243
+
244
+
245
+ def output_diff(
246
+ path1: str,
247
+ path2: str,
248
+ prices_live: bool,
249
+ ) -> None:
250
+ """Print diff mode output (two-file comparison)."""
251
+ try:
252
+ text1 = read_file(path1)
253
+ text2 = read_file(path2)
254
+ except FileNotFoundError as exc:
255
+ print(f"Error: {exc}", file=sys.stderr)
256
+ sys.exit(1)
257
+
258
+ count1 = count_tokens(text1)
259
+ count2 = count_tokens(text2)
260
+ delta = count2 - count1
261
+ delta_pct = (delta / count1 * 100) if count1 > 0 else 0.0
262
+ delta_sign = "+" if delta >= 0 else ""
263
+
264
+ price_map, live_prices = _get_prices(prices_live)
265
+
266
+ print(f"File 1: {path1:30s} → {format_number(count1):>8} tokens")
267
+ print(f"File 2: {path2:30s} → {format_number(count2):>8} tokens")
268
+ print()
269
+ print(f"Delta: {delta_sign}{format_number(delta)} tokens ({delta_sign}{delta_pct:.1f}%)")
270
+ print()
271
+ label = _price_label(prices_live, live_prices)
272
+ if label is not None:
273
+ print(label)
274
+ print()
275
+ print("Cost delta per model:")
276
+
277
+ for m in MODELS:
278
+ oid = str(m["openrouter_id"])
279
+ ppm = price_map[oid]
280
+ cost1 = (count1 / 1_000_000) * ppm
281
+ cost2 = (count2 / 1_000_000) * ppm
282
+ cost_delta = cost2 - cost1
283
+ delta_sign_cost = "+" if cost_delta >= 0 else ""
284
+ print(f" {m['display_name']:<20} {delta_sign_cost}${format_cost(cost_delta)}")
285
+
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # CLI
289
+ # ---------------------------------------------------------------------------
290
+
291
+
292
+ def build_parser() -> argparse.ArgumentParser:
293
+ """Build the argument parser.
294
+
295
+ Returns:
296
+ The configured ArgumentParser instance.
297
+ """
298
+ parser = argparse.ArgumentParser(
299
+ description="Count tokens and estimate LLM API costs.",
300
+ )
301
+ parser.add_argument(
302
+ "--text",
303
+ "-t",
304
+ type=str,
305
+ default=None,
306
+ help="Inline text to count tokens for.",
307
+ )
308
+ parser.add_argument(
309
+ "--file",
310
+ "-f",
311
+ type=str,
312
+ default=None,
313
+ help="Path to a file whose content should be tokenized.",
314
+ )
315
+ parser.add_argument(
316
+ "--diff",
317
+ "-d",
318
+ type=str,
319
+ nargs=2,
320
+ metavar=("FILE1", "FILE2"),
321
+ default=None,
322
+ help="Diff mode: compare token counts of two files.",
323
+ )
324
+ parser.add_argument(
325
+ "--live-prices",
326
+ action="store_true",
327
+ default=False,
328
+ help="Fetch real-time pricing from OpenRouter API.",
329
+ )
330
+ return parser
331
+
332
+
333
+ def get_input_text(args: argparse.Namespace) -> str:
334
+ """Resolve the input text from the provided arguments or stdin.
335
+
336
+ Priority: --text > --file > stdin pipe.
337
+
338
+ Args:
339
+ args: Parsed command-line arguments.
340
+
341
+ Returns:
342
+ The input text string.
343
+
344
+ Raises:
345
+ SystemExit: If no input source is available.
346
+ """
347
+ if args.text is not None:
348
+ return args.text
349
+
350
+ if args.file is not None:
351
+ try:
352
+ return read_file(args.file)
353
+ except FileNotFoundError as exc:
354
+ print(f"Error: {exc}", file=sys.stderr)
355
+ sys.exit(1)
356
+
357
+ stdin_text = read_stdin()
358
+ if stdin_text is not None:
359
+ return stdin_text
360
+
361
+ print(
362
+ "Error: no input provided. Use --text, --file, or pipe data via stdin.",
363
+ file=sys.stderr,
364
+ )
365
+ sys.exit(1)
366
+
367
+
368
+ def main() -> None:
369
+ """Main entry point."""
370
+ parser = build_parser()
371
+ args = parser.parse_args()
372
+
373
+ if args.diff is not None:
374
+ output_diff(args.diff[0], args.diff[1], args.live_prices)
375
+ else:
376
+ text = get_input_text(args)
377
+ token_count = count_tokens(text)
378
+ output_basic(token_count, args.live_prices)
379
+
380
+
381
+ if __name__ == "__main__":
382
+ main()