"""Map local char counts onto real prompt tokens, per model.
``ContextCompressor.estimate_context_tokens`` (chars // 3) is the only token
count available *before* an LLM call, and on this workload it is off by a
factor of ~1.8. Prod sessions measured 101 134 real prompt tokens against a
~56 400 estimate (tool output, JSON schemas and Russian text are token-dense),
so every window decision taken on the estimate — the compression target, the
hard-truncate budgets, the pre-call size check — was taken in a unit roughly
half the size of the real one. Compression aimed at "65% of the window" and
landed above it, the next two tool results re-triggered it, and a session could
run 48 passes in a day.
Every LLM call reports ``prompt_eval_count``, and the char count of the exact
prompt that produced it is known too. Two observations of different size pin a
line::
real_tokens ≈ overhead + chars / chars_per_token
The line predicts the real cost of any hypothetical prompt assembled from the
same pieces (system text + tool schemas + messages), whichever part of it
changes — a smaller kept region after compression included. Observations are
kept per model: tokenizers differ enough that one line does not cover glm and
gemma.
The fit needs size spread to separate overhead from slope; until it has that,
``DEFAULT_CHARS_PER_TOKEN`` is used. It is deliberately more conservative than
chars // 3 (which was measured at ~1.8x below reality) but not as aggressive as
the worst observed ratio.
"""
from __future__ import annotations
from dataclasses import dataclass
# Fallback until a fitted line exists. The old estimator used 3 chars/token;
# prod measurements sat at 1.67–1.79. 1.8 is the safe middle: closer to truth
# on message text without assuming the token-densest content everywhere.
DEFAULT_CHARS_PER_TOKEN = 1.8
_MAX_OBSERVATIONS = 8
# The two observations must differ this much in size, or the fitted slope is
# noise over a narrow band.
_MIN_SPREAD = 1.25
# Sanity clamps on the fitted slope (tokens per char). A fit outside this band
# means the observations are not comparable (e.g. one prompt was truncated by
# the server) — keep the default instead.
_MIN_TOKENS_PER_CHAR = 1 / 4.0
_MAX_TOKENS_PER_CHAR = 1 / 1.0
@dataclass(frozen=True)
class _Observation:
chars: int
tokens: int
class TokenCalibration:
"""Linear fit of prompt tokens against prompt chars for one model."""
def __init__(self) -> None:
self._observations: list[_Observation] = []
def observe(self, chars: int, tokens: int | None) -> None:
"""Record one (prompt chars, real prompt tokens) pair."""
if not chars or not tokens or tokens <= 0:
return
self._observations.append(_Observation(int(chars), int(tokens)))
del self._observations[: -_MAX_OBSERVATIONS]
def _line(self) -> tuple[float, float] | None:
"""(overhead, tokens_per_char) or None when the observations can't pin one."""
n = len(self._observations)
if n < 2:
return None
xs = [o.chars for o in self._observations]
if max(xs) < min(xs) * _MIN_SPREAD:
return None
mean_x = sum(xs) / n
mean_y = sum(o.tokens for o in self._observations) / n
sxx = sum((x - mean_x) ** 2 for x in xs)
if sxx <= 0:
return None
sxy = sum((o.chars - mean_x) * (o.tokens - mean_y) for o in self._observations)
slope = sxy / sxx
if not (_MIN_TOKENS_PER_CHAR <= slope <= _MAX_TOKENS_PER_CHAR):
return None
intercept = mean_y - slope * mean_x
return max(0.0, intercept), slope
def tokens_for(self, chars: int) -> int:
"""Estimated real prompt tokens for a prompt of ``chars`` characters."""
line = self._line()
if line is None:
return int(chars / DEFAULT_CHARS_PER_TOKEN)
intercept, slope = line
return max(0, round(intercept + chars * slope))
def delta_for(self, chars: int) -> int:
"""Price *appended* content with the slope only.
The fitted intercept is the fixed prompt overhead (system prompt and
tool schemas). That overhead is already inside a real baseline count,
so charging it again to the delta added on top of that count would
double-count it.
"""
line = self._line()
if line is None:
return int(chars / DEFAULT_CHARS_PER_TOKEN)
return max(0, round(chars * line[1]))
@property
def observations(self) -> int:
return len(self._observations)
_registry: dict[str, TokenCalibration] = {}
def _key(model: str | list[str] | None) -> str:
if isinstance(model, (list, tuple)):
model = model[0] if model else None
return (model or "").strip().lower()
def calibration_for(model: str | list[str] | None = None) -> TokenCalibration:
key = _key(model)
calibration = _registry.get(key)
if calibration is None:
calibration = _registry[key] = TokenCalibration()
return calibration
def observe_prompt(
model: str | list[str] | None, chars: int, tokens: int | None
) -> None:
"""Record a real prompt-token count for ``model``."""
calibration_for(model).observe(chars, tokens)
def reset_calibration() -> None:
"""Drop every observation — tests, and any operator-driven reset."""
_registry.clear()