| 1 | """Versioned token-calibration factors for the reporting layer.
|
| 2 |
|
| 3 | Rows stay RAW: they carry char-based token estimates exactly as measured.
|
| 4 | Converting raw char counts into calibrated token estimates is a reporting
|
| 5 | concern, and every converted number must cite the calibration version and
|
| 6 | method it came from so two reports can never silently disagree about what
|
| 7 | "tokens" means.
|
| 8 |
|
| 9 | Factors live in config/token_calibration.json, written by
|
| 10 | scripts/token_calibration_campaign.py. The committed file is an honest
|
| 11 | uncalibrated placeholder (n=0, null CIs) using documented heuristic
|
| 12 | constants: ~4.0 chars/token for prose-like output, ~3.2 for diff-like
|
| 13 | output, which BPE-family tokenizers split more finely.
|
| 14 |
|
| 15 | Null means unmeasured (ADR-0002): an unknown style or a style whose factor
|
| 16 | is null yields None — never a silent default factor. Optional enhancers
|
| 17 | (tiktoken, the Anthropic count-tokens API) are feature-detected and never
|
| 18 | required (ADR-0001).
|
| 19 | """
|
| 20 |
|
| 21 | from __future__ import annotations
|
| 22 |
|
| 23 | import importlib.util
|
| 24 | import json
|
| 25 | import os
|
| 26 | from pathlib import Path
|
| 27 | from typing import Any, Optional
|
| 28 |
|
| 29 | SCHEMA_VERSION = 1
|
| 30 |
|
| 31 | DEFAULT_CALIBRATION_PATH = Path(__file__).resolve().parents[2] / "config" / "token_calibration.json"
|
| 32 |
|
| 33 |
|
| 34 | def load_calibration(path: Path | str = DEFAULT_CALIBRATION_PATH) -> dict[str, Any]:
|
| 35 | """Load and validate a calibration factors file.
|
| 36 |
|
| 37 | Raises ValueError with a clear message on a missing/foreign
|
| 38 | schema_version so a stale or future-format file fails loudly instead of
|
| 39 | mispricing every report.
|
| 40 | """
|
| 41 | data = json.loads(Path(path).read_text())
|
| 42 | if not isinstance(data, dict):
|
| 43 | raise ValueError(f"calibration file {path}: expected a JSON object, got {type(data).__name__}")
|
| 44 | schema_version = data.get("schema_version")
|
| 45 | if schema_version != SCHEMA_VERSION:
|
| 46 | raise ValueError(
|
| 47 | f"calibration file {path}: unsupported schema_version {schema_version!r}; "
|
| 48 | f"this harness understands schema_version {SCHEMA_VERSION}"
|
| 49 | )
|
| 50 | if not isinstance(data.get("factors"), dict):
|
| 51 | raise ValueError(f"calibration file {path}: missing 'factors' object")
|
| 52 | if not isinstance(data.get("calibration_version"), str) or not data["calibration_version"]:
|
| 53 | raise ValueError(f"calibration file {path}: missing non-empty 'calibration_version'")
|
| 54 | return data
|
| 55 |
|
| 56 |
|
| 57 | def calibrated_tokens(
|
| 58 | char_count: Optional[float], style: str, calibration: dict[str, Any]
|
| 59 | ) -> Optional[dict[str, Any]]:
|
| 60 | """Convert a raw char count to a calibrated token estimate, citing its source.
|
| 61 |
|
| 62 | Returns None when char_count is None, the style has no factor entry, or
|
| 63 | the style's factor is null — unmeasured stays unmeasured (ADR-0002);
|
| 64 | there is no silent default factor.
|
| 65 | """
|
| 66 | if char_count is None:
|
| 67 | return None
|
| 68 | factors = calibration.get("factors")
|
| 69 | if not isinstance(factors, dict):
|
| 70 | return None
|
| 71 | entry = factors.get(style)
|
| 72 | if not isinstance(entry, dict):
|
| 73 | return None
|
| 74 | chars_per_token = entry.get("chars_per_token")
|
| 75 | if not isinstance(chars_per_token, (int, float)) or isinstance(chars_per_token, bool) or chars_per_token <= 0:
|
| 76 | return None
|
| 77 | return {
|
| 78 | "tokens_estimated": float(char_count) / float(chars_per_token),
|
| 79 | "calibration_version": str(calibration.get("calibration_version")),
|
| 80 | "method": str(calibration.get("method")),
|
| 81 | "style": style,
|
| 82 | }
|
| 83 |
|
| 84 |
|
| 85 | def available_token_counters() -> dict[str, bool]:
|
| 86 | """Feature-detect optional token counters without importing or calling them.
|
| 87 |
|
| 88 | tiktoken: importable module (never required, ADR-0001).
|
| 89 | anthropic_api: ANTHROPIC_API_KEY present in the environment — presence
|
| 90 | only; nothing here performs a network call.
|
| 91 | """
|
| 92 | return {
|
| 93 | "tiktoken": importlib.util.find_spec("tiktoken") is not None,
|
| 94 | "anthropic_api": bool(os.environ.get("ANTHROPIC_API_KEY")),
|
| 95 | }
|