llm-rates 0.3.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.
- llm_rates-0.3.0/LICENSE +21 -0
- llm_rates-0.3.0/PKG-INFO +237 -0
- llm_rates-0.3.0/README.md +211 -0
- llm_rates-0.3.0/generate_tokenator_table.py +192 -0
- llm_rates-0.3.0/llm_rates/__init__.py +400 -0
- llm_rates-0.3.0/llm_rates/catalog.json +184 -0
- llm_rates-0.3.0/llm_rates/overlay.json +164 -0
- llm_rates-0.3.0/llm_rates.egg-info/PKG-INFO +237 -0
- llm_rates-0.3.0/llm_rates.egg-info/SOURCES.txt +18 -0
- llm_rates-0.3.0/llm_rates.egg-info/dependency_links.txt +1 -0
- llm_rates-0.3.0/llm_rates.egg-info/requires.txt +6 -0
- llm_rates-0.3.0/llm_rates.egg-info/top_level.txt +4 -0
- llm_rates-0.3.0/pyproject.toml +56 -0
- llm_rates-0.3.0/refresh.py +155 -0
- llm_rates-0.3.0/refresh_context.py +232 -0
- llm_rates-0.3.0/setup.cfg +4 -0
- llm_rates-0.3.0/tests/test_generate_tokenator_table.py +183 -0
- llm_rates-0.3.0/tests/test_llm_rates.py +448 -0
- llm_rates-0.3.0/tests/test_refresh.py +87 -0
- llm_rates-0.3.0/tests/test_refresh_context.py +207 -0
llm_rates-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 m0j0d
|
|
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.
|
llm_rates-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-rates
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: One vendored litellm-catalog snapshot + one internal overlay + shared lookup/cost math for portfolio LLM pricing
|
|
5
|
+
Author-email: Mike Donnelly <82827803+m0j0d@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/m0j0d/libs/tree/main/llm-rates
|
|
8
|
+
Project-URL: Repository, https://github.com/m0j0d/libs
|
|
9
|
+
Project-URL: Issues, https://github.com/m0j0d/libs/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Development Status :: 4 - Beta
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
23
|
+
Provides-Extra: refresh
|
|
24
|
+
Requires-Dist: anthropic>=0.122.0; extra == "refresh"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# llm-rates
|
|
28
|
+
|
|
29
|
+
Named `llm-rates` (not `llm-prices`, its name until 2026-08-21) because
|
|
30
|
+
PyPI's similarity check rejected `llm-prices` as too close to the existing
|
|
31
|
+
`llmprices` project — collapsed-separator collision, not an exact-name
|
|
32
|
+
clash. `llm-rates` was verified free in every form (`llm-rates`,
|
|
33
|
+
`llmrates`, `llm_rates`, singular `llm-rate`/`llmrate`) before adopting it;
|
|
34
|
+
don't re-litigate the name.
|
|
35
|
+
|
|
36
|
+
One vendored model-price catalog + one internal overlay + a shared Python
|
|
37
|
+
lookup/cost-math module, replacing four independently hand-maintained price
|
|
38
|
+
tables that were drifting apart. Full origin story and decisions: this
|
|
39
|
+
repo's sibling checkout `plugin/docs/plans/model-catalog-consolidation.md`
|
|
40
|
+
(cross-repo — `plugin` is a separate git repository from `libs`, so this is
|
|
41
|
+
a path reference, not a clickable link).
|
|
42
|
+
|
|
43
|
+
## Why this exists
|
|
44
|
+
|
|
45
|
+
Four tables held the same vendor facts (`plugin/scripts/prices/anthropic.json`,
|
|
46
|
+
`plugin/skills/tokenator/scripts/pricing.json`,
|
|
47
|
+
`sessions/src/sessmon/pricing.py`,
|
|
48
|
+
`factory-bench/runs/model-bench/model-bench-runner.py`). One of them silently
|
|
49
|
+
returned `$0.00` for any model it didn't recognise — a lookup bug, not a data
|
|
50
|
+
bug — and under-reported real spend by over $800 in one 30-day window. This
|
|
51
|
+
package fixes the *lookup logic* (never $0 for an unknown model) and gives
|
|
52
|
+
the data one home.
|
|
53
|
+
|
|
54
|
+
## Files
|
|
55
|
+
|
|
56
|
+
- `catalog.json` — vendored snapshot of
|
|
57
|
+
[litellm's `model_prices_and_context_window.json`](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json),
|
|
58
|
+
filtered to the providers/models the portfolio actually uses. Vendor list
|
|
59
|
+
price, per-single-token USD (litellm's native unit). Never hand-edit —
|
|
60
|
+
regenerate prices via `refresh.py --apply`; for Anthropic-provider rows,
|
|
61
|
+
the capability fields (`max_input_tokens`/`max_output_tokens`) are instead
|
|
62
|
+
kept in sync with Anthropic's own live Models API via
|
|
63
|
+
`refresh_context.py --apply` (litellm's snapshot is a third-party guess
|
|
64
|
+
for those fields, not authoritative).
|
|
65
|
+
- `overlay.json` — everything no public catalog can know: actual-billing
|
|
66
|
+
overrides (e.g. Groq OSS models are free-tier here, not their list price),
|
|
67
|
+
family-rate fallbacks (`claude-opus` → tier rate, so an unrecognised new
|
|
68
|
+
model prices at its family's rate instead of $0), model-ID aliases (dated
|
|
69
|
+
suffixes → canonical key), retirement/deprecation history (a retired
|
|
70
|
+
model's row is kept, never deleted, so historical transcripts still
|
|
71
|
+
reprice correctly), `context_overrides` for the rare model whose real
|
|
72
|
+
default-served context genuinely differs from its published maximum
|
|
73
|
+
(empty as of 2026-08-21 — see "Context-window data" below), and
|
|
74
|
+
per-consumer portfolio pins.
|
|
75
|
+
- `llm_rates/` — `lookup(model_id) -> PriceRecord` + cost math (in
|
|
76
|
+
`__init__.py`); `catalog.json` and `overlay.json` ship inside this
|
|
77
|
+
directory as package data so an installed wheel carries them alongside
|
|
78
|
+
the code. Shared by every Python consumer; the sole PowerShell consumer
|
|
79
|
+
(`tokenator.ps1`) can't import Python, so it reads a **generated** JSON
|
|
80
|
+
table instead — see `generate_tokenator_table.py` below and the plan
|
|
81
|
+
doc's "Known constraint".
|
|
82
|
+
- `refresh.py` — re-pulls the live litellm catalog, re-filters it to the
|
|
83
|
+
same model set, diffs against the vendored `catalog.json`, and prints a
|
|
84
|
+
report. Never applies silently — pass `--apply` to write. Price source
|
|
85
|
+
only; does not touch capability fields for Anthropic-provider rows.
|
|
86
|
+
- `refresh_context.py` — re-pulls `max_input_tokens`/`max_tokens` for every
|
|
87
|
+
Anthropic-provider `catalog.json` row from Anthropic's own live Models API
|
|
88
|
+
(`client.models.retrieve()`), diffs against the vendored values, and
|
|
89
|
+
prints a report. Never applies silently — pass `--apply` to write.
|
|
90
|
+
Requires the `anthropic` package (`pip install "llm-rates[refresh]"`) and
|
|
91
|
+
a resolvable Anthropic credential (`ANTHROPIC_API_KEY`,
|
|
92
|
+
`ANTHROPIC_AUTH_TOKEN`, or an `ant auth login` profile) — never falls back
|
|
93
|
+
to a guessed value if no credential resolves. Run manually; not wired
|
|
94
|
+
into any scheduled job. `lookup()`/`cost()` never import this module or
|
|
95
|
+
touch the network — see "Context-window data" below.
|
|
96
|
+
- `generate_tokenator_table.py` — emits a plain JSON price table shaped for
|
|
97
|
+
`tokenator.ps1` (a `models` map of `{input, output, context}` per model id,
|
|
98
|
+
plus flat `cache_read_multiplier`/`cache_write_multiplier` and a
|
|
99
|
+
`default` row) from `catalog.json` + `overlay.json`, so that repo can hold
|
|
100
|
+
a generated copy instead of a hand-maintained one. Deterministic — the
|
|
101
|
+
same inputs always produce byte-identical output.
|
|
102
|
+
|
|
103
|
+
## Usage
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from llm_rates import lookup, cost, UnknownModelError
|
|
107
|
+
|
|
108
|
+
record = lookup("claude-sonnet-5")
|
|
109
|
+
record.input # 2.0 ($/MTok)
|
|
110
|
+
record.output # 10.0 ($/MTok)
|
|
111
|
+
record.source # "catalog"
|
|
112
|
+
record.context # 1000000 (context-window token limit, or None)
|
|
113
|
+
|
|
114
|
+
# A client-side "[1m]" resolvedModel signal (not a vendor id — see
|
|
115
|
+
# overlay.json's aliases) resolves to the same price and, as of 2026-08-21,
|
|
116
|
+
# the same context — the base id already reports the model's real
|
|
117
|
+
# 1,000,000-token window:
|
|
118
|
+
lookup("claude-sonnet-5[1m]").context # 1000000
|
|
119
|
+
|
|
120
|
+
# One-shot cost for a turn:
|
|
121
|
+
usd = cost(
|
|
122
|
+
"claude-opus-4-8",
|
|
123
|
+
input_tokens=12_000,
|
|
124
|
+
output_tokens=800,
|
|
125
|
+
cache_read_tokens=50_000,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# An unrecognised model still resolves at its family rate:
|
|
129
|
+
lookup("claude-opus-4-9").source # "overlay-family-fallback"
|
|
130
|
+
|
|
131
|
+
# A genuinely unknown vendor/model raises instead of returning $0:
|
|
132
|
+
try:
|
|
133
|
+
lookup("some-new-vendor/mystery-model")
|
|
134
|
+
except UnknownModelError as e:
|
|
135
|
+
...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`lookup()` and `cost()` both accept optional `catalog=`/`overlay=` kwargs
|
|
139
|
+
(already-loaded dicts) — useful for tests, or for a caller that wants to load
|
|
140
|
+
once and reuse across many lookups instead of re-reading the JSON files each
|
|
141
|
+
call.
|
|
142
|
+
|
|
143
|
+
## Refreshing the catalog
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
python refresh.py # pull live litellm catalog, diff, print report
|
|
147
|
+
python refresh.py --apply # also overwrite catalog.json with the diff
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`refresh.py` always pulls the **live** GitHub-hosted catalog URL, never the
|
|
151
|
+
`litellm` pip package's bundled snapshot — that bundled copy is stale (it was
|
|
152
|
+
missing Haiku 4.5, Opus 5, Opus 4.8, Fable 5, and Sonnet 5 entirely as of
|
|
153
|
+
2026-07-28; see the plan doc's "Two traps found").
|
|
154
|
+
|
|
155
|
+
## Refreshing context-window data
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
python refresh_context.py # pull the live Models API, diff, print report
|
|
159
|
+
python refresh_context.py --apply # also overwrite catalog.json with the diff
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Context-window limits (`max_input_tokens`) and output caps
|
|
163
|
+
(`max_output_tokens`) for Anthropic-provider rows come from Anthropic's own
|
|
164
|
+
**Models API** (`client.models.retrieve(model_id)`), not litellm's snapshot
|
|
165
|
+
— litellm is a third-party-maintained guess for these fields, and it was
|
|
166
|
+
wrong at least once (see "Pricing notes worth knowing" below). `lookup()`
|
|
167
|
+
itself never calls this API: it's a refresh-time-only script, exactly like
|
|
168
|
+
`refresh.py`, that writes into the vendored `catalog.json`, so
|
|
169
|
+
`lookup()`/`cost()` stay pure and offline (they price historical
|
|
170
|
+
transcripts and run behind a PowerShell overlay with no network access).
|
|
171
|
+
Requires `pip install "llm-rates[refresh]"` (the `anthropic` SDK) and a
|
|
172
|
+
credential the SDK's own resolution chain can find
|
|
173
|
+
(`ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN`/`ant auth login`) — with none
|
|
174
|
+
resolvable, the script fails with an actionable message and writes nothing,
|
|
175
|
+
never a guessed value.
|
|
176
|
+
|
|
177
|
+
## Generating tokenator's pricing table
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
python generate_tokenator_table.py # print to stdout
|
|
181
|
+
python generate_tokenator_table.py --out FILE # write to FILE
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This is a read-only export — it never touches `plugin`'s checkout. The
|
|
185
|
+
consumer migration that adopts the generated bytes as `tokenator/pricing.json`
|
|
186
|
+
is tracked separately. Before emitting anything, the generator proves (via
|
|
187
|
+
`verify_cache_multipliers()`) that its flat cache multipliers (0.1x read,
|
|
188
|
+
2.0x write of input — tuned to Claude Code's 1h-TTL cache behaviour) match
|
|
189
|
+
this package's real per-model cache rates for every model tokenator prices;
|
|
190
|
+
a divergence raises instead of silently drifting.
|
|
191
|
+
|
|
192
|
+
## Pricing notes worth knowing
|
|
193
|
+
|
|
194
|
+
- **`claude-sonnet-5` is $2.00/$10.00 per MTok, permanently** — not a
|
|
195
|
+
time-limited introductory rate. It was announced as an introductory price
|
|
196
|
+
through 2026-08-31, but the vendor cancelled the scheduled 2026-09-01 rise
|
|
197
|
+
to $3.00/$15.00 (~2026-08-17). See `overlay.json`'s `notes`.
|
|
198
|
+
- **Groq OSS models list at $0.075–$0.29/MTok but bill $0.00** on the
|
|
199
|
+
portfolio's free-tier key — the vendor list price lives in `catalog.json`,
|
|
200
|
+
the actual billed rate lives in `overlay.json`'s
|
|
201
|
+
`actual_billing_overrides`. Both are correct; they answer different
|
|
202
|
+
questions.
|
|
203
|
+
- **A retired/de-listed model keeps its row.** `claude-opus-4-1` (retired
|
|
204
|
+
2026-08-05) and the Groq-delisted `llama-4-scout`/`qwen3-32b` entries stay
|
|
205
|
+
priced at their historical rate so old transcripts still reprice
|
|
206
|
+
correctly — deleting a row would silently re-price past usage at whatever
|
|
207
|
+
family-fallback rate happens to apply now.
|
|
208
|
+
- **`claude-fable-5`, `claude-opus-5`, and `claude-sonnet-5` all serve
|
|
209
|
+
1,000,000 tokens of context, natively** — confirmed by Anthropic's own
|
|
210
|
+
Models API. A prior `overlay.json` `context_overrides` block forced all
|
|
211
|
+
three to 200,000 (on the mistaken theory that catalog.json's
|
|
212
|
+
`max_input_tokens=1000000` was an extended-context-beta-only ceiling);
|
|
213
|
+
that override has been removed (corrected 2026-08-21, C8). Only
|
|
214
|
+
`claude-haiku-4-5` among current Claude models genuinely has a
|
|
215
|
+
200,000-token window.
|
|
216
|
+
|
|
217
|
+
## Setup for consumers
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
pip install -e C:/data/projects/libs/llm-rates
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Same convention as `libs/plan-doc` and `libs/triad-base` — see
|
|
224
|
+
`plugin/CLAUDE.md`.
|
|
225
|
+
|
|
226
|
+
## Status
|
|
227
|
+
|
|
228
|
+
**Rollout step 1 of `model-catalog-consolidation.md` — the library itself,
|
|
229
|
+
built and tested.** Consumer migrations (`activity_cost.py`, `sessmon`,
|
|
230
|
+
`model-bench-runner.py`, `tokenator.ps1`) are steps 2–5, tracked separately
|
|
231
|
+
in the plan doc and out of scope for this package's initial PR.
|
|
232
|
+
|
|
233
|
+
## Testing
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
pytest llm-rates/tests/ -v
|
|
237
|
+
```
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# llm-rates
|
|
2
|
+
|
|
3
|
+
Named `llm-rates` (not `llm-prices`, its name until 2026-08-21) because
|
|
4
|
+
PyPI's similarity check rejected `llm-prices` as too close to the existing
|
|
5
|
+
`llmprices` project — collapsed-separator collision, not an exact-name
|
|
6
|
+
clash. `llm-rates` was verified free in every form (`llm-rates`,
|
|
7
|
+
`llmrates`, `llm_rates`, singular `llm-rate`/`llmrate`) before adopting it;
|
|
8
|
+
don't re-litigate the name.
|
|
9
|
+
|
|
10
|
+
One vendored model-price catalog + one internal overlay + a shared Python
|
|
11
|
+
lookup/cost-math module, replacing four independently hand-maintained price
|
|
12
|
+
tables that were drifting apart. Full origin story and decisions: this
|
|
13
|
+
repo's sibling checkout `plugin/docs/plans/model-catalog-consolidation.md`
|
|
14
|
+
(cross-repo — `plugin` is a separate git repository from `libs`, so this is
|
|
15
|
+
a path reference, not a clickable link).
|
|
16
|
+
|
|
17
|
+
## Why this exists
|
|
18
|
+
|
|
19
|
+
Four tables held the same vendor facts (`plugin/scripts/prices/anthropic.json`,
|
|
20
|
+
`plugin/skills/tokenator/scripts/pricing.json`,
|
|
21
|
+
`sessions/src/sessmon/pricing.py`,
|
|
22
|
+
`factory-bench/runs/model-bench/model-bench-runner.py`). One of them silently
|
|
23
|
+
returned `$0.00` for any model it didn't recognise — a lookup bug, not a data
|
|
24
|
+
bug — and under-reported real spend by over $800 in one 30-day window. This
|
|
25
|
+
package fixes the *lookup logic* (never $0 for an unknown model) and gives
|
|
26
|
+
the data one home.
|
|
27
|
+
|
|
28
|
+
## Files
|
|
29
|
+
|
|
30
|
+
- `catalog.json` — vendored snapshot of
|
|
31
|
+
[litellm's `model_prices_and_context_window.json`](https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json),
|
|
32
|
+
filtered to the providers/models the portfolio actually uses. Vendor list
|
|
33
|
+
price, per-single-token USD (litellm's native unit). Never hand-edit —
|
|
34
|
+
regenerate prices via `refresh.py --apply`; for Anthropic-provider rows,
|
|
35
|
+
the capability fields (`max_input_tokens`/`max_output_tokens`) are instead
|
|
36
|
+
kept in sync with Anthropic's own live Models API via
|
|
37
|
+
`refresh_context.py --apply` (litellm's snapshot is a third-party guess
|
|
38
|
+
for those fields, not authoritative).
|
|
39
|
+
- `overlay.json` — everything no public catalog can know: actual-billing
|
|
40
|
+
overrides (e.g. Groq OSS models are free-tier here, not their list price),
|
|
41
|
+
family-rate fallbacks (`claude-opus` → tier rate, so an unrecognised new
|
|
42
|
+
model prices at its family's rate instead of $0), model-ID aliases (dated
|
|
43
|
+
suffixes → canonical key), retirement/deprecation history (a retired
|
|
44
|
+
model's row is kept, never deleted, so historical transcripts still
|
|
45
|
+
reprice correctly), `context_overrides` for the rare model whose real
|
|
46
|
+
default-served context genuinely differs from its published maximum
|
|
47
|
+
(empty as of 2026-08-21 — see "Context-window data" below), and
|
|
48
|
+
per-consumer portfolio pins.
|
|
49
|
+
- `llm_rates/` — `lookup(model_id) -> PriceRecord` + cost math (in
|
|
50
|
+
`__init__.py`); `catalog.json` and `overlay.json` ship inside this
|
|
51
|
+
directory as package data so an installed wheel carries them alongside
|
|
52
|
+
the code. Shared by every Python consumer; the sole PowerShell consumer
|
|
53
|
+
(`tokenator.ps1`) can't import Python, so it reads a **generated** JSON
|
|
54
|
+
table instead — see `generate_tokenator_table.py` below and the plan
|
|
55
|
+
doc's "Known constraint".
|
|
56
|
+
- `refresh.py` — re-pulls the live litellm catalog, re-filters it to the
|
|
57
|
+
same model set, diffs against the vendored `catalog.json`, and prints a
|
|
58
|
+
report. Never applies silently — pass `--apply` to write. Price source
|
|
59
|
+
only; does not touch capability fields for Anthropic-provider rows.
|
|
60
|
+
- `refresh_context.py` — re-pulls `max_input_tokens`/`max_tokens` for every
|
|
61
|
+
Anthropic-provider `catalog.json` row from Anthropic's own live Models API
|
|
62
|
+
(`client.models.retrieve()`), diffs against the vendored values, and
|
|
63
|
+
prints a report. Never applies silently — pass `--apply` to write.
|
|
64
|
+
Requires the `anthropic` package (`pip install "llm-rates[refresh]"`) and
|
|
65
|
+
a resolvable Anthropic credential (`ANTHROPIC_API_KEY`,
|
|
66
|
+
`ANTHROPIC_AUTH_TOKEN`, or an `ant auth login` profile) — never falls back
|
|
67
|
+
to a guessed value if no credential resolves. Run manually; not wired
|
|
68
|
+
into any scheduled job. `lookup()`/`cost()` never import this module or
|
|
69
|
+
touch the network — see "Context-window data" below.
|
|
70
|
+
- `generate_tokenator_table.py` — emits a plain JSON price table shaped for
|
|
71
|
+
`tokenator.ps1` (a `models` map of `{input, output, context}` per model id,
|
|
72
|
+
plus flat `cache_read_multiplier`/`cache_write_multiplier` and a
|
|
73
|
+
`default` row) from `catalog.json` + `overlay.json`, so that repo can hold
|
|
74
|
+
a generated copy instead of a hand-maintained one. Deterministic — the
|
|
75
|
+
same inputs always produce byte-identical output.
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from llm_rates import lookup, cost, UnknownModelError
|
|
81
|
+
|
|
82
|
+
record = lookup("claude-sonnet-5")
|
|
83
|
+
record.input # 2.0 ($/MTok)
|
|
84
|
+
record.output # 10.0 ($/MTok)
|
|
85
|
+
record.source # "catalog"
|
|
86
|
+
record.context # 1000000 (context-window token limit, or None)
|
|
87
|
+
|
|
88
|
+
# A client-side "[1m]" resolvedModel signal (not a vendor id — see
|
|
89
|
+
# overlay.json's aliases) resolves to the same price and, as of 2026-08-21,
|
|
90
|
+
# the same context — the base id already reports the model's real
|
|
91
|
+
# 1,000,000-token window:
|
|
92
|
+
lookup("claude-sonnet-5[1m]").context # 1000000
|
|
93
|
+
|
|
94
|
+
# One-shot cost for a turn:
|
|
95
|
+
usd = cost(
|
|
96
|
+
"claude-opus-4-8",
|
|
97
|
+
input_tokens=12_000,
|
|
98
|
+
output_tokens=800,
|
|
99
|
+
cache_read_tokens=50_000,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# An unrecognised model still resolves at its family rate:
|
|
103
|
+
lookup("claude-opus-4-9").source # "overlay-family-fallback"
|
|
104
|
+
|
|
105
|
+
# A genuinely unknown vendor/model raises instead of returning $0:
|
|
106
|
+
try:
|
|
107
|
+
lookup("some-new-vendor/mystery-model")
|
|
108
|
+
except UnknownModelError as e:
|
|
109
|
+
...
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`lookup()` and `cost()` both accept optional `catalog=`/`overlay=` kwargs
|
|
113
|
+
(already-loaded dicts) — useful for tests, or for a caller that wants to load
|
|
114
|
+
once and reuse across many lookups instead of re-reading the JSON files each
|
|
115
|
+
call.
|
|
116
|
+
|
|
117
|
+
## Refreshing the catalog
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python refresh.py # pull live litellm catalog, diff, print report
|
|
121
|
+
python refresh.py --apply # also overwrite catalog.json with the diff
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`refresh.py` always pulls the **live** GitHub-hosted catalog URL, never the
|
|
125
|
+
`litellm` pip package's bundled snapshot — that bundled copy is stale (it was
|
|
126
|
+
missing Haiku 4.5, Opus 5, Opus 4.8, Fable 5, and Sonnet 5 entirely as of
|
|
127
|
+
2026-07-28; see the plan doc's "Two traps found").
|
|
128
|
+
|
|
129
|
+
## Refreshing context-window data
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python refresh_context.py # pull the live Models API, diff, print report
|
|
133
|
+
python refresh_context.py --apply # also overwrite catalog.json with the diff
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Context-window limits (`max_input_tokens`) and output caps
|
|
137
|
+
(`max_output_tokens`) for Anthropic-provider rows come from Anthropic's own
|
|
138
|
+
**Models API** (`client.models.retrieve(model_id)`), not litellm's snapshot
|
|
139
|
+
— litellm is a third-party-maintained guess for these fields, and it was
|
|
140
|
+
wrong at least once (see "Pricing notes worth knowing" below). `lookup()`
|
|
141
|
+
itself never calls this API: it's a refresh-time-only script, exactly like
|
|
142
|
+
`refresh.py`, that writes into the vendored `catalog.json`, so
|
|
143
|
+
`lookup()`/`cost()` stay pure and offline (they price historical
|
|
144
|
+
transcripts and run behind a PowerShell overlay with no network access).
|
|
145
|
+
Requires `pip install "llm-rates[refresh]"` (the `anthropic` SDK) and a
|
|
146
|
+
credential the SDK's own resolution chain can find
|
|
147
|
+
(`ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN`/`ant auth login`) — with none
|
|
148
|
+
resolvable, the script fails with an actionable message and writes nothing,
|
|
149
|
+
never a guessed value.
|
|
150
|
+
|
|
151
|
+
## Generating tokenator's pricing table
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
python generate_tokenator_table.py # print to stdout
|
|
155
|
+
python generate_tokenator_table.py --out FILE # write to FILE
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
This is a read-only export — it never touches `plugin`'s checkout. The
|
|
159
|
+
consumer migration that adopts the generated bytes as `tokenator/pricing.json`
|
|
160
|
+
is tracked separately. Before emitting anything, the generator proves (via
|
|
161
|
+
`verify_cache_multipliers()`) that its flat cache multipliers (0.1x read,
|
|
162
|
+
2.0x write of input — tuned to Claude Code's 1h-TTL cache behaviour) match
|
|
163
|
+
this package's real per-model cache rates for every model tokenator prices;
|
|
164
|
+
a divergence raises instead of silently drifting.
|
|
165
|
+
|
|
166
|
+
## Pricing notes worth knowing
|
|
167
|
+
|
|
168
|
+
- **`claude-sonnet-5` is $2.00/$10.00 per MTok, permanently** — not a
|
|
169
|
+
time-limited introductory rate. It was announced as an introductory price
|
|
170
|
+
through 2026-08-31, but the vendor cancelled the scheduled 2026-09-01 rise
|
|
171
|
+
to $3.00/$15.00 (~2026-08-17). See `overlay.json`'s `notes`.
|
|
172
|
+
- **Groq OSS models list at $0.075–$0.29/MTok but bill $0.00** on the
|
|
173
|
+
portfolio's free-tier key — the vendor list price lives in `catalog.json`,
|
|
174
|
+
the actual billed rate lives in `overlay.json`'s
|
|
175
|
+
`actual_billing_overrides`. Both are correct; they answer different
|
|
176
|
+
questions.
|
|
177
|
+
- **A retired/de-listed model keeps its row.** `claude-opus-4-1` (retired
|
|
178
|
+
2026-08-05) and the Groq-delisted `llama-4-scout`/`qwen3-32b` entries stay
|
|
179
|
+
priced at their historical rate so old transcripts still reprice
|
|
180
|
+
correctly — deleting a row would silently re-price past usage at whatever
|
|
181
|
+
family-fallback rate happens to apply now.
|
|
182
|
+
- **`claude-fable-5`, `claude-opus-5`, and `claude-sonnet-5` all serve
|
|
183
|
+
1,000,000 tokens of context, natively** — confirmed by Anthropic's own
|
|
184
|
+
Models API. A prior `overlay.json` `context_overrides` block forced all
|
|
185
|
+
three to 200,000 (on the mistaken theory that catalog.json's
|
|
186
|
+
`max_input_tokens=1000000` was an extended-context-beta-only ceiling);
|
|
187
|
+
that override has been removed (corrected 2026-08-21, C8). Only
|
|
188
|
+
`claude-haiku-4-5` among current Claude models genuinely has a
|
|
189
|
+
200,000-token window.
|
|
190
|
+
|
|
191
|
+
## Setup for consumers
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
pip install -e C:/data/projects/libs/llm-rates
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Same convention as `libs/plan-doc` and `libs/triad-base` — see
|
|
198
|
+
`plugin/CLAUDE.md`.
|
|
199
|
+
|
|
200
|
+
## Status
|
|
201
|
+
|
|
202
|
+
**Rollout step 1 of `model-catalog-consolidation.md` — the library itself,
|
|
203
|
+
built and tested.** Consumer migrations (`activity_cost.py`, `sessmon`,
|
|
204
|
+
`model-bench-runner.py`, `tokenator.ps1`) are steps 2–5, tracked separately
|
|
205
|
+
in the plan doc and out of scope for this package's initial PR.
|
|
206
|
+
|
|
207
|
+
## Testing
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
pytest llm-rates/tests/ -v
|
|
211
|
+
```
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Generate a plain JSON price table for tokenator.ps1 — the sole PowerShell
|
|
2
|
+
consumer of this package's data, which can't `import llm_rates` and instead
|
|
3
|
+
reads a generated JSON file directly (see README.md's "Known constraint").
|
|
4
|
+
|
|
5
|
+
Deterministic: the same catalog.json/overlay.json inputs always produce
|
|
6
|
+
byte-identical output, so a downstream drift test in the tokenator skill's
|
|
7
|
+
own repo can assert its committed pricing.json matches a fresh generation.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python generate_tokenator_table.py # print to stdout
|
|
11
|
+
python generate_tokenator_table.py --out FILE # write to FILE
|
|
12
|
+
|
|
13
|
+
Never writes tokenator's actual pricing.json from this repo — that file
|
|
14
|
+
lives in a separate repository (plugin) and is owned by a separate
|
|
15
|
+
migration; this script only produces the bytes for that migration to adopt.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from llm_rates import load_catalog, load_overlay, lookup
|
|
27
|
+
|
|
28
|
+
# Order matters for output determinism/readability — mirrors tokenator's
|
|
29
|
+
# existing pricing.json key order exactly. Each key IS the model_id passed
|
|
30
|
+
# to lookup(): the bare family ids ("claude-opus", "claude-sonnet",
|
|
31
|
+
# "claude-haiku") resolve via overlay.json's family_fallback (no specific
|
|
32
|
+
# version pinned), and the "[1m]" entries are client-side resolvedModel
|
|
33
|
+
# signals (not vendor ids) that resolve via overlay.json's aliases +
|
|
34
|
+
# context_overrides — see llm_rates.py's module docstring "Context-window
|
|
35
|
+
# limits" section and overlay.json's notes.
|
|
36
|
+
TOKENATOR_MODEL_IDS = [
|
|
37
|
+
"claude-fable-5",
|
|
38
|
+
"claude-mythos-5",
|
|
39
|
+
"claude-opus-5[1m]",
|
|
40
|
+
"claude-opus",
|
|
41
|
+
"claude-sonnet-5[1m]",
|
|
42
|
+
"claude-sonnet-5",
|
|
43
|
+
"claude-sonnet",
|
|
44
|
+
"claude-haiku",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# tokenator.ps1 uses flat cache multipliers tuned to Claude Code's 1h-TTL
|
|
48
|
+
# cache behaviour: reads at 0.1x input, writes at 2.0x input (the 1h-TTL
|
|
49
|
+
# creation rate — Claude Code writes 1h-TTL caches exclusively; see
|
|
50
|
+
# tokenator/pricing.json's own _comment). These are not independently
|
|
51
|
+
# invented constants: verify_cache_multipliers() below proves, for every
|
|
52
|
+
# model in TOKENATOR_MODEL_IDS, that the lib's real per-model
|
|
53
|
+
# cache_read/cache_creation_1h rates divide by that model's input rate to
|
|
54
|
+
# exactly these two numbers, before build_table() ever emits them. If a
|
|
55
|
+
# future catalog update broke that identity for any model, this raises
|
|
56
|
+
# instead of silently drifting from what tokenator actually bills.
|
|
57
|
+
CACHE_READ_MULTIPLIER = 0.1
|
|
58
|
+
CACHE_WRITE_MULTIPLIER = 2.0
|
|
59
|
+
|
|
60
|
+
# tokenator's "default" row (used when no prefix in its models map matches)
|
|
61
|
+
# has always carried the claude-opus family rate — same lookup id used for
|
|
62
|
+
# the bare "claude-opus" entry above.
|
|
63
|
+
DEFAULT_MODEL_ID = "claude-opus"
|
|
64
|
+
|
|
65
|
+
_RATIO_TOLERANCE = 1e-9
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def verify_cache_multipliers(
|
|
69
|
+
model_ids: list[str],
|
|
70
|
+
*,
|
|
71
|
+
catalog: dict[str, Any] | None = None,
|
|
72
|
+
overlay: dict[str, Any] | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Raise ValueError if any model's real cache_read/cache_creation_1h
|
|
75
|
+
rate diverges from the flat CACHE_READ_MULTIPLIER/CACHE_WRITE_MULTIPLIER
|
|
76
|
+
this module emits. Called by build_table() before it writes anything.
|
|
77
|
+
"""
|
|
78
|
+
for model_id in model_ids:
|
|
79
|
+
record = lookup(model_id, catalog=catalog, overlay=overlay)
|
|
80
|
+
if record.input == 0:
|
|
81
|
+
# Free-tier override (not reachable for any current tokenator
|
|
82
|
+
# id, but guarded for future-proofing) — ratio is undefined.
|
|
83
|
+
continue
|
|
84
|
+
read_ratio = record.cache_read / record.input
|
|
85
|
+
write_ratio = record.cache_creation_1h / record.input
|
|
86
|
+
if abs(read_ratio - CACHE_READ_MULTIPLIER) > _RATIO_TOLERANCE:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"{model_id}: cache_read/input ratio {read_ratio!r} does not "
|
|
89
|
+
f"match declared CACHE_READ_MULTIPLIER {CACHE_READ_MULTIPLIER!r} "
|
|
90
|
+
"— tokenator's flat multiplier would silently diverge from "
|
|
91
|
+
"the lib's real rate for this model."
|
|
92
|
+
)
|
|
93
|
+
if abs(write_ratio - CACHE_WRITE_MULTIPLIER) > _RATIO_TOLERANCE:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"{model_id}: cache_creation_1h/input ratio {write_ratio!r} "
|
|
96
|
+
f"does not match declared CACHE_WRITE_MULTIPLIER "
|
|
97
|
+
f"{CACHE_WRITE_MULTIPLIER!r}."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_table(
|
|
102
|
+
*,
|
|
103
|
+
catalog: dict[str, Any] | None = None,
|
|
104
|
+
overlay: dict[str, Any] | None = None,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
"""Pure function: build the tokenator-shaped pricing dict from the
|
|
107
|
+
catalog + overlay. No I/O beyond catalog=/overlay=None triggering the
|
|
108
|
+
package's own load_catalog()/load_overlay().
|
|
109
|
+
"""
|
|
110
|
+
catalog = catalog if catalog is not None else load_catalog()
|
|
111
|
+
overlay = overlay if overlay is not None else load_overlay()
|
|
112
|
+
|
|
113
|
+
verify_cache_multipliers(TOKENATOR_MODEL_IDS, catalog=catalog, overlay=overlay)
|
|
114
|
+
|
|
115
|
+
models: dict[str, Any] = {}
|
|
116
|
+
for model_id in TOKENATOR_MODEL_IDS:
|
|
117
|
+
record = lookup(model_id, catalog=catalog, overlay=overlay)
|
|
118
|
+
if record.context is None:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"{model_id}: lookup() resolved no context-window value — "
|
|
121
|
+
"tokenator needs a context for every emitted model; refusing "
|
|
122
|
+
"to emit a guessed number. Add a context_overrides row (or a "
|
|
123
|
+
"family_fallback 'context' field) in overlay.json."
|
|
124
|
+
)
|
|
125
|
+
models[model_id] = {
|
|
126
|
+
"input": record.input,
|
|
127
|
+
"output": record.output,
|
|
128
|
+
"context": record.context,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
default_record = lookup(DEFAULT_MODEL_ID, catalog=catalog, overlay=overlay)
|
|
132
|
+
if default_record.context is None:
|
|
133
|
+
raise ValueError(f"{DEFAULT_MODEL_ID}: no context-window value for default row.")
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"_comment": (
|
|
137
|
+
"USD per million tokens. GENERATED by "
|
|
138
|
+
"libs/llm-rates/generate_tokenator_table.py from llm-rates' "
|
|
139
|
+
"catalog.json + overlay.json — do not hand-edit; regenerate "
|
|
140
|
+
"instead. Cache multipliers are flat (read 0.1x, write 2.0x of "
|
|
141
|
+
"input) because Claude Code uses 1h-TTL cache writes exclusively; "
|
|
142
|
+
"the generator's verify_cache_multipliers() proves per-model "
|
|
143
|
+
"that this matches the lib's real per-token cache rates before "
|
|
144
|
+
"every generation. Longest-prefix match on model id; 'default' "
|
|
145
|
+
"used when no prefix matches. 'context' is the model's "
|
|
146
|
+
"context-window token limit. The bracketed '[1m]' entries are "
|
|
147
|
+
"client-side resolvedModel signals (tokenator.ps1's Get-Context) "
|
|
148
|
+
"for the 1M-context beta, not vendor model ids -- the base model "
|
|
149
|
+
"id never carries a bracket suffix on its own."
|
|
150
|
+
),
|
|
151
|
+
"models": models,
|
|
152
|
+
"cache_read_multiplier": CACHE_READ_MULTIPLIER,
|
|
153
|
+
"cache_write_multiplier": CACHE_WRITE_MULTIPLIER,
|
|
154
|
+
"default": {
|
|
155
|
+
"input": default_record.input,
|
|
156
|
+
"output": default_record.output,
|
|
157
|
+
"context": default_record.context,
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def render_json(table: dict[str, Any]) -> str:
|
|
163
|
+
"""Deterministic JSON rendering — same table -> byte-identical string
|
|
164
|
+
every call. Trailing newline matches the existing hand-maintained file.
|
|
165
|
+
"""
|
|
166
|
+
return json.dumps(table, indent=2) + "\n"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _cli(argv: list[str] | None = None) -> int:
|
|
170
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
171
|
+
parser.add_argument(
|
|
172
|
+
"--out",
|
|
173
|
+
type=Path,
|
|
174
|
+
default=None,
|
|
175
|
+
help="Write to this path instead of stdout.",
|
|
176
|
+
)
|
|
177
|
+
args = parser.parse_args(argv)
|
|
178
|
+
|
|
179
|
+
table = build_table()
|
|
180
|
+
rendered = render_json(table)
|
|
181
|
+
|
|
182
|
+
if args.out is not None:
|
|
183
|
+
args.out.write_text(rendered, encoding="utf-8", newline="\n")
|
|
184
|
+
print(f"Wrote {args.out}", file=sys.stderr)
|
|
185
|
+
else:
|
|
186
|
+
sys.stdout.write(rendered)
|
|
187
|
+
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
raise SystemExit(_cli())
|