coding-agent-cost 0.5.1__tar.gz → 0.5.3__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.
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/PKG-INFO +3 -1
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/__init__.py +1 -1
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/rates.json +146 -3
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/PKG-INFO +3 -1
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/SOURCES.txt +2 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/pyproject.toml +3 -1
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_astra_pricing.py +2 -2
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_billing_plan.py +27 -4
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_cli.py +1 -1
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_e0a_review3_hardening.py +3 -3
- coding_agent_cost-0.5.3/tests/test_fable_5_2_rates.py +83 -0
- coding_agent_cost-0.5.3/tests/test_gpt_6_1_rates.py +117 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_haiku_5_5_rates.py +1 -1
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_rates.py +6 -3
- coding_agent_cost-0.5.3/tests/test_sonnet_5_5_rates.py +120 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_version_consistency.py +7 -7
- coding_agent_cost-0.5.1/tests/test_sonnet_5_5_rates.py +0 -69
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/LICENSE +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/README.md +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/aggregate.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/billing_plan.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/cli.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/config.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/facts.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/rates.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/readers/__init__.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/readers/claude.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/readers/codex.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/agent_cost/renderers.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/dependency_links.txt +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/entry_points.txt +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/top_level.txt +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/setup.cfg +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_aggregate.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_e0a_fable_5_1_and_sonnet_5_correction.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_facts.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_measure_v1_contract.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_opus_5_5_rates.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_reader_claude.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_reader_codex.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_reader_dedup.py +0 -0
- {coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/tests/test_reader_output_lower_bound.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-agent-cost
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Estimate AI coding agent (Claude Code / Codex CLI) token usage and cost from local logs
|
|
5
5
|
Author: shiki-yusuke
|
|
6
6
|
License: MIT
|
|
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
22
|
Classifier: Topic :: Software Development :: Build Tools
|
|
21
23
|
Classifier: Topic :: Utilities
|
|
22
24
|
Requires-Python: >=3.9
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "2",
|
|
3
|
-
"catalog_version": "2026-10
|
|
3
|
+
"catalog_version": "2026-11-10",
|
|
4
4
|
"currency": "USD",
|
|
5
5
|
"unit": "per_mtok",
|
|
6
6
|
"usd_per_credit": "0.04",
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
"claude-sonnet-5 corrected 2026-09-09: a prior period (rate_id claude-sonnet-5-standard, effective_from 2026-09-01, values 3.0/0.30/3.75/6.0/15.0) priced a previously-announced rate increase that the pricing page's claude-sonnet-5-introductory-pricing note states did not occur (\"The previously scheduled increase to $3/$15 ... on September 1, 2026 will not occur\"). That period has been replaced by claude-sonnet-5-standard-2026-09-01, which carries the same $2/$0.20/$2.50/$4/$10 values as the preceding claude-sonnet-5-launch-promo period, since the launch price is confirmed to be the ongoing price with no change at the 2026-09-01 boundary. Any measure/v1 or report output computed against the pre-correction catalog for claude-sonnet-5 sessions occurring on or after 2026-09-01 overstated cost by 1.5x for that model. To detect this class of error going forward, cross-check each priced model's rate periods against its primary pricing page's Note text on a recurring (e.g. monthly) basis for mentions of a rate-change announcement being withdrawn or not taking effect -- a period can be internally valid (schema-wise) and still price a rate that was announced but never actually charged.",
|
|
14
14
|
"claude-opus-5-5 added 2026-09-23: rate_id claude-opus-5-5-launch-2026-09-22. effective_from 2026-09-22T00:00:00Z is 00:00:00Z of the official public launch date (anthropic.com/claude-opus-5-5, 'September 22, 2026'); the launch time of day is not published, but a transcript row whose raw model ID is claude-opus-5-5 cannot predate the launch, so pricing from the start of that UTC day cannot price a pre-launch event. Values 4.0/0.20/5.0/8.0/20.0 per the pricing page: cache_read is 0.05x base input (pricing-page footnote 2), a genuine per-model difference from the standard 0.1x and from claude-fable-5-1's 0.025x. fast_multiplier 2.0 (fast mode $8/$40 per MTok on the pricing page). Claude Code's CHANGELOG entry for 2.1.280 states Opus 5.5 became the default model (see sources); rows whose raw model ID is claude-opus-5-5 would be unpriced without this entry. Claude Sonnet 5.5 and Claude Haiku 5.5 are announced to follow 'in the coming weeks' -- add them as separate entries when their model IDs and prices are published, never as aliases of the 5.x entries.",
|
|
15
15
|
"claude-sonnet-5-5 added 2026-09-29: rate_id claude-sonnet-5-5-launch-2026-09-28. effective_from 2026-09-28T00:00:00Z is 00:00:00Z of the official public launch date (anthropic.com/claude-sonnet-5-5, 'September 28, 2026'); as with claude-opus-5-5, the launch time of day is not published, but a transcript row whose raw model ID is claude-sonnet-5-5 cannot predate the launch, so pricing from the start of that UTC day cannot price a pre-launch event. Values 2.0/0.20/2.50/4.0/10.0 per the pricing page (Sonnet 5.5 row, no footnote: cache_read is the standard 0.1x, unlike claude-opus-5-5's 0.05x and claude-fable-5-1's 0.025x). Identical to claude-sonnet-5's ongoing values, but kept as a separate entry, never an alias: the pricing page lists the two rows independently and a future change to one need not apply to the other. fast_multiplier 1.0: the pricing page's fast-mode table lists only the Opus models. Claude Code's CHANGELOG entry for 2.1.284 states Sonnet 5.5 is now the default Sonnet model, so the `sonnet` alias in agent frontmatter resolves to it and rows whose raw model ID is claude-sonnet-5-5 are expected from that version onward; no such row existed in the local transcripts on 2026-09-29. Claude Haiku 5.5 is announced to follow 'in the coming weeks' -- add it as a separate entry when its model ID and prices are published.",
|
|
16
|
-
"claude-haiku-5-5 added 2026-10-09: rate_id claude-haiku-5-5-launch-2026-10-07. effective_from 2026-10-07T00:00:00Z is 00:00:00Z of the official release date (platform.claude.com/docs/en/models/haiku-5-5/overview, 'Released October 7, 2026'); as with claude-sonnet-5-5, the release time of day is not published, but a transcript row whose raw model ID is claude-haiku-5-5 cannot predate the release. Its prices are tiered by prompt length: the period's top-level values (0.10/0.01/0.125/0.20/0.50) apply to prompts up to 100,000 tokens, and prompt_tiers[0] (prompt_tokens_over 100000: 0.50/0.05/0.625/1.0/2.50) to prompts over 100,000 tokens, per the pricing page's two Haiku 5.5 rows (no footnote: cache_read is the standard 0.1x). Prompt length follows the pricing page's Long context pricing section: it counts all of a request's input tokens including cache reads and cache writes, and each request is priced on its own, so a request over the threshold pays the higher prices for every token kind, output included. A fact whose prompt length is unknown (Codex, a conflicting Claude dedup group, inconsistent usage) is priced at the top-level values as a lower_bound. A separate entry, never an alias of claude-haiku-4-5. fast_multiplier 1.0: the pricing page's fast-mode table lists only the Opus models. Claude Code's CHANGELOG entry for 2.1.293 states Haiku 5.5 is now the default Haiku model, so the `haiku` alias resolves to it from that version onward. schema_version is 2 because prompt_tiers is new: agent-cost 0.3.0 and earlier reject schema_version 2 as unsupported instead of silently pricing every claude-haiku-5-5 request at the cheaper tier, and a schema_version 1 catalog may not carry prompt_tiers."
|
|
16
|
+
"claude-haiku-5-5 added 2026-10-09: rate_id claude-haiku-5-5-launch-2026-10-07. effective_from 2026-10-07T00:00:00Z is 00:00:00Z of the official release date (platform.claude.com/docs/en/models/haiku-5-5/overview, 'Released October 7, 2026'); as with claude-sonnet-5-5, the release time of day is not published, but a transcript row whose raw model ID is claude-haiku-5-5 cannot predate the release. Its prices are tiered by prompt length: the period's top-level values (0.10/0.01/0.125/0.20/0.50) apply to prompts up to 100,000 tokens, and prompt_tiers[0] (prompt_tokens_over 100000: 0.50/0.05/0.625/1.0/2.50) to prompts over 100,000 tokens, per the pricing page's two Haiku 5.5 rows (no footnote: cache_read is the standard 0.1x). Prompt length follows the pricing page's Long context pricing section: it counts all of a request's input tokens including cache reads and cache writes, and each request is priced on its own, so a request over the threshold pays the higher prices for every token kind, output included. A fact whose prompt length is unknown (Codex, a conflicting Claude dedup group, inconsistent usage) is priced at the top-level values as a lower_bound. A separate entry, never an alias of claude-haiku-4-5. fast_multiplier 1.0: the pricing page's fast-mode table lists only the Opus models. Claude Code's CHANGELOG entry for 2.1.293 states Haiku 5.5 is now the default Haiku model, so the `haiku` alias resolves to it from that version onward. schema_version is 2 because prompt_tiers is new: agent-cost 0.3.0 and earlier reject schema_version 2 as unsupported instead of silently pricing every claude-haiku-5-5 request at the cheaper tier, and a schema_version 1 catalog may not carry prompt_tiers.",
|
|
17
|
+
"claude-sonnet-5-5 cache_read cut 2026-10-13: the launch period (rate_id claude-sonnet-5-5-launch-2026-09-28) now ends at 2026-10-07T00:00:00Z and a new period (rate_id claude-sonnet-5-5-cache-read-cut-2026-10-07) prices cache_read at 0.10 instead of 0.20 from that instant; input_nocache, cache_write_5m, cache_write_1h and output are unchanged (2.0/2.50/4.0/10.0). anthropic.com/claude-haiku-5-5 (dated October 7, 2026) states 'starting today, we're lowering the price of cache reads on Claude Sonnet 5.5' and 'Cache reads now cost 50% less: $0.10 per million tokens rather than $0.20'; the source gives a calendar date only, so effective_from is 00:00:00Z of that day, following the claude-sonnet-5-standard-2026-09-01 precedent. The pricing page (retrieved 2026-10-13) lists the Sonnet 5.5 row as $2 / $2.50 / $4 / $0.10 / $10 with footnote 2: cache hits and refreshes on Claude Opus 5.5 and Claude Sonnet 5.5 are 0.05x the base input price, so cache_read is now 0.05x like claude-opus-5-5. Claude Code's CHANGELOG entry for 2.1.296 (2026-10-09) updated /cost, the status line, --max-budget-usd and the SDK's cost figures to $0.10 per million Sonnet 5.5 cache-read tokens; that is the date Claude Code's own figures changed, not the API price's effective date. catalog 2026-10-09 priced claude-sonnet-5-5 cache_read at 0.20 for events from 2026-10-07 through 2026-10-13, overstating that component 2x; reports produced with it for windows touching 2026-10-07 or later should be regenerated. claude-sonnet-5 is unchanged (cache_read stays 0.20): the pricing page's footnote 3 covers its input/output prices only, and footnote 2 names Opus 5.5 and Sonnet 5.5, not Sonnet 5.",
|
|
18
|
+
"gpt-6.1-terra, gpt-6.1-sol and codex-auto-review added 2026-11-09: standard tier, short context USD per MTok from the human-approved price table pricing-2026-11-v4 (developers.openai.com/api/docs/pricing, retrieved 2026-11-05); effective_from 2026-10-01T00:00:00Z is a placeholder before the first observed use (official launch date unconfirmed; rate_id *-launch-date-unconfirmed, following the claude-opus-5 placeholder precedent); usage from 2026-11-04 must be priced, so the retrieval date 2026-11-05 cannot be used. codex-auto-review is the model name of the Codex automatic review (guardian subagent) and has no published price; a human decision on 2026-11-06 prices it the same as gpt-6.1-terra. credits_per_mtok follow usd_per_credit=0.04. Existing entries (including gpt-5.6-terra) are unchanged.",
|
|
19
|
+
"claude-fable-5-2 and claude-mythos-5-2 added 2026-11-10: rate_ids claude-fable-5-2-launch-2026-11-09 / claude-mythos-5-2-launch-2026-11-09. effective_from 2026-11-09T00:00:00Z is 00:00:00Z of the official announcement date (anthropic.com/claude-fable-and-mythos-5-2, published November 9, 2026); the launch time of day is not published, but a transcript row whose raw model ID is claude-fable-5-2 or claude-mythos-5-2 cannot predate the launch. Values 8.0/0.20/10.0/16.0/40.0 per the pricing page (retrieved 2026-11-10): cache_read is 0.025x base input (footnote 1), like claude-fable-5-1. claude-mythos-5-2 (limited availability, verified organizations only) has the same prices; it is kept as a separate entry, never an alias of claude-fable-5-2. fast_multiplier 2.0 (fast mode $16/$80 per MTok on the pricing page). No prompt-length tiers (1M context at standard prices). Claude Code's CHANGELOG entry for 2.1.321 adds claude-fable-5-2 at $8/$40 with $0.20 cache reads. claude-fable-5-1 is unchanged (10.0/0.25/12.50/20.0/50.0). claude-mythos-5-1 stays absent (official release date unconfirmed), so its rows remain unpriced."
|
|
17
20
|
],
|
|
18
21
|
"sources": [
|
|
19
22
|
{
|
|
@@ -113,6 +116,30 @@
|
|
|
113
116
|
"url": "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md",
|
|
114
117
|
"retrieved_at": "2026-10-09",
|
|
115
118
|
"note": "Claude Code 2.1.293 entry: Added Claude Haiku 5.5 (claude-haiku-5-5), now the default Haiku model; 1M context, $0.10/$0.50 per MTok ($0.50/$2.50 for prompts over 100K); the reason claude-haiku-5-5 rows are expected in transcripts from that version"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"url": "https://platform.claude.com/docs/en/about-claude/pricing",
|
|
122
|
+
"retrieved_at": "2026-10-13",
|
|
123
|
+
"note": "used for the claude-sonnet-5-5 cache_read cut (Sonnet 5.5 row: $2 / $2.50 / $4 / $0.10 / $10; footnote 2: cache hits and refreshes on Claude Opus 5.5 and Claude Sonnet 5.5 are priced at 0.05x the base input price)"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"url": "https://www.anthropic.com/claude-haiku-5-5",
|
|
127
|
+
"retrieved_at": "2026-10-13",
|
|
128
|
+
"note": "Haiku 5.5 announcement dated October 7, 2026, 'Further updates': starting that day, Claude Sonnet 5.5 cache reads cost $0.10 per million tokens rather than $0.20; the effective date of the claude-sonnet-5-5 cache_read cut"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"url": "https://developers.openai.com/api/docs/pricing",
|
|
132
|
+
"retrieved_at": "2026-11-05"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"url": "https://www.anthropic.com/claude-fable-and-mythos-5-2",
|
|
136
|
+
"retrieved_at": "2026-11-10",
|
|
137
|
+
"note": "Fable 5.2 / Mythos 5.2 announcement published November 9, 2026; the effective date of claude-fable-5-2 and claude-mythos-5-2"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"url": "https://platform.claude.com/docs/en/about-claude/pricing",
|
|
141
|
+
"retrieved_at": "2026-11-10",
|
|
142
|
+
"note": "Fable 5.2 / Mythos 5.2 rows: $8 input, $10 5m cache write, $16 1h cache write, $0.20 cache hits (footnote 1: 0.025x), $40 output; fast mode $16/$80"
|
|
116
143
|
}
|
|
117
144
|
],
|
|
118
145
|
"models": [
|
|
@@ -150,6 +177,23 @@
|
|
|
150
177
|
}
|
|
151
178
|
]
|
|
152
179
|
},
|
|
180
|
+
{
|
|
181
|
+
"model_key": "claude-fable-5-2",
|
|
182
|
+
"aliases": [],
|
|
183
|
+
"fast_multiplier": "2.0",
|
|
184
|
+
"rates": [
|
|
185
|
+
{
|
|
186
|
+
"rate_id": "claude-fable-5-2-launch-2026-11-09",
|
|
187
|
+
"effective_from": "2026-11-09T00:00:00+00:00",
|
|
188
|
+
"effective_until": null,
|
|
189
|
+
"input_nocache": "8.0",
|
|
190
|
+
"cache_read": "0.20",
|
|
191
|
+
"cache_write_5m": "10.0",
|
|
192
|
+
"cache_write_1h": "16.0",
|
|
193
|
+
"output": "40.0"
|
|
194
|
+
}
|
|
195
|
+
]
|
|
196
|
+
},
|
|
153
197
|
{
|
|
154
198
|
"model_key": "claude-mythos-5",
|
|
155
199
|
"aliases": [],
|
|
@@ -167,6 +211,23 @@
|
|
|
167
211
|
}
|
|
168
212
|
]
|
|
169
213
|
},
|
|
214
|
+
{
|
|
215
|
+
"model_key": "claude-mythos-5-2",
|
|
216
|
+
"aliases": [],
|
|
217
|
+
"fast_multiplier": "2.0",
|
|
218
|
+
"rates": [
|
|
219
|
+
{
|
|
220
|
+
"rate_id": "claude-mythos-5-2-launch-2026-11-09",
|
|
221
|
+
"effective_from": "2026-11-09T00:00:00+00:00",
|
|
222
|
+
"effective_until": null,
|
|
223
|
+
"input_nocache": "8.0",
|
|
224
|
+
"cache_read": "0.20",
|
|
225
|
+
"cache_write_5m": "10.0",
|
|
226
|
+
"cache_write_1h": "16.0",
|
|
227
|
+
"output": "40.0"
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
},
|
|
170
231
|
{
|
|
171
232
|
"model_key": "claude-opus-4-8",
|
|
172
233
|
"aliases": [],
|
|
@@ -304,12 +365,22 @@
|
|
|
304
365
|
{
|
|
305
366
|
"rate_id": "claude-sonnet-5-5-launch-2026-09-28",
|
|
306
367
|
"effective_from": "2026-09-28T00:00:00+00:00",
|
|
307
|
-
"effective_until":
|
|
368
|
+
"effective_until": "2026-10-07T00:00:00+00:00",
|
|
308
369
|
"input_nocache": "2.0",
|
|
309
370
|
"cache_read": "0.20",
|
|
310
371
|
"cache_write_5m": "2.50",
|
|
311
372
|
"cache_write_1h": "4.0",
|
|
312
373
|
"output": "10.0"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"rate_id": "claude-sonnet-5-5-cache-read-cut-2026-10-07",
|
|
377
|
+
"effective_from": "2026-10-07T00:00:00+00:00",
|
|
378
|
+
"effective_until": null,
|
|
379
|
+
"input_nocache": "2.0",
|
|
380
|
+
"cache_read": "0.10",
|
|
381
|
+
"cache_write_5m": "2.50",
|
|
382
|
+
"cache_write_1h": "4.0",
|
|
383
|
+
"output": "10.0"
|
|
313
384
|
}
|
|
314
385
|
]
|
|
315
386
|
},
|
|
@@ -668,6 +739,78 @@
|
|
|
668
739
|
"output": "50.0"
|
|
669
740
|
}
|
|
670
741
|
]
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
"model_key": "gpt-6.1-terra",
|
|
745
|
+
"aliases": [],
|
|
746
|
+
"fast_multiplier": "1.0",
|
|
747
|
+
"credits_per_mtok": {
|
|
748
|
+
"input_nocache": "25.0",
|
|
749
|
+
"cache_read": "1.25",
|
|
750
|
+
"cache_write_5m": null,
|
|
751
|
+
"cache_write_1h": null,
|
|
752
|
+
"output": "125.0"
|
|
753
|
+
},
|
|
754
|
+
"rates": [
|
|
755
|
+
{
|
|
756
|
+
"rate_id": "gpt-6.1-terra-launch-date-unconfirmed",
|
|
757
|
+
"effective_from": "2026-10-01T00:00:00+00:00",
|
|
758
|
+
"effective_until": null,
|
|
759
|
+
"input_nocache": "1.00",
|
|
760
|
+
"cache_read": "0.05",
|
|
761
|
+
"cache_write_5m": null,
|
|
762
|
+
"cache_write_1h": null,
|
|
763
|
+
"output": "5.00"
|
|
764
|
+
}
|
|
765
|
+
]
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
"model_key": "gpt-6.1-sol",
|
|
769
|
+
"aliases": [],
|
|
770
|
+
"fast_multiplier": "1.0",
|
|
771
|
+
"credits_per_mtok": {
|
|
772
|
+
"input_nocache": "50.0",
|
|
773
|
+
"cache_read": "2.5",
|
|
774
|
+
"cache_write_5m": null,
|
|
775
|
+
"cache_write_1h": null,
|
|
776
|
+
"output": "250.0"
|
|
777
|
+
},
|
|
778
|
+
"rates": [
|
|
779
|
+
{
|
|
780
|
+
"rate_id": "gpt-6.1-sol-launch-date-unconfirmed",
|
|
781
|
+
"effective_from": "2026-10-01T00:00:00+00:00",
|
|
782
|
+
"effective_until": null,
|
|
783
|
+
"input_nocache": "2.00",
|
|
784
|
+
"cache_read": "0.10",
|
|
785
|
+
"cache_write_5m": null,
|
|
786
|
+
"cache_write_1h": null,
|
|
787
|
+
"output": "10.00"
|
|
788
|
+
}
|
|
789
|
+
]
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
"model_key": "codex-auto-review",
|
|
793
|
+
"aliases": [],
|
|
794
|
+
"fast_multiplier": "1.0",
|
|
795
|
+
"credits_per_mtok": {
|
|
796
|
+
"input_nocache": "25.0",
|
|
797
|
+
"cache_read": "1.25",
|
|
798
|
+
"cache_write_5m": null,
|
|
799
|
+
"cache_write_1h": null,
|
|
800
|
+
"output": "125.0"
|
|
801
|
+
},
|
|
802
|
+
"rates": [
|
|
803
|
+
{
|
|
804
|
+
"rate_id": "codex-auto-review-2026-11-terra-equivalent",
|
|
805
|
+
"effective_from": "2026-10-01T00:00:00+00:00",
|
|
806
|
+
"effective_until": null,
|
|
807
|
+
"input_nocache": "1.00",
|
|
808
|
+
"cache_read": "0.05",
|
|
809
|
+
"cache_write_5m": null,
|
|
810
|
+
"cache_write_1h": null,
|
|
811
|
+
"output": "5.00"
|
|
812
|
+
}
|
|
813
|
+
]
|
|
671
814
|
}
|
|
672
815
|
]
|
|
673
816
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-agent-cost
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Estimate AI coding agent (Claude Code / Codex CLI) token usage and cost from local logs
|
|
5
5
|
Author: shiki-yusuke
|
|
6
6
|
License: MIT
|
|
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
22
|
Classifier: Topic :: Software Development :: Build Tools
|
|
21
23
|
Classifier: Topic :: Utilities
|
|
22
24
|
Requires-Python: >=3.9
|
|
@@ -24,7 +24,9 @@ tests/test_billing_plan.py
|
|
|
24
24
|
tests/test_cli.py
|
|
25
25
|
tests/test_e0a_fable_5_1_and_sonnet_5_correction.py
|
|
26
26
|
tests/test_e0a_review3_hardening.py
|
|
27
|
+
tests/test_fable_5_2_rates.py
|
|
27
28
|
tests/test_facts.py
|
|
29
|
+
tests/test_gpt_6_1_rates.py
|
|
28
30
|
tests/test_haiku_5_5_rates.py
|
|
29
31
|
tests/test_measure_v1_contract.py
|
|
30
32
|
tests/test_opus_5_5_rates.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "coding-agent-cost"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.3"
|
|
8
8
|
description = "Estimate AI coding agent (Claude Code / Codex CLI) token usage and cost from local logs"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -31,6 +31,8 @@ classifiers = [
|
|
|
31
31
|
"Programming Language :: Python :: 3.10",
|
|
32
32
|
"Programming Language :: Python :: 3.11",
|
|
33
33
|
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3.13",
|
|
35
|
+
"Programming Language :: Python :: 3.14",
|
|
34
36
|
"Topic :: Software Development :: Build Tools",
|
|
35
37
|
"Topic :: Utilities",
|
|
36
38
|
]
|
|
@@ -45,7 +45,7 @@ def test_standard_fast_and_unknown_mode_prices(kind, usd, credits, mode, multipl
|
|
|
45
45
|
|
|
46
46
|
def test_catalog_observation_boundary_is_not_backdated():
|
|
47
47
|
catalog = load_rates()
|
|
48
|
-
assert catalog.catalog_version == "2026-10
|
|
48
|
+
assert catalog.catalog_version == "2026-11-10"
|
|
49
49
|
entry = catalog.models["gpt-6-astra"]
|
|
50
50
|
assert entry.aliases == ()
|
|
51
51
|
assert entry.rates[0].effective_from == CUTOFF
|
|
@@ -149,7 +149,7 @@ def test_measure_reads_isolated_synthetic_astra_with_existing_contract(tmp_path,
|
|
|
149
149
|
assert cli.main(["measure", "--session-id", "synthetic-astra", "--agent", "codex"]) == 0
|
|
150
150
|
report = json.loads(capsys.readouterr().out)
|
|
151
151
|
assert report["protocol_version"] == "measure/v1"
|
|
152
|
-
assert report["rates"]["catalog_version"] == "2026-10
|
|
152
|
+
assert report["rates"]["catalog_version"] == "2026-11-10"
|
|
153
153
|
assert len(report["rates"]["sha256"]) == 64
|
|
154
154
|
assert report["sessions"]["synthetic-astra"]["totals"]["estimated_cost_usd"] == 122
|
|
155
155
|
assert report["total"]["totals"]["credits"] == 3050
|
|
@@ -13,6 +13,8 @@ import copy
|
|
|
13
13
|
import hashlib
|
|
14
14
|
import json
|
|
15
15
|
import os
|
|
16
|
+
|
|
17
|
+
_REAL_FSTAT = os.fstat
|
|
16
18
|
import traceback
|
|
17
19
|
from datetime import datetime, timezone
|
|
18
20
|
from decimal import Decimal
|
|
@@ -583,11 +585,24 @@ def test_realpath_oserror_is_billing_plan_error(tmp_path, monkeypatch):
|
|
|
583
585
|
_assert_no_leak(info.value, path)
|
|
584
586
|
|
|
585
587
|
|
|
588
|
+
def _is_fd_of(fd, path):
|
|
589
|
+
"""True when fd refers to path. Keeps fakes scoped to the plan file: pytest's
|
|
590
|
+
capture machinery calls os.fstat/os.read/os.close on its own fds (pytest 9.2+)."""
|
|
591
|
+
try:
|
|
592
|
+
st = _REAL_FSTAT(fd)
|
|
593
|
+
except OSError:
|
|
594
|
+
return False
|
|
595
|
+
target = os.stat(path)
|
|
596
|
+
return (st.st_dev, st.st_ino) == (target.st_dev, target.st_ino)
|
|
597
|
+
|
|
598
|
+
|
|
586
599
|
def test_fstat_oserror_is_billing_plan_error(tmp_path, monkeypatch):
|
|
587
600
|
path = _write_plan(tmp_path, _base_plan())
|
|
588
601
|
|
|
589
602
|
def failing_fstat(fd):
|
|
590
|
-
|
|
603
|
+
if _is_fd_of(fd, path):
|
|
604
|
+
raise _oserror_on(path)
|
|
605
|
+
return _REAL_FSTAT(fd)
|
|
591
606
|
|
|
592
607
|
monkeypatch.setattr(bp.os, "fstat", failing_fstat)
|
|
593
608
|
with pytest.raises(bp.BillingPlanError) as info:
|
|
@@ -598,8 +613,12 @@ def test_fstat_oserror_is_billing_plan_error(tmp_path, monkeypatch):
|
|
|
598
613
|
def test_read_oserror_is_billing_plan_error(tmp_path, monkeypatch):
|
|
599
614
|
path = _write_plan(tmp_path, _base_plan())
|
|
600
615
|
|
|
616
|
+
real_read = os.read
|
|
617
|
+
|
|
601
618
|
def failing_read(fd, n):
|
|
602
|
-
|
|
619
|
+
if _is_fd_of(fd, path):
|
|
620
|
+
raise _oserror_on(path)
|
|
621
|
+
return real_read(fd, n)
|
|
603
622
|
|
|
604
623
|
monkeypatch.setattr(bp.os, "read", failing_read)
|
|
605
624
|
with pytest.raises(bp.BillingPlanError) as info:
|
|
@@ -612,8 +631,10 @@ def test_close_oserror_is_billing_plan_error(tmp_path, monkeypatch):
|
|
|
612
631
|
real_close = os.close
|
|
613
632
|
|
|
614
633
|
def failing_close(fd):
|
|
634
|
+
mine = _is_fd_of(fd, path)
|
|
615
635
|
real_close(fd)
|
|
616
|
-
|
|
636
|
+
if mine:
|
|
637
|
+
raise _oserror_on(path)
|
|
617
638
|
|
|
618
639
|
monkeypatch.setattr(bp.os, "close", failing_close)
|
|
619
640
|
with pytest.raises(bp.BillingPlanError) as info:
|
|
@@ -626,8 +647,10 @@ def test_close_oserror_does_not_replace_an_earlier_error(tmp_path, monkeypatch):
|
|
|
626
647
|
real_close = os.close
|
|
627
648
|
|
|
628
649
|
def failing_close(fd):
|
|
650
|
+
mine = _is_fd_of(fd, path)
|
|
629
651
|
real_close(fd)
|
|
630
|
-
|
|
652
|
+
if mine:
|
|
653
|
+
raise _oserror_on(path)
|
|
631
654
|
|
|
632
655
|
monkeypatch.setattr(bp.os, "close", failing_close)
|
|
633
656
|
with pytest.raises(bp.BillingPlanError) as info:
|
|
@@ -886,7 +886,7 @@ def test_rates_show_model_without_tiers_prints_no_prompt_tiers(capsys):
|
|
|
886
886
|
def test_rates_validate_packaged_catalog_with_prompt_tiers_exits_zero(capsys):
|
|
887
887
|
rc = cli.main(["rates", "validate"])
|
|
888
888
|
assert rc == 0
|
|
889
|
-
assert "catalog_version=2026-10
|
|
889
|
+
assert "catalog_version=2026-11-10" in capsys.readouterr().out
|
|
890
890
|
|
|
891
891
|
|
|
892
892
|
# ---------------------------------------------------------------------------
|
|
@@ -19,7 +19,7 @@ architect レビュー (3 巡目) と builder レビューで、既存テスト
|
|
|
19
19
|
かつ確定金額が計上されない。境界ちょうどは priced になる。
|
|
20
20
|
|
|
21
21
|
条件D (PR レビュー3点目・追加分): catalog は
|
|
22
|
-
catalog_version == "2026-10
|
|
22
|
+
catalog_version == "2026-11-10"(rates.json の現行値)であり、sources の中に url
|
|
23
23
|
"https://platform.claude.com/docs/en/about-claude/pricing" の
|
|
24
24
|
entry があって、その note に
|
|
25
25
|
"sha256=d79ad28567196bd55dd50e2fd89341b9da9774a45c1d02fcf387078507fd15e0"
|
|
@@ -224,7 +224,7 @@ def test_condition_c_claude_fable_5_1_at_effective_from_boundary_is_priced_contr
|
|
|
224
224
|
|
|
225
225
|
|
|
226
226
|
def test_condition_d_catalog_version_and_pricing_source_sha256_note():
|
|
227
|
-
"""条件D: catalog_version == "2026-10
|
|
227
|
+
"""条件D: catalog_version == "2026-11-10"(rates.json の現行値)であり、sources に url
|
|
228
228
|
"https://platform.claude.com/docs/en/about-claude/pricing" の entry が
|
|
229
229
|
あって、その note に指定の sha256 文字列を含むこと。
|
|
230
230
|
|
|
@@ -233,7 +233,7 @@ def test_condition_d_catalog_version_and_pricing_source_sha256_note():
|
|
|
233
233
|
ダイジェストを貼り付けてしまう。
|
|
234
234
|
"""
|
|
235
235
|
catalog = load_rates()
|
|
236
|
-
assert catalog.catalog_version == "2026-10
|
|
236
|
+
assert catalog.catalog_version == "2026-11-10"
|
|
237
237
|
|
|
238
238
|
matches = [
|
|
239
239
|
s
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Acceptance tests for claude-fable-5-2 / claude-mythos-5-2 (catalog_version 2026-11-10).
|
|
2
|
+
|
|
3
|
+
Source: platform.claude.com/docs/en/about-claude/pricing (retrieved 2026-11-10) and the
|
|
4
|
+
announcement anthropic.com/claude-fable-and-mythos-5-2 (published 2026-11-09).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from decimal import Decimal
|
|
9
|
+
|
|
10
|
+
from agent_cost.rates import load_rates
|
|
11
|
+
|
|
12
|
+
USD = {
|
|
13
|
+
"input_nocache": "8.0",
|
|
14
|
+
"cache_read": "0.20",
|
|
15
|
+
"cache_write_5m": "10.0",
|
|
16
|
+
"cache_write_1h": "16.0",
|
|
17
|
+
"output": "40.0",
|
|
18
|
+
}
|
|
19
|
+
MODELS = ("claude-fable-5-2", "claude-mythos-5-2")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def dt(s):
|
|
23
|
+
return datetime.fromisoformat(s)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_fable_and_mythos_5_2_resolve_with_pricing_page_values_from_launch():
|
|
27
|
+
catalog = load_rates()
|
|
28
|
+
for model in MODELS:
|
|
29
|
+
for occurred_at in ("2026-11-09T00:00:00+00:00", "2026-11-10T03:00:00+00:00", "2027-06-01T00:00:00+00:00"):
|
|
30
|
+
resolved, period = catalog.rate_for(model, dt(occurred_at))
|
|
31
|
+
assert resolved == model
|
|
32
|
+
assert period is not None, (model, occurred_at)
|
|
33
|
+
assert period.rate_id == f"{model}-launch-2026-11-09"
|
|
34
|
+
assert period.effective_until is None
|
|
35
|
+
assert period.prompt_tiers == ()
|
|
36
|
+
for kind, value in USD.items():
|
|
37
|
+
assert period.values[kind] == Decimal(value), (model, kind)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_fable_and_mythos_5_2_are_unpriced_before_launch():
|
|
41
|
+
catalog = load_rates()
|
|
42
|
+
for model in MODELS:
|
|
43
|
+
resolved, period = catalog.rate_for(model, dt("2026-11-08T23:59:59+00:00"))
|
|
44
|
+
assert resolved == model
|
|
45
|
+
assert period is None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_cache_read_is_0_025x_and_fast_mode_is_2x():
|
|
49
|
+
catalog = load_rates()
|
|
50
|
+
for model in MODELS:
|
|
51
|
+
entry = catalog.models[model]
|
|
52
|
+
assert entry.aliases == ()
|
|
53
|
+
assert entry.fast_multiplier == Decimal("2.0") # fast mode $16 / $80
|
|
54
|
+
assert len(entry.rates) == 1
|
|
55
|
+
_, period = catalog.rate_for(model, dt("2026-11-10T00:00:00+00:00"))
|
|
56
|
+
assert period.values["cache_read"] == period.values["input_nocache"] * Decimal("0.025")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_separate_entries_not_aliases():
|
|
60
|
+
catalog = load_rates()
|
|
61
|
+
for model in MODELS:
|
|
62
|
+
for other in catalog.models.values():
|
|
63
|
+
assert model not in other.aliases
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_fable_5_1_is_unchanged_and_mythos_5_1_stays_unpriced():
|
|
67
|
+
catalog = load_rates()
|
|
68
|
+
_, period = catalog.rate_for("claude-fable-5-1", dt("2026-11-10T00:00:00+00:00"))
|
|
69
|
+
assert period.rate_id == "claude-fable-5-1-local-first-observed-2026-09-02"
|
|
70
|
+
assert period.values["input_nocache"] == Decimal("10.0")
|
|
71
|
+
assert period.values["cache_read"] == Decimal("0.25")
|
|
72
|
+
assert period.values["output"] == Decimal("50.0")
|
|
73
|
+
assert "claude-mythos-5-1" not in catalog.models
|
|
74
|
+
_, period = catalog.rate_for("claude-mythos-5-1", dt("2026-11-10T00:00:00+00:00"))
|
|
75
|
+
assert period is None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_catalog_version_and_sources_record_fable_5_2():
|
|
79
|
+
catalog = load_rates()
|
|
80
|
+
assert catalog.catalog_version == "2026-11-10"
|
|
81
|
+
urls = [(s.get("url"), s.get("retrieved_at")) for s in catalog.sources]
|
|
82
|
+
assert ("https://www.anthropic.com/claude-fable-and-mythos-5-2", "2026-11-10") in urls
|
|
83
|
+
assert ("https://platform.claude.com/docs/en/about-claude/pricing", "2026-11-10") in urls
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Acceptance tests for gpt-6.1-terra / gpt-6.1-sol / codex-auto-review (catalog_version 2026-11-09).
|
|
2
|
+
|
|
3
|
+
Source: human-approved price table pricing-2026-11-v4 (developers.openai.com/api/docs/pricing,
|
|
4
|
+
retrieved 2026-11-05; standard tier, short context). codex-auto-review (Codex automatic
|
|
5
|
+
review / guardian subagent) has no published price; priced as gpt-6.1-terra by a human
|
|
6
|
+
decision on 2026-11-06.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from decimal import Decimal
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import agent_cost
|
|
15
|
+
from agent_cost.rates import load_rates
|
|
16
|
+
|
|
17
|
+
UTC = timezone.utc
|
|
18
|
+
|
|
19
|
+
EXPECTED = {
|
|
20
|
+
"gpt-6.1-terra": (
|
|
21
|
+
"gpt-6.1-terra-launch-date-unconfirmed",
|
|
22
|
+
{"input_nocache": "1.00", "cache_read": "0.05", "output": "5.00"},
|
|
23
|
+
{"input_nocache": "25.0", "cache_read": "1.25", "output": "125.0"},
|
|
24
|
+
),
|
|
25
|
+
"gpt-6.1-sol": (
|
|
26
|
+
"gpt-6.1-sol-launch-date-unconfirmed",
|
|
27
|
+
{"input_nocache": "2.00", "cache_read": "0.10", "output": "10.00"},
|
|
28
|
+
{"input_nocache": "50.0", "cache_read": "2.5", "output": "250.0"},
|
|
29
|
+
),
|
|
30
|
+
"codex-auto-review": (
|
|
31
|
+
"codex-auto-review-2026-11-terra-equivalent",
|
|
32
|
+
{"input_nocache": "1.00", "cache_read": "0.05", "output": "5.00"},
|
|
33
|
+
{"input_nocache": "25.0", "cache_read": "1.25", "output": "125.0"},
|
|
34
|
+
),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def dt(s):
|
|
39
|
+
return datetime.fromisoformat(s)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_gpt_6_1_models_resolve_with_pricing_page_values_from_effective_from():
|
|
43
|
+
catalog = load_rates()
|
|
44
|
+
for model, (rate_id, usd, _credits) in EXPECTED.items():
|
|
45
|
+
for occurred_at in ("2026-10-01T00:00:00+00:00", "2026-11-04T07:49:42+00:00", "2027-01-01T00:00:00+00:00"):
|
|
46
|
+
resolved, period = catalog.rate_for(model, dt(occurred_at))
|
|
47
|
+
assert resolved == model
|
|
48
|
+
assert period is not None, (model, occurred_at)
|
|
49
|
+
assert period.rate_id == rate_id
|
|
50
|
+
assert period.effective_until is None
|
|
51
|
+
for kind, value in usd.items():
|
|
52
|
+
assert period.values[kind] == Decimal(value), (model, kind)
|
|
53
|
+
assert period.values["cache_write_5m"] is None
|
|
54
|
+
assert period.values["cache_write_1h"] is None
|
|
55
|
+
assert period.prompt_tiers == ()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_gpt_6_1_models_are_unpriced_before_effective_from():
|
|
59
|
+
catalog = load_rates()
|
|
60
|
+
for model in EXPECTED:
|
|
61
|
+
resolved, period = catalog.rate_for(model, dt("2026-09-30T23:59:59+00:00"))
|
|
62
|
+
assert resolved == model
|
|
63
|
+
assert period is None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_gpt_6_1_models_credits_follow_usd_per_credit_and_entry_shape():
|
|
67
|
+
catalog = load_rates()
|
|
68
|
+
usd_per_credit = Decimal(catalog.usd_per_credit)
|
|
69
|
+
for model, (_rate_id, usd, credits) in EXPECTED.items():
|
|
70
|
+
entry = catalog.models[model]
|
|
71
|
+
assert entry.aliases == ()
|
|
72
|
+
assert entry.fast_multiplier == Decimal("1.0")
|
|
73
|
+
assert entry.credits_per_mtok["cache_write_5m"] is None
|
|
74
|
+
assert entry.credits_per_mtok["cache_write_1h"] is None
|
|
75
|
+
for kind, value in credits.items():
|
|
76
|
+
assert entry.credits_per_mtok[kind] == Decimal(value), (model, kind)
|
|
77
|
+
assert entry.credits_per_mtok[kind] * usd_per_credit == Decimal(usd[kind])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_codex_auto_review_is_priced_like_gpt_6_1_terra_but_is_its_own_entry():
|
|
81
|
+
catalog = load_rates()
|
|
82
|
+
at = dt("2026-11-06T00:00:00+00:00")
|
|
83
|
+
_, terra = catalog.rate_for("gpt-6.1-terra", at)
|
|
84
|
+
_, review = catalog.rate_for("codex-auto-review", at)
|
|
85
|
+
assert review.values == terra.values
|
|
86
|
+
assert review.rate_id != terra.rate_id
|
|
87
|
+
assert "codex-auto-review" not in catalog.models["gpt-6.1-terra"].aliases
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_unknown_gpt_6_1_luna_stays_unpriced():
|
|
91
|
+
catalog = load_rates()
|
|
92
|
+
assert "gpt-6.1-luna" not in catalog.models
|
|
93
|
+
resolved, period = catalog.rate_for("gpt-6.1-luna", dt("2026-11-06T00:00:00+00:00"))
|
|
94
|
+
assert period is None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_existing_gpt_5_6_terra_values_are_unchanged():
|
|
98
|
+
catalog = load_rates()
|
|
99
|
+
entry = catalog.models["gpt-5.6-terra"]
|
|
100
|
+
assert len(entry.rates) == 1
|
|
101
|
+
_, period = catalog.rate_for("gpt-5.6-terra", dt("2026-11-06T00:00:00+00:00"))
|
|
102
|
+
assert period.rate_id == "gpt-5.6-terra-2026-07-secondary-sourced"
|
|
103
|
+
assert period.values["input_nocache"] == Decimal("2.50")
|
|
104
|
+
assert period.values["cache_read"] == Decimal("0.25")
|
|
105
|
+
assert period.values["output"] == Decimal("15.0")
|
|
106
|
+
assert entry.credits_per_mtok["input_nocache"] == Decimal("62.5")
|
|
107
|
+
assert entry.credits_per_mtok["cache_read"] == Decimal("6.25")
|
|
108
|
+
assert entry.credits_per_mtok["output"] == Decimal("375.0")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_catalog_version_and_source_record_the_gpt_6_1_pricing_page():
|
|
112
|
+
catalog = load_rates()
|
|
113
|
+
assert catalog.catalog_version >= "2026-11-09"
|
|
114
|
+
urls = [(s.get("url"), s.get("retrieved_at")) for s in catalog.sources]
|
|
115
|
+
assert urls.count(("https://developers.openai.com/api/docs/pricing", "2026-11-05")) == 1
|
|
116
|
+
raw = json.loads((Path(agent_cost.__file__).parent / "rates.json").read_text(encoding="utf-8"))
|
|
117
|
+
assert any("codex-auto-review" in n and "2026-11-06" in n for n in raw["notes"])
|
|
@@ -79,7 +79,7 @@ def test_claude_haiku_5_5_cache_multipliers_are_standard_in_every_tier_and_no_fa
|
|
|
79
79
|
|
|
80
80
|
def test_catalog_version_schema_version_and_sources_record_the_haiku_5_5_primary_sources():
|
|
81
81
|
catalog = load_rates()
|
|
82
|
-
assert catalog.catalog_version == "2026-10
|
|
82
|
+
assert catalog.catalog_version == "2026-11-10"
|
|
83
83
|
assert catalog.schema_version == "2"
|
|
84
84
|
urls = {(s.get("url"), s.get("retrieved_at")) for s in catalog.sources}
|
|
85
85
|
assert ("https://platform.claude.com/docs/en/about-claude/pricing", "2026-10-09") in urls
|
|
@@ -514,8 +514,10 @@ def test_packaged_catalog_only_claude_haiku_5_5_has_prompt_tiers():
|
|
|
514
514
|
|
|
515
515
|
|
|
516
516
|
# sha256 of the packaged catalog's models other than claude-haiku-5-5, as
|
|
517
|
-
# json.dumps(sort_keys=True, separators=(",", ":")).
|
|
518
|
-
|
|
517
|
+
# json.dumps(sort_keys=True, separators=(",", ":")). Updated deliberately in
|
|
518
|
+
# 0.5.2: claude-sonnet-5-5's launch period gained effective_until 2026-10-07
|
|
519
|
+
# and a cache_read-cut period was appended (was f3bfe615...2e90d9).
|
|
520
|
+
_EXISTING_MODELS_SHA256 = "1f326f1446e013fef49070e22113f78875fb3f8e7e8a65bd7ed4c5ca819e61fa"
|
|
519
521
|
|
|
520
522
|
|
|
521
523
|
def test_packaged_catalog_existing_models_are_unchanged():
|
|
@@ -525,6 +527,7 @@ def test_packaged_catalog_existing_models_are_unchanged():
|
|
|
525
527
|
rows are never changed in place; a price change adds a new period.
|
|
526
528
|
"""
|
|
527
529
|
raw = json.loads(resources.files("agent_cost").joinpath("rates.json").read_bytes())
|
|
528
|
-
|
|
530
|
+
added_after = {"claude-haiku-5-5", "gpt-6.1-terra", "gpt-6.1-sol", "codex-auto-review", "claude-fable-5-2", "claude-mythos-5-2"}
|
|
531
|
+
existing = [m for m in raw["models"] if m["model_key"] not in added_after]
|
|
529
532
|
digest = hashlib.sha256(json.dumps(existing, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
530
533
|
assert digest == _EXISTING_MODELS_SHA256
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Acceptance tests for the claude-sonnet-5-5 rate entry (catalog_version 2026-10-13).
|
|
2
|
+
|
|
3
|
+
Primary sources: platform.claude.com/docs/en/about-claude/pricing (Sonnet 5.5
|
|
4
|
+
row; retrieved 2026-09-29 with no cache_read footnote, retrieved 2026-10-13 at
|
|
5
|
+
$0.10 cache_read with footnote 2 = 0.05x), anthropic.com/claude-sonnet-5-5
|
|
6
|
+
(launch 2026-09-28), anthropic.com/claude-haiku-5-5 (cache_read cut 'starting
|
|
7
|
+
today', 2026-10-07), Claude Code CHANGELOG 2.1.284 (Sonnet 5.5 is the default
|
|
8
|
+
Sonnet) and 2.1.296 (Sonnet 5.5 cache reads priced at $0.10).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
|
|
14
|
+
from agent_cost.rates import load_rates
|
|
15
|
+
|
|
16
|
+
UTC = timezone.utc
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def dt(s):
|
|
20
|
+
return datetime.fromisoformat(s).replace(tzinfo=UTC)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
LAUNCH = "claude-sonnet-5-5-launch-2026-09-28"
|
|
24
|
+
CACHE_READ_CUT = "claude-sonnet-5-5-cache-read-cut-2026-10-07"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _assert_unchanged_values(period):
|
|
28
|
+
assert period.values["input_nocache"] == Decimal("2.0")
|
|
29
|
+
assert period.values["cache_write_5m"] == Decimal("2.50")
|
|
30
|
+
assert period.values["cache_write_1h"] == Decimal("4.0")
|
|
31
|
+
assert period.values["output"] == Decimal("10.0")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_claude_sonnet_5_5_resolves_to_launch_period_before_cache_read_cut():
|
|
35
|
+
catalog = load_rates()
|
|
36
|
+
for occurred_at in ("2026-09-28T00:00:00+00:00", "2026-10-06T23:59:59+00:00"):
|
|
37
|
+
resolved, period = catalog.rate_for("claude-sonnet-5-5", dt(occurred_at))
|
|
38
|
+
assert resolved == "claude-sonnet-5-5"
|
|
39
|
+
assert period is not None
|
|
40
|
+
assert period.rate_id == LAUNCH
|
|
41
|
+
assert period.values["cache_read"] == Decimal("0.20")
|
|
42
|
+
_assert_unchanged_values(period)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_claude_sonnet_5_5_resolves_to_cache_read_cut_period_from_2026_10_07():
|
|
46
|
+
catalog = load_rates()
|
|
47
|
+
for occurred_at in ("2026-10-07T00:00:00+00:00", "2026-12-01T00:00:00+00:00"):
|
|
48
|
+
resolved, period = catalog.rate_for("claude-sonnet-5-5", dt(occurred_at))
|
|
49
|
+
assert resolved == "claude-sonnet-5-5"
|
|
50
|
+
assert period is not None
|
|
51
|
+
assert period.rate_id == CACHE_READ_CUT
|
|
52
|
+
assert period.values["cache_read"] == Decimal("0.10")
|
|
53
|
+
_assert_unchanged_values(period)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_claude_sonnet_5_5_cache_read_cut_boundary():
|
|
57
|
+
catalog = load_rates()
|
|
58
|
+
_, before = catalog.rate_for("claude-sonnet-5-5", dt("2026-10-06T23:59:59+00:00"))
|
|
59
|
+
_, at = catalog.rate_for("claude-sonnet-5-5", dt("2026-10-07T00:00:00+00:00"))
|
|
60
|
+
assert before.rate_id == LAUNCH
|
|
61
|
+
assert before.values["cache_read"] == Decimal("0.20")
|
|
62
|
+
assert at.rate_id == CACHE_READ_CUT
|
|
63
|
+
assert at.values["cache_read"] == Decimal("0.10")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_claude_sonnet_5_5_is_unpriced_before_launch_date():
|
|
67
|
+
catalog = load_rates()
|
|
68
|
+
resolved, before = catalog.rate_for("claude-sonnet-5-5", dt("2026-09-27T23:59:59+00:00"))
|
|
69
|
+
assert resolved == "claude-sonnet-5-5"
|
|
70
|
+
assert before is None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_claude_sonnet_5_5_is_its_own_entry_not_an_alias_of_sonnet_5():
|
|
74
|
+
# Same values as claude-sonnet-5 today, but the pricing page lists the rows
|
|
75
|
+
# independently; a future change to one must not silently apply to the other.
|
|
76
|
+
catalog = load_rates()
|
|
77
|
+
assert "claude-sonnet-5-5" in catalog.models
|
|
78
|
+
assert catalog.models["claude-sonnet-5-5"].aliases == ()
|
|
79
|
+
assert "claude-sonnet-5-5" not in catalog.models["claude-sonnet-5"].aliases
|
|
80
|
+
_, s5 = catalog.rate_for("claude-sonnet-5", dt("2026-09-28T00:00:00+00:00"))
|
|
81
|
+
_, s55 = catalog.rate_for("claude-sonnet-5-5", dt("2026-09-28T00:00:00+00:00"))
|
|
82
|
+
assert s5.rate_id != s55.rate_id
|
|
83
|
+
assert s5.values == s55.values
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_claude_sonnet_5_cache_read_is_not_cut_with_sonnet_5_5():
|
|
87
|
+
# The 2026-10-07 cut applies to Sonnet 5.5 only (pricing-page footnote 2
|
|
88
|
+
# names Opus 5.5 and Sonnet 5.5); claude-sonnet-5 keeps cache_read 0.20.
|
|
89
|
+
catalog = load_rates()
|
|
90
|
+
for occurred_at in ("2026-10-07T00:00:00+00:00", "2026-12-01T00:00:00+00:00"):
|
|
91
|
+
_, s5 = catalog.rate_for("claude-sonnet-5", dt(occurred_at))
|
|
92
|
+
_, s55 = catalog.rate_for("claude-sonnet-5-5", dt(occurred_at))
|
|
93
|
+
assert s5.values["cache_read"] == Decimal("0.20")
|
|
94
|
+
assert s55.values["cache_read"] == Decimal("0.10")
|
|
95
|
+
assert s5.values != s55.values
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_claude_sonnet_5_5_cache_read_multiplier_per_period_and_no_fast_mode():
|
|
99
|
+
catalog = load_rates()
|
|
100
|
+
entry = catalog.models["claude-sonnet-5-5"]
|
|
101
|
+
assert entry.fast_multiplier == Decimal("1.0")
|
|
102
|
+
periods = {p.rate_id: p for p in entry.rates}
|
|
103
|
+
assert set(periods) == {LAUNCH, CACHE_READ_CUT}
|
|
104
|
+
launch = periods[LAUNCH]
|
|
105
|
+
cut = periods[CACHE_READ_CUT]
|
|
106
|
+
assert launch.values["cache_read"] == launch.values["input_nocache"] * Decimal("0.1")
|
|
107
|
+
assert cut.values["cache_read"] == cut.values["input_nocache"] * Decimal("0.05")
|
|
108
|
+
for period in (launch, cut):
|
|
109
|
+
assert period.values["cache_write_5m"] == period.values["input_nocache"] * Decimal("1.25")
|
|
110
|
+
assert period.values["cache_write_1h"] == period.values["input_nocache"] * Decimal("2")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_catalog_version_and_sources_record_the_sonnet_5_5_primary_sources():
|
|
114
|
+
catalog = load_rates()
|
|
115
|
+
assert catalog.catalog_version == "2026-11-10"
|
|
116
|
+
urls = {(s.get("url"), s.get("retrieved_at")) for s in catalog.sources}
|
|
117
|
+
assert ("https://platform.claude.com/docs/en/about-claude/pricing", "2026-09-29") in urls
|
|
118
|
+
assert ("https://www.anthropic.com/claude-sonnet-5-5", "2026-09-29") in urls
|
|
119
|
+
assert ("https://platform.claude.com/docs/en/about-claude/pricing", "2026-10-13") in urls
|
|
120
|
+
assert ("https://www.anthropic.com/claude-haiku-5-5", "2026-10-13") in urls
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Spec: pyproject.toml's [project].version and agent_cost.__version__ must
|
|
2
|
-
agree, and both must read "0.5.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
agree, and both must read "0.5.3" for this change (the packaged catalog adds
|
|
3
|
+
claude-fable-5-2, claude-mythos-5-2, gpt-6.1-terra, gpt-6.1-sol and
|
|
4
|
+
codex-auto-review -- catalog entries only, with no code or schema change, so
|
|
5
|
+
it bumps the patch version). A broken
|
|
6
6
|
implementation would bump one file but not the other, or forget the bump
|
|
7
7
|
entirely.
|
|
8
8
|
|
|
@@ -30,6 +30,6 @@ def test_pyproject_version_matches_package_version():
|
|
|
30
30
|
assert _pyproject_version() == agent_cost.__version__
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def
|
|
34
|
-
assert agent_cost.__version__ == "0.5.
|
|
35
|
-
assert _pyproject_version() == "0.5.
|
|
33
|
+
def test_version_is_0_5_2():
|
|
34
|
+
assert agent_cost.__version__ == "0.5.3"
|
|
35
|
+
assert _pyproject_version() == "0.5.3"
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
"""Acceptance tests for the claude-sonnet-5-5 rate entry (catalog_version 2026-09-29).
|
|
2
|
-
|
|
3
|
-
Primary sources: platform.claude.com/docs/en/about-claude/pricing (Sonnet 5.5
|
|
4
|
-
row, no cache_read footnote), anthropic.com/claude-sonnet-5-5 (launch
|
|
5
|
-
2026-09-28), Claude Code CHANGELOG 2.1.284 (Sonnet 5.5 is the default Sonnet).
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from decimal import Decimal
|
|
10
|
-
|
|
11
|
-
from agent_cost.rates import load_rates
|
|
12
|
-
|
|
13
|
-
UTC = timezone.utc
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def dt(s):
|
|
17
|
-
return datetime.fromisoformat(s).replace(tzinfo=UTC)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_claude_sonnet_5_5_resolves_with_pricing_page_values_on_or_after_launch():
|
|
21
|
-
catalog = load_rates()
|
|
22
|
-
for occurred_at in ("2026-09-28T00:00:00+00:00", "2026-12-01T00:00:00+00:00"):
|
|
23
|
-
resolved, period = catalog.rate_for("claude-sonnet-5-5", dt(occurred_at))
|
|
24
|
-
assert resolved == "claude-sonnet-5-5"
|
|
25
|
-
assert period is not None
|
|
26
|
-
assert period.rate_id == "claude-sonnet-5-5-launch-2026-09-28"
|
|
27
|
-
assert period.values["input_nocache"] == Decimal("2.0")
|
|
28
|
-
assert period.values["cache_read"] == Decimal("0.20")
|
|
29
|
-
assert period.values["cache_write_5m"] == Decimal("2.50")
|
|
30
|
-
assert period.values["cache_write_1h"] == Decimal("4.0")
|
|
31
|
-
assert period.values["output"] == Decimal("10.0")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_claude_sonnet_5_5_is_unpriced_before_launch_date():
|
|
35
|
-
catalog = load_rates()
|
|
36
|
-
resolved, before = catalog.rate_for("claude-sonnet-5-5", dt("2026-09-27T23:59:59+00:00"))
|
|
37
|
-
assert resolved == "claude-sonnet-5-5"
|
|
38
|
-
assert before is None
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_claude_sonnet_5_5_is_its_own_entry_not_an_alias_of_sonnet_5():
|
|
42
|
-
# Same values as claude-sonnet-5 today, but the pricing page lists the rows
|
|
43
|
-
# independently; a future change to one must not silently apply to the other.
|
|
44
|
-
catalog = load_rates()
|
|
45
|
-
assert "claude-sonnet-5-5" in catalog.models
|
|
46
|
-
assert catalog.models["claude-sonnet-5-5"].aliases == ()
|
|
47
|
-
assert "claude-sonnet-5-5" not in catalog.models["claude-sonnet-5"].aliases
|
|
48
|
-
_, s5 = catalog.rate_for("claude-sonnet-5", dt("2026-09-28T00:00:00+00:00"))
|
|
49
|
-
_, s55 = catalog.rate_for("claude-sonnet-5-5", dt("2026-09-28T00:00:00+00:00"))
|
|
50
|
-
assert s5.rate_id != s55.rate_id
|
|
51
|
-
assert s5.values == s55.values
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_claude_sonnet_5_5_cache_read_is_standard_multiplier_and_no_fast_mode():
|
|
55
|
-
catalog = load_rates()
|
|
56
|
-
entry = catalog.models["claude-sonnet-5-5"]
|
|
57
|
-
assert entry.fast_multiplier == Decimal("1.0")
|
|
58
|
-
period = entry.rates[0]
|
|
59
|
-
assert period.values["cache_read"] == period.values["input_nocache"] * Decimal("0.1")
|
|
60
|
-
assert period.values["cache_write_5m"] == period.values["input_nocache"] * Decimal("1.25")
|
|
61
|
-
assert period.values["cache_write_1h"] == period.values["input_nocache"] * Decimal("2")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_catalog_version_and_sources_record_the_sonnet_5_5_primary_sources():
|
|
65
|
-
catalog = load_rates()
|
|
66
|
-
assert catalog.catalog_version == "2026-10-09"
|
|
67
|
-
urls = {(s.get("url"), s.get("retrieved_at")) for s in catalog.sources}
|
|
68
|
-
assert ("https://platform.claude.com/docs/en/about-claude/pricing", "2026-09-29") in urls
|
|
69
|
-
assert ("https://www.anthropic.com/claude-sonnet-5-5", "2026-09-29") in urls
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{coding_agent_cost-0.5.1 → coding_agent_cost-0.5.3}/coding_agent_cost.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|