Skip to content

Commit 8e75017

Browse files
committed
release: v1.108.130 — correct receipt price table to current Opus pricing (+ Fable 5)
1 parent c11a0a0 commit 8e75017

5 files changed

Lines changed: 58 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,33 @@
22

33
All notable changes to jcodemunch-mcp are documented here.
44

5+
## [1.108.130] - 2026-07-16 - receipt price table corrected to current Opus pricing (+ Fable 5)
6+
7+
The `receipt` CLI's model price table carried a stale Opus input rate of
8+
$15/MTok (the retired Opus 4.0/4.1 price). Current Opus (4.8/4.7/4.6) is
9+
$5/MTok, so `receipt --model opus` overstated dollar savings roughly 3x.
10+
11+
### Fixed
12+
- `cli/receipt.py`: `_MODEL_PRICES_USD_PER_MTOK` corrected to the live rates
13+
(Opus $5, Sonnet $3, Haiku $1). Comment now cites the dated source
14+
(anthropic.com/pricing, 2026-06-24).
15+
- `tests/test_receipt.py`: the two assertions that pinned the stale rates
16+
($15 Opus, $0.80 Haiku) updated to the correct values.
17+
18+
### Added
19+
- Claude Fable 5 ($10/MTok) as a selectable `--model` value and an
20+
alternate-model comparison line (both derive from the price table, so they
21+
pick it up automatically).
22+
- A dated-source rate pin over the whole table in `tests/test_receipt.py`, so a
23+
future price drift fails loudly instead of silently misreporting.
24+
25+
Present-value note: `receipt` is a per-call estimator ("what this session would
26+
cost at current prices"). It does not feed the public token-savings counter,
27+
which stores tokens (not dollars) and values them at display time. jcm's own
28+
`storage/token_tracker.PRICING` was already correct at $5; `receipt` was the
29+
sole stale spot. No INDEX_VERSION bump, no tool-count change, no wire-shape
30+
change.
31+
532
## [1.108.129] - 2026-07-14 - Keystone-protected structural compression for get_ranked_context
633

734
`get_ranked_context` gains an opt-in `compress=True` that fits **more** relevant

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# jcodemunch-mcp — Project Brief
22

33
## Current State
4+
- **Version:** 1.108.130 — **`receipt` CLI price table corrected to current Opus pricing (+ Fable 5).** The `receipt` model price table (`cli/receipt.py` `_MODEL_PRICES_USD_PER_MTOK`) carried a stale $15/MTok Opus rate (the retired 4.0/4.1 price); current Opus 4.8/4.7/4.6 is $5/MTok, so `receipt --model opus` overstated dollar savings ~3x. Corrected to Opus $5 / Sonnet $3 / Haiku $1, added **Claude Fable 5 ($10)** as a selectable model + alternate comparison line (both derive from the table, auto-picked-up), and dated the source comment (anthropic.com/pricing 2026-06-24). `tests/test_receipt.py`: the two stale-rate assertions ($15 Opus, $0.80 Haiku) fixed + hardened into a dated-source pin over the whole table (future drift fails loudly) + a Fable test. **Present-value only: a per-call "what this session costs at current prices" estimator; does NOT feed the public token-savings counter, which stores tokens and values them at display time.** jcm's own `storage/token_tracker.PRICING` was already correct at $5 — `receipt` was the sole stale spot. Sibling parity: jdoc v1.97.0 / jdata v1.19.0 fix the same stale $15 in their `token_tracker.PRICING`, which DID feed live per-response `_meta.cost_avoided` (overstating every jdoc/jdata response's Opus figure 3x). NO INDEX_VERSION / tool-count / wire change.
45
- **Version:** 1.108.129 — **Keystone-protected structural compression for `get_ranked_context` (opt-in `compress=True`; clean-room F-15, competitor unnamed in shipped artifacts).** Renovates an entropy/relevance-guided context-selection idea into jcm's read-only/local/model-free idiom: an oversized symbol body is pruned toward a per-item soft cap so MORE relevant symbols fit the same `token_budget`. New `retrieval/entropy_prune.py` — `prune_source(source, max_tokens, count_tokens)` ranks lines by `line_signal` (normalized Shannon token-entropy × length-weight), keeps the highest-signal lines, and ALWAYS keeps `is_keystone` lines (control-flow/return/raise/yield/assert, signatures `def`/`class`/`@`/`func`/etc., operators `!=`/`==`/`->`/`=>`, and NL constraint cues `must`/`only if`/`unless`/`never`/`require`); dropped runs collapse to an honest `… N low-signal line(s) elided …` marker. O(L) — one token-cost measurement per line; `max_tokens` is a SOFT cap (keystone protection + elision markers can exceed it slightly; the outer `_pack_budget` still enforces the hard total). Wired into BOTH the default and `fusion=True` paths via shared `_compressing_get_tokens(base_get_tokens, token_budget, registry)` (constants `_COMPRESS_TARGET_ITEMS=8`/`_COMPRESS_MIN_ITEM_TOKENS=120`) + `_prune_fields()`; pruned items carry `source_pruned`/`source_kept_lines`/`source_elided_lines`/`source_total_lines`/`source_is_pruned_view`. **Default `compress=False` is byte-identical**; param compact-stripped (`_COMPACT_STRIP_PARAMS["get_ranked_context"]`) so `core_compact` unchanged; NO new tool, NO tool-count change, NO INDEX_VERSION bump. Live-index smoke: 1500-tok budget delivered 12 symbols vs 9 default (3 pruned), same total. New `tests/test_v1_108_129.py` (13); ruff clean; ranked-context/negative-evidence/schema-budget/dispatch-parity/server suites green (150). Origin: a clean-room F-15 renovation of an Atelier-family runtime's entropy/perplexity context-compression stage (competitor unnamed in shipped artifacts, [[feedback_no_competitor_names_in_shipped_artifacts]]; intel kept in session memory); the versus.php warn-box conceded their selection stage was ahead — this closes it.
56
- **Version:** 1.108.128 — **Never auto-bill a paid cloud provider from a bare env key (money-safety, jjg-reported).** A bare `ANTHROPIC_API_KEY` (or any cloud key) in the environment silently enabled AI summarization under the default `use_ai_summaries="auto"` — `get_provider_name` (`summarizer/batch_summarize.py`) walked `_AUTO_DETECT_ORDER` and returned the first provider whose key was present, billing per-symbol on every index. Acute because an editor running on OAuth + an ambient key = quiet spend ("falls out of OAuth mode and eats balance"). Fix: new `_PAID_CLOUD_PROVIDERS` frozenset + `_auto_entry_is_paid_cloud()` (openai counts as paid only when `OPENAI_API_BASE` is off-localhost) + `_paid_summaries_allowed()`; the auto-detect loop now SKIPS a paid-cloud entry unless opted in, with a one-time `logger.warning` naming the exact setting. **Opt-in is explicit**: `summarizer_provider=<name>` (already bypasses auto-detect, unchanged) OR new `JCODEMUNCH_ALLOW_PAID_SUMMARIES=1` / `allow_paid_summaries` config key (added to DEFAULTS/CONFIG_TYPES/template/ENV_VAR_MAPPING). Free/local endpoints (localhost `OPENAI_API_BASE`) still auto-enable. Verified end-to-end: bare ANTHROPIC_API_KEY + default config → provider None, no summarizer, warning emitted; opt-in restores it. NO INDEX_VERSION bump. New `tests/test_v1_108_128.py` (9); 7 existing auto-detect tests updated to set the opt-in flag (they test the mapping/priority mechanism, which now requires opt-in). ruff clean; summarizer/config/guard suites green (270). **Sibling parity: jdoc auto-detects on bare `OPENAI_API_KEY` too (same footgun) — needs the same guard; jdata summarizer doesn't cloud-auto-detect (embeddings gated on explicit `*_EMBED_MODEL`).**
67
- **Version:** 1.108.127 — **Fix `index_repo` full re-index crash `'dict' object has no attribute 'summary'` (#367, @jgravelle).** A non-incremental `index_repo` re-index crashed while `index_folder` on the same repo succeeded. Root cause: loaded indexes carry symbols as **dicts** (`SQLiteIndexStore._build_index_from_rows` → `_row_to_symbol_dict`), but `index_repo`'s full-path summary-preservation map (`index_repo.py` ~L537) read them by ATTRIBUTE (`s.summary`/`s.file`/`s.name`/`s.kind`); `index_folder` (~L2171) already used dict access (`s["file"]`/`s.get("summary")`) — the divergence. The comprehension only fires when a prior index exists AND ≥1 file is unchanged (hash match) AND has a stored summary, so it needed a real re-index-with-summaries to trip (matching the report). Fixed to key access, mirroring `index_folder`. Incremental path was already clean (routes through `store.incremental_save`/`parse_and_prepare_incremental`, never attribute-accesses loaded symbols). Pure read path, NO INDEX_VERSION bump. New `tests/test_v1_108_127.py` (2: no-AttributeError full re-index over a pre-seeded index with mocked GitHub fetch + a loaded-symbols-are-dicts contract guard); verified the test fails with the pre-fix code. ruff clean; index_repo/server/storage suites green (96).

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "jcodemunch-mcp"
3-
version = "1.108.129"
3+
version = "1.108.130"
44
description = "Token-efficient MCP server for source code exploration via tree-sitter AST parsing"
55
readme = "README.md"
66
requires-python = ">=3.10"

src/jcodemunch_mcp/cli/receipt.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,15 @@
9898
# typically 10% of normal input pricing for Anthropic models, but we use
9999
# normal input pricing here because savings are computed against a
100100
# counterfactual (naive Read+Grep would have been *fresh* input, not
101-
# cached). Opus is the default — most jcodemunch users are running an
102-
# Opus-grade model where savings actually move a budget needle.
101+
# cached). Opus is the default: most jcodemunch users run an Opus-grade
102+
# model where savings actually move a budget needle.
103+
# Rates as of 2026-06-24 (anthropic.com/pricing). Update when the public
104+
# price list changes; the test suite pins these to that dated source.
103105
_MODEL_PRICES_USD_PER_MTOK: dict[str, float] = {
104-
"sonnet": 3.0, # Claude Sonnet 4.x
105-
"opus": 15.0, # Claude Opus 4.x
106-
"haiku": 0.80, # Claude Haiku 4.x
106+
"fable": 10.0, # Claude Fable 5 ($10/MTok input)
107+
"opus": 5.0, # Claude Opus 4.8 / 4.7 / 4.6 ($5/MTok input; retired 4.0/4.1 were $15)
108+
"sonnet": 3.0, # Claude Sonnet 5 / 4.6 ($3/MTok input)
109+
"haiku": 1.0, # Claude Haiku 4.5 ($1/MTok input)
107110
}
108111

109112
_DEFAULT_MODEL = "opus"
@@ -308,7 +311,7 @@ def render_text(agg: dict, *, days: int, model: str, primary_only: bool = False)
308311
out.write(f" Saved at {model.title()} pricing (${rate:.2f}/MTok input): ${primary_dollars:,.2f}\n")
309312

310313
if not primary_only:
311-
for other in ("sonnet", "opus", "haiku"):
314+
for other in ("fable", "opus", "sonnet", "haiku"):
312315
if other == model.lower():
313316
continue
314317
other_rate = _MODEL_PRICES_USD_PER_MTOK[other]

tests/test_receipt.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,14 @@ def test_sonnet_rate(self):
175175
assert dollar_savings(1_000_000, "sonnet") == pytest.approx(3.0)
176176

177177
def test_opus_rate(self):
178-
assert dollar_savings(1_000_000, "opus") == pytest.approx(15.0)
178+
# Opus 4.8 / 4.7 / 4.6 = $5/MTok input (retired 4.0/4.1 were $15).
179+
assert dollar_savings(1_000_000, "opus") == pytest.approx(5.0)
179180

180181
def test_haiku_rate(self):
181-
assert dollar_savings(1_000_000, "haiku") == pytest.approx(0.80)
182+
assert dollar_savings(1_000_000, "haiku") == pytest.approx(1.0)
183+
184+
def test_fable_rate(self):
185+
assert dollar_savings(1_000_000, "fable") == pytest.approx(10.0)
182186

183187
def test_unknown_model_zero(self):
184188
assert dollar_savings(1_000_000, "made-up") == 0.0
@@ -242,11 +246,24 @@ def test_json_payload_shape(self):
242246

243247

244248
class TestModelPriceTable:
249+
# Pinned to anthropic.com/pricing as of 2026-06-24. Update this table AND
250+
# the source table in cli/receipt.py together when the price list changes.
251+
_EXPECTED_RATES = {
252+
"fable": 10.0,
253+
"opus": 5.0,
254+
"sonnet": 3.0,
255+
"haiku": 1.0,
256+
}
257+
245258
def test_known_models_present(self):
246-
for m in ("sonnet", "opus", "haiku"):
259+
for m in ("fable", "sonnet", "opus", "haiku"):
247260
assert m in _MODEL_PRICES_USD_PER_MTOK
248261
assert _MODEL_PRICES_USD_PER_MTOK[m] > 0
249262

263+
def test_rates_match_dated_source(self):
264+
for model, rate in self._EXPECTED_RATES.items():
265+
assert _MODEL_PRICES_USD_PER_MTOK[model] == pytest.approx(rate)
266+
250267
def test_opus_more_expensive_than_sonnet(self):
251268
assert _MODEL_PRICES_USD_PER_MTOK["opus"] > _MODEL_PRICES_USD_PER_MTOK["sonnet"]
252269

0 commit comments

Comments
 (0)