testsAndMisc/python_pkg/token_audit/imagecost.py
Krzysztof kuhy Rudnicki f7e8c59e8f
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Add token_audit package: per-session token attribution and reporting
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 22:20:44 +02:00

84 lines
3.3 KiB
Python

"""Estimate what images cost *after* the turn that loaded them.
An image does not leave the context once read. Every subsequent request in the
session re-sends it as cache-read, so a screenshot loaded at turn 20 of a
1000-turn session is paid for ~980 more times. At the cache-read weight of 0.1
that turns ~2.5k tokens into ~250k, which is why images are worth tracking as
their own axis rather than lumping them in with text tool results.
Measured over a real 7-day window this amplification was ~1.6% of weighted
spend — real, but an order of magnitude below session length. An earlier
version of this module reported ~27%, because it sized images by their base64
payload length; see :func:`parse._result_tokens` for why that is ~20x too high.
The lesson is kept here deliberately: an estimate that is never checked against
billed tokens can invent a headline finding out of nothing.
Two honest limits on the number this produces:
* It is an **estimate**, derived from result sizes rather than billed tokens.
It is deliberately never folded into the reconciled totals, which come only
from the API's own usage figures.
* Compaction discards history. An image loaded before a compaction stops being
re-sent, so charging it forever would roughly double the figure and yields
the impossible result of sessions costing more than they measurably did.
Compaction is detected from a sharp drop in context size.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from python_pkg.token_audit.model import (
COMPACTION_DROP_RATIO,
WEIGHTS,
Session,
ToolCall,
Turn,
)
if TYPE_CHECKING:
from collections.abc import Iterable
# Images are re-sent from cache, so each later turn charges them at cache-read.
_REREAD_WEIGHT = WEIGHTS["cache_read_input_tokens"]
def _is_compaction(context: int, previous: int | None) -> bool:
"""Whether this turn's context implies history was just discarded.
Context grows monotonically as a session accumulates messages, so the only
way it falls sharply is compaction (or a fresh branch). A ratio test rather
than an absolute threshold keeps this valid for both small and huge
sessions.
"""
return previous is not None and context < previous * COMPACTION_DROP_RATIO
def image_cost(events: Iterable[tuple[str, object]]) -> float:
"""Return cost-weighted tokens spent re-sending images across turns.
``events`` must preserve transcript order — the whole calculation is about
how many turns followed each image before compaction cleared it.
"""
live: list[int] = []
previous: int | None = None
total = 0.0
for kind, event in events:
if kind == "tool":
if isinstance(event, ToolCall) and event.is_image:
live.append(event.result_tokens)
elif isinstance(event, Turn):
if _is_compaction(event.context, previous):
live.clear()
# Charge every still-live image once for this turn: it was part of
# the context this request re-sent.
total += sum(live) * _REREAD_WEIGHT
previous = event.context
return total
def annotate(session: Session, events: Iterable[tuple[str, object]]) -> Session:
"""Attach the image amplification estimate to a session."""
session.image_cost = image_cost(events)
return session