Skip to content

Commit d0f4daf

Browse files
fix: include cache-write tokens in quota audit (#577) (#739)
* fix(optimize): include cache writes in quota audit * test(optimize): cover cache-write counterfactual pricing --------- Co-authored-by: Dhruv Garg <DhruvGarg111@users.noreply.github.com> Co-authored-by: Anil Murty <19495789+anilmurty@users.noreply.github.com>
1 parent cfbc515 commit d0f4daf

2 files changed

Lines changed: 162 additions & 35 deletions

File tree

tests/unit/test_opus_quota_audit.py

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,16 @@ def _new_session(db, session_id, *, plan_tier="max_5x") -> None:
6666

6767

6868
def _turn(db, session_id, seq, *, model="claude-opus-4-7", input_tokens=500,
69-
output_tokens=100, cache_tokens=0, cost_usd=1.0, tool_calls=0,
69+
output_tokens=100, cache_tokens=0, cache_write_tokens=0, cost_usd=1.0,
70+
tool_calls=0,
7071
delegate=False) -> None:
7172
"""Append one assistant turn (an LLM span + optional tool/Task spans) to a
7273
session, at minute ``seq`` — so turns order deterministically by start_time
7374
and their tool spans attribute to the enclosing turn (nearest-preceding)."""
7475
span = make_llm_span(
7576
model=model, input_tokens=input_tokens, output_tokens=output_tokens,
76-
cache_tokens=cache_tokens, cost_usd=cost_usd, session_id=session_id,
77+
cache_tokens=cache_tokens, cache_write_tokens=cache_write_tokens,
78+
cost_usd=cost_usd, session_id=session_id,
7779
)
7880
span.start_time = BASE + timedelta(minutes=seq)
7981
db.insert_span(span)
@@ -143,6 +145,109 @@ def test_multi_model_session_example_labels_dominant_model(db):
143145
assert set(audit.suggestions) == {"claude-fable-5", "claude-opus-4-8"}
144146

145147

148+
def test_api_counterfactual_prices_cache_write_on_alt_side(db):
149+
"""The audit's implied-dollar counterfactual must price cache writes on
150+
the alternative model, not silently drop them during session aggregation."""
151+
from tokenjam.core.cost import calculate_cost
152+
153+
cache_read_tokens = 10_000
154+
cache_write_tokens = 50_000
155+
actual_cost = calculate_cost(
156+
"anthropic", "claude-opus-4-7", 500, 100,
157+
cache_read_tokens=cache_read_tokens,
158+
cache_write_tokens=cache_write_tokens,
159+
at=BASE,
160+
)
161+
_new_session(db, "cache-write")
162+
_turn(
163+
db, "cache-write", 0, input_tokens=500, output_tokens=100,
164+
cache_tokens=cache_read_tokens, cache_write_tokens=cache_write_tokens,
165+
cost_usd=actual_cost,
166+
)
167+
168+
audit = _audit(db)
169+
170+
expected_alt_cost = calculate_cost(
171+
"anthropic", "claude-haiku-4-5", 500, 100,
172+
cache_read_tokens=cache_read_tokens,
173+
cache_write_tokens=cache_write_tokens,
174+
at=BASE,
175+
)
176+
broken_alt_cost = calculate_cost(
177+
"anthropic", "claude-haiku-4-5", 500, 100,
178+
cache_read_tokens=cache_read_tokens,
179+
at=BASE,
180+
)
181+
assert audit.candidate_sessions == 1
182+
assert audit.actual_cost_usd == pytest.approx(actual_cost, abs=1e-6)
183+
assert expected_alt_cost > broken_alt_cost
184+
assert audit.alternative_cost_usd == pytest.approx(expected_alt_cost, abs=1e-6)
185+
186+
187+
def test_api_counterfactual_prices_mixed_models_per_model(db):
188+
"""Mixed premium models must keep each model's tokens on its own downgrade.
189+
190+
Fable and Opus have different alternatives and cache-write rates, so pricing
191+
their combined tokens at the dominant model's alternative understates the
192+
counterfactual.
193+
"""
194+
from tokenjam.core.cost import calculate_cost
195+
196+
fable_input, fable_output, fable_cache_write = 400, 80, 40_000
197+
opus_input, opus_output, opus_cache_write = 1_500, 250, 40_000
198+
actual_cost = sum((
199+
calculate_cost(
200+
"anthropic", "claude-fable-5", fable_input, fable_output,
201+
cache_write_tokens=fable_cache_write, at=BASE,
202+
),
203+
calculate_cost(
204+
"anthropic", "claude-opus-4-8", opus_input, opus_output,
205+
cache_write_tokens=opus_cache_write, at=BASE,
206+
),
207+
))
208+
_new_session(db, "mixed-cache-write")
209+
_turn(
210+
db, "mixed-cache-write", 0, model="claude-fable-5",
211+
input_tokens=fable_input, output_tokens=fable_output,
212+
cache_write_tokens=fable_cache_write,
213+
cost_usd=calculate_cost(
214+
"anthropic", "claude-fable-5", fable_input, fable_output,
215+
cache_write_tokens=fable_cache_write, at=BASE,
216+
),
217+
)
218+
_turn(
219+
db, "mixed-cache-write", 1, model="claude-opus-4-8",
220+
input_tokens=opus_input, output_tokens=opus_output,
221+
cache_write_tokens=opus_cache_write,
222+
cost_usd=calculate_cost(
223+
"anthropic", "claude-opus-4-8", opus_input, opus_output,
224+
cache_write_tokens=opus_cache_write, at=BASE,
225+
),
226+
)
227+
228+
audit = _audit(db)
229+
230+
expected_alt_cost = sum((
231+
calculate_cost(
232+
"anthropic", "claude-sonnet-4-6", fable_input, fable_output,
233+
cache_write_tokens=fable_cache_write, at=BASE,
234+
),
235+
calculate_cost(
236+
"anthropic", "claude-haiku-4-5", opus_input, opus_output,
237+
cache_write_tokens=opus_cache_write, at=BASE,
238+
),
239+
))
240+
broken_alt_cost = calculate_cost(
241+
"anthropic", "claude-haiku-4-5",
242+
fable_input + opus_input, fable_output + opus_output,
243+
cache_write_tokens=fable_cache_write + opus_cache_write, at=BASE,
244+
)
245+
assert audit.candidate_sessions == 1
246+
assert audit.actual_cost_usd == pytest.approx(actual_cost, abs=1e-6)
247+
assert expected_alt_cost > broken_alt_cost
248+
assert audit.alternative_cost_usd == pytest.approx(expected_alt_cost, abs=1e-6)
249+
250+
146251
# ── (b) per-span attribution: mixed-model session ───────────────────────────
147252

148253
def test_mixed_model_session_attributes_tokens_per_actual_model(db):

tokenjam/core/optimize/analyzers/model_downgrade.py

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def lookup_downgrade(provider: str, model: str) -> str | None:
220220

221221
def _alt_unit_cost(provider: str, original_model: str, alt_model: str,
222222
input_tokens: int, output_tokens: int, cache_tokens: int,
223-
cache_write_tokens: int = 0, *,
223+
cache_write_tokens: int, *,
224224
at: datetime) -> float | None:
225225
"""Cost of the given token mix priced at ``alt_model``, or ``None`` when the
226226
alternative has no pricing data at ``at``.
@@ -233,10 +233,9 @@ def _alt_unit_cost(provider: str, original_model: str, alt_model: str,
233233
cache-write from the alternative side while the actual side (the
234234
``cost_usd`` column) already includes it; that asymmetry inflated every
235235
savings figure derived from this function by the full cache-write cost of
236-
the candidate. ``cache_write_tokens`` defaults to 0 for the one call site
237-
(the per-turn quota-audit counterfactual) whose upstream aggregation does
238-
not carry a cache-write figure at all — that is unchanged behaviour, not a
239-
new omission.
236+
the candidate. Both callers provide cache-write tokens from
237+
their respective aggregates so the alternative side cannot silently omit a
238+
billed token class.
240239
241240
``at`` is the instant the ORIGINAL traffic ran, and is required. A
242241
counterfactual priced at a different date than the traffic it replaces
@@ -998,25 +997,38 @@ def _segment_counts(
998997
)
999998

1000999

1000+
class _ModelCostAgg:
1001+
"""Per-model token/cost aggregate for a mixed-model counterfactual."""
1002+
1003+
__slots__ = ("new_input", "output", "reread", "cache_write", "cost",
1004+
"first_turn_at")
1005+
1006+
def __init__(self) -> None:
1007+
self.new_input = 0
1008+
self.output = 0
1009+
self.reread = 0
1010+
self.cache_write = 0
1011+
self.cost = 0.0
1012+
self.first_turn_at: datetime | None = None
1013+
1014+
10011015
class _SessionAgg:
10021016
"""Per-session aggregate of the flagged cheap-segment PREMIUM turns, for the
10031017
spot-check example list (largest-quota first)."""
10041018

10051019
__slots__ = ("session_id", "quota_by_model", "quota", "new_input",
1006-
"output", "reread", "tool_calls", "cost", "first_turn_at")
1020+
"output", "reread", "tool_calls", "cost", "pricing_by_model")
10071021

10081022
def __init__(self, session_id: str) -> None:
10091023
self.session_id = session_id
1010-
# Earliest flagged turn in this session — the instant the counterfactual
1011-
# below is priced at. Carried on the aggregate because the aggregate is
1012-
# what gets priced: without it the dollar figure would fall back to
1013-
# today's rate for traffic that ran weeks ago.
1014-
self.first_turn_at: datetime | None = None
10151024
# (provider, model) -> flagged premium quota, so the example reports the
10161025
# model that actually carried the most misallocated quota in this session
10171026
# rather than freezing on whichever premium turn happened to appear first
10181027
# (a mixed-model session could otherwise mislabel the routing suggestion).
10191028
self.quota_by_model: dict[tuple[str | None, str], float] = {}
1029+
# Keep token classes separated by original model. A mixed-model session
1030+
# cannot be priced by applying one model's downgrade to every turn.
1031+
self.pricing_by_model: dict[tuple[str | None, str], _ModelCostAgg] = {}
10201032
self.quota = 0.0
10211033
self.new_input = 0
10221034
self.output = 0
@@ -1121,11 +1133,20 @@ def audit_opus_quota(
11211133
agg.reread += t.reread_tokens
11221134
agg.tool_calls += t.tool_fanout
11231135
agg.cost += t.cost_usd
1136+
key = (t.provider, t.model)
1137+
pricing_agg = agg.pricing_by_model.setdefault(
1138+
key, _ModelCostAgg(),
1139+
)
1140+
pricing_agg.new_input += t.new_input_tokens
1141+
pricing_agg.output += t.output_tokens
1142+
pricing_agg.reread += t.reread_tokens
1143+
pricing_agg.cache_write += t.cache_write_tokens
1144+
pricing_agg.cost += t.cost_usd
11241145
if t.start_time is not None and (
1125-
agg.first_turn_at is None or t.start_time < agg.first_turn_at
1146+
pricing_agg.first_turn_at is None
1147+
or t.start_time < pricing_agg.first_turn_at
11261148
):
1127-
agg.first_turn_at = t.start_time
1128-
key = (t.provider, t.model)
1149+
pricing_agg.first_turn_at = t.start_time
11291150
agg.quota_by_model[key] = (
11301151
agg.quota_by_model.get(key, 0.0) + t.quota_weighted_tokens
11311152
)
@@ -1139,26 +1160,27 @@ def audit_opus_quota(
11391160
if session_flagged:
11401161
audit.candidate_sessions += 1
11411162

1142-
# Secondary API-only implied-dollar counterfactual, aggregated over the
1143-
# flagged premium turns and priced at each session's dominant premium model's
1144-
# cheaper alternative (never the headline). Deliberately never folded into
1145-
# `analyze_model_downgrade`'s `DowngradeFinding.past_overspend_usd`
1146-
# (the `tj optimize` downsize card) — see the comment on that assignment
1147-
# for why the two stay separate.
1163+
# Secondary API-only implied-dollar counterfactual, priced per original
1164+
# premium model at that model's cheaper alternative (never the headline).
1165+
# Deliberately never folded into `analyze_model_downgrade`'s
1166+
# `DowngradeFinding.past_overspend_usd` (the `tj optimize` downsize card) —
1167+
# see the comment on that assignment for why the two stay separate.
11481168
for agg in aggs.values():
1149-
provider, model = agg.dominant_model()
1150-
alt = lookup_downgrade(provider, model) if provider else None
1151-
if not alt:
1152-
continue
1153-
# ``alt`` is only set when ``provider`` was truthy above.
1154-
assert provider is not None
1155-
alt_unit = _alt_unit_cost(
1156-
provider, model, alt, agg.new_input, agg.output, agg.reread,
1157-
at=span_instant(agg.first_turn_at, window_start=since),
1158-
)
1159-
if alt_unit is not None:
1160-
actual_cost += agg.cost
1161-
alt_cost += alt_unit
1169+
for (provider, model), pricing_agg in agg.pricing_by_model.items():
1170+
alt = lookup_downgrade(provider, model) if provider else None
1171+
if not alt:
1172+
continue
1173+
# ``alt`` is only set when ``provider`` was truthy above.
1174+
assert provider is not None
1175+
alt_unit = _alt_unit_cost(
1176+
provider, model, alt,
1177+
pricing_agg.new_input, pricing_agg.output, pricing_agg.reread,
1178+
cache_write_tokens=pricing_agg.cache_write,
1179+
at=span_instant(pricing_agg.first_turn_at, window_start=since),
1180+
)
1181+
if alt_unit is not None:
1182+
actual_cost += pricing_agg.cost
1183+
alt_cost += alt_unit
11621184

11631185
audit.opus_tokens = int(round(premium_quota))
11641186
audit.candidate_tokens = int(round(misallocated_quota))

0 commit comments

Comments
 (0)