headroom/tests/test_proxy_savings_history.py
Parideboy 6d5516dcb8
feat(code): add PHP support to CodeAwareCompressor (#2423)
## Description

Adds PHP to `CodeAwareCompressor`, fixing #201. PHP was already
*detected* as code (Magika labels in `headroom/compression/detector.py`
include `php`, and the Rust `magika_detector.rs` lists it too) but there
was no PHP `LangConfig`, so PHP content silently passed through
uncompressed. This wires PHP through the tree-sitter compression path
following the C# pattern (the most recently added, fully functional
language — deliberately not the quarantined Perl path).

A secondary detection bug is fixed along the way: PHP's `$variables`
match Perl's prefilter regex, and the existing Perl-dominance guard in
`detect_language` returned `UNKNOWN` for PHP files. An explicit `<?php`
open tag — which no Perl source contains — now drops Perl from the
candidate set before that guard runs.

## Type of Change

- [ ] Bug fix
- [x] New feature
- [ ] Documentation update
- [ ] Refactor
- [ ] Other

## Changes Made

- `headroom/transforms/code_compressor.py`: `CodeLanguage.PHP` +
`phtml`/`php5`/`php7`/`php8` aliases; PHP `LangConfig` built from the
actual tree-sitter-php grammar (node names verified by parsing samples):
`namespace_use_declaration` imports,
`function_definition`/`method_declaration` functions,
`class_declaration`/`interface_declaration`/`trait_declaration` classes,
`enum_declaration` types, `declaration_list` class bodies,
`compound_statement` function bodies. `namespace_definition` maps to
`package_node` so statement-scoped `namespace App;` hoists ahead of the
`use` imports (required PHP ordering); the rare block-scoped `namespace
A { }` form takes the same path and is preserved verbatim — valid
output, just no compression inside the block. PHP prefilter regexes
added; supported-languages error message updated; `<?php`-tag Perl
disambiguation in `detect_language`.
- `headroom/transforms/content_detector.py`: `php` entry in
`_CODE_PATTERNS` so raw PHP classifies as `SOURCE_CODE` and reaches the
code-aware route.
- `tests/test_transforms/test_code_compressor.py`: new `TestPhpSupport`
mirroring `TestCSharpSupport` — signatures preserved / bodies elided,
`<?php` → `namespace` → `use` → declarations ordering, auto-detection
despite the Perl sigil overlap, alias coercion, malformed passthrough.
- `tests/test_code_compressor_language_alias.py`: `php` in the canonical
list, `phtml` in the alias table.
- `docs/content/docs/code-compression.mdx`: PHP added to the Tier 2
supported-languages row.

No new dependency: `tree-sitter-language-pack` (the existing `[code]`
extra) already ships the PHP grammar. No Rust changes needed.

## Testing

- [x] New unit tests added and passing
- [x] Full affected test suites pass locally

**Test Output**

```
$ python -m pytest tests/test_transforms/test_code_compressor.py tests/test_code_compressor_language_alias.py -q
============================= 120 passed in 7.81s =============================

$ python -m pytest tests/test_transforms/ -q
3 failed, 443 passed   # the 3 failures (kompress ONNX thread caps, kompress size gate,
                       # text_crusher unicode parity) reproduce identically on a clean
                       # upstream/main checkout in this environment — pre-existing local
                       # ONNX runtime quirks, unrelated to this change

$ ruff check . (0.15.17, CI-pinned) → All checks passed!  |  ruff format --check → clean
$ mypy headroom/transforms/code_compressor.py headroom/transforms/content_detector.py --ignore-missing-imports
Success: no issues found in 2 source files
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13, tree-sitter +
tree-sitter-language-pack (<1.0) installed, branch
`feat/201-php-code-compression` off `upstream/main`.
- Exact command / steps: parsed PHP samples (namespaced class w/
methods, block-scoped namespace, mixed HTML+PHP) with
`tree_sitter_language_pack.get_parser('php')` to verify every node name
used in the config; then ran `CodeAwareCompressor().compress(php_code,
language="php")` and `compress(php_code)` (auto-detection) on a 48-line
realistic service class.
- Observed result: explicit and auto-detected paths both return
`language=CodeLanguage.PHP`, `compression_ratio=0.64`,
`syntax_valid=True`; method bodies elided to `// [N lines omitted]`
while `<?php`, `namespace`, `use` lines, class header, and all
signatures are preserved verbatim in the original order. Before the
detection fix, auto-detection returned `UNKNOWN` (Perl prefilter
dominance) — reproduced and then verified fixed.
- Not tested: exotic PHP shapes (heredoc-heavy code, attributes `#[...]`
on methods, interleaved multi-`<?php ?>` HTML templates beyond the basic
mixed case); these fall back to verbatim preservation via the
uncaptured-node pass or malformed-passthrough, both of which are covered
by tests for the simple cases.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-31 15:54:13 -07:00

1857 lines
68 KiB
Python

"""Tests for durable proxy savings history."""
from __future__ import annotations
import asyncio
import json
import math
import os
import stat
import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
from types import SimpleNamespace
import pytest
pytest.importorskip("fastapi")
from fastapi.testclient import TestClient
import headroom.proxy.savings_tracker as savings_tracker_module
from headroom.proxy.savings_tracker import HEADROOM_SAVINGS_PATH_ENV_VAR, SavingsTracker
from headroom.proxy.server import ProxyConfig, create_app
def _record_request(
client: TestClient,
*,
model: str,
tokens_saved: int,
input_tokens: int = 120,
) -> None:
proxy = client.app.state.proxy
if proxy.cost_tracker:
proxy.cost_tracker.record_tokens(model, tokens_saved, input_tokens)
asyncio.run(
proxy.metrics.record_request(
provider="openai",
model=model,
input_tokens=input_tokens,
output_tokens=24,
tokens_saved=tokens_saved,
latency_ms=15.0,
)
)
def test_savings_tracker_helpers_normalize_inputs_and_paths(tmp_path, monkeypatch):
override_path = tmp_path / "custom-savings.json"
monkeypatch.setenv(HEADROOM_SAVINGS_PATH_ENV_VAR, str(override_path))
assert savings_tracker_module.get_default_savings_storage_path() == str(override_path)
monkeypatch.delenv(HEADROOM_SAVINGS_PATH_ENV_VAR, raising=False)
# HEADROOM_WORKSPACE_DIR overrides the default savings path too (see
# headroom/paths.py); unset it so this assertion checks the actual
# library default rather than whatever workspace a live deployment on
# this machine happens to have exported.
monkeypatch.delenv("HEADROOM_WORKSPACE_DIR", raising=False)
default_path = savings_tracker_module.get_default_savings_storage_path()
assert Path(default_path).as_posix().endswith(".headroom/proxy_savings.json")
assert savings_tracker_module._parse_timestamp("") is None
assert savings_tracker_module._parse_timestamp("not-a-timestamp") is None
assert savings_tracker_module._parse_timestamp("2026-03-27T09:00:00") == datetime(
2026, 3, 27, 9, 0, tzinfo=timezone.utc
)
assert savings_tracker_module._coerce_int("7") == 7
assert savings_tracker_module._coerce_int(-5) == 0
assert savings_tracker_module._coerce_float("0.25") == pytest.approx(0.25)
assert savings_tracker_module._coerce_float(-0.25) == 0.0
assert savings_tracker_module._normalize_history_entry(
["2026-03-27T09:00:00Z", "12", "0.5"]
) == {
"timestamp": "2026-03-27T09:00:00Z",
"provider": "unknown",
"model": "unknown",
"total_tokens_saved": 12,
"compression_savings_usd": 0.5,
"cache_read_tokens": 0,
"cache_savings_usd": 0.0,
"total_input_tokens": 0,
"total_input_cost_usd": 0.0,
"output_tokens_saved": 0,
"output_savings_usd": 0.0,
}
assert savings_tracker_module._normalize_history_entry({"timestamp": "bad"}) is None
assert savings_tracker_module._normalize_history_entry(object()) is None
def test_savings_tracker_sanitizes_legacy_state_and_applies_retention(tmp_path):
path = tmp_path / "proxy_savings.json"
path.write_text(
json.dumps(
{
"schema_version": 0,
"lifetime": {
"tokens_saved": 1,
"compression_savings_usd": 0.001,
},
"history": [
["2026-03-24T08:00:00Z", 10, 0.01],
{
"timestamp": "2026-03-26T12:00:00Z",
"total_tokens_saved": 20,
"compression_savings_usd": 0.02,
},
{
"timestamp": "2026-03-27T09:00:00Z",
"total_tokens_saved": 30,
"compression_savings_usd": 0.03,
},
{"timestamp": "bad", "total_tokens_saved": 999},
],
}
),
encoding="utf-8",
)
tracker = SavingsTracker(
path=str(path),
max_history_points=1,
max_history_age_days=2,
)
snapshot = tracker.snapshot()
assert snapshot["schema_version"] == 5
assert snapshot["lifetime"] == {
"requests": 0,
"tokens_saved": 30,
"compression_savings_usd": pytest.approx(0.03),
"cache_read_tokens": 0,
"cache_savings_usd": 0.0,
"total_input_tokens": 0,
"total_input_cost_usd": 0.0,
}
assert snapshot["display_session"] == savings_tracker_module._empty_display_session()
assert snapshot["history"] == [
{
"timestamp": "2026-03-27T09:00:00Z",
"provider": "unknown",
"model": "unknown",
"total_tokens_saved": 30,
"compression_savings_usd": 0.03,
"cache_read_tokens": 0,
"cache_savings_usd": 0.0,
"total_input_tokens": 0,
"total_input_cost_usd": 0.0,
"output_tokens_saved": 0,
"output_savings_usd": 0.0,
}
]
assert snapshot["retention"] == {
"max_history_points": 1,
"max_history_age_days": 2,
"max_response_history_points": 500,
}
def test_non_dict_savings_state_resets_to_default(tmp_path):
path = tmp_path / "proxy_savings.json"
path.write_text("[]", encoding="utf-8")
tracker = SavingsTracker(path=str(path))
snapshot = tracker.snapshot()
assert snapshot["lifetime"] == {
"requests": 0,
"tokens_saved": 0,
"compression_savings_usd": 0.0,
"cache_read_tokens": 0,
"cache_savings_usd": 0.0,
"total_input_tokens": 0,
"total_input_cost_usd": 0.0,
}
assert snapshot["display_session"] == savings_tracker_module._empty_display_session()
assert snapshot["history"] == []
def test_record_compression_savings_skips_empty_updates_and_normalizes_timestamps(
tmp_path, monkeypatch
):
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path))
monkeypatch.setattr(
savings_tracker_module,
"_estimate_compression_savings_usd",
lambda model, tokens_saved: tokens_saved / 1000.0,
)
assert tracker.record_compression_savings(model="gpt-4o", tokens_saved=0) is False
assert not path.exists()
local_time = datetime(2026, 3, 27, 10, 0, tzinfo=timezone(timedelta(hours=2)))
assert tracker.record_compression_savings(
model="gpt-4o",
tokens_saved=10,
total_input_tokens=120,
total_input_cost_usd=0.24,
timestamp=local_time,
)
fallback_time = datetime(2026, 3, 27, 12, 34, tzinfo=timezone.utc)
monkeypatch.setattr(savings_tracker_module, "_utc_now", lambda: fallback_time)
assert tracker.record_compression_savings(
model="gpt-4o",
tokens_saved=5,
total_input_tokens=180,
total_input_cost_usd=0.36,
timestamp="not-a-timestamp",
)
snapshot = tracker.snapshot()
assert snapshot["history"] == [
{
"timestamp": "2026-03-27T08:00:00Z",
"provider": "unknown",
"model": "gpt-4o",
"total_tokens_saved": 10,
"compression_savings_usd": 0.01,
"total_input_tokens": 120,
"total_input_cost_usd": 0.24,
},
{
"timestamp": "2026-03-27T12:34:00Z",
"provider": "unknown",
"model": "gpt-4o",
"total_tokens_saved": 15,
"compression_savings_usd": 0.015,
"total_input_tokens": 180,
"total_input_cost_usd": 0.36,
},
]
persisted = json.loads(path.read_text(encoding="utf-8"))
assert persisted["lifetime"]["tokens_saved"] == 15
assert persisted["lifetime"]["total_input_tokens"] == 180
assert persisted["lifetime"]["total_input_cost_usd"] == pytest.approx(0.36)
assert persisted["history"][-1]["timestamp"] == "2026-03-27T12:34:00Z"
def test_stateless_savings_tracker_writes_nothing(tmp_path):
"""In stateless mode the tracker updates in-memory counters but never
touches the filesystem — no proxy_savings.json is created."""
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path), stateless=True)
# Both write paths that would normally persist a checkpoint:
assert tracker.record_compression_savings(model="gpt-4o", tokens_saved=4096) is True
tracker.record_request(
model="gpt-4o",
input_tokens=8192,
tokens_saved=4096,
timestamp="2026-03-27T09:00:00Z",
)
# Nothing written to disk...
assert not path.exists()
# ...but live in-memory counters still reflect the activity.
assert tracker.snapshot()["lifetime"]["tokens_saved"] >= 4096
def test_non_stateless_savings_tracker_still_persists(tmp_path):
"""Control: default (stateless=False) behavior is unchanged — it persists."""
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path))
tracker.record_request(
model="gpt-4o",
input_tokens=8192,
tokens_saved=4096,
timestamp="2026-03-27T09:00:00Z",
)
assert path.exists()
def test_savings_tracker_save_does_not_flock_target_inode_before_replace(tmp_path, monkeypatch):
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path))
tracker.record_request(
model="gpt-4o",
input_tokens=120,
tokens_saved=10,
timestamp="2026-03-27T09:00:00Z",
)
assert path.exists()
flock_calls: list[int] = []
class _FcntlSpy:
LOCK_EX = 1
LOCK_UN = 2
def flock(self, _fh, operation: int) -> None:
flock_calls.append(operation)
monkeypatch.setattr(savings_tracker_module, "_HAS_FCNTL", True, raising=False)
monkeypatch.setattr(savings_tracker_module, "_fcntl", _FcntlSpy(), raising=False)
tracker.record_request(
model="gpt-4o",
input_tokens=80,
tokens_saved=5,
timestamp="2026-03-27T09:10:00Z",
)
assert flock_calls == []
persisted = json.loads(path.read_text(encoding="utf-8"))
assert persisted["lifetime"]["tokens_saved"] == 15
def test_savings_tracker_save_fsyncs_parent_directory(tmp_path, monkeypatch):
# The file fsync persists contents, but the rename isn't durable until the
# parent directory is fsynced too — without it a crash can drop the last
# save. Assert a directory fd is fsynced on save. (FP4b)
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path))
real_fsync = os.fsync
dir_fds_synced: list[int] = []
def _spy_fsync(fd: int) -> None:
try:
if stat.S_ISDIR(os.fstat(fd).st_mode):
dir_fds_synced.append(fd)
except OSError:
pass
real_fsync(fd)
monkeypatch.setattr(savings_tracker_module.os, "fsync", _spy_fsync)
tracker.record_request(
model="gpt-4o",
input_tokens=120,
tokens_saved=10,
timestamp="2026-03-27T09:00:00Z",
)
# Parent directory fsynced (rename durable) and the save still landed intact.
assert dir_fds_synced, "parent directory was never fsynced after os.replace"
persisted = json.loads(path.read_text(encoding="utf-8"))
assert persisted["lifetime"]["tokens_saved"] == 10
def test_savings_tracker_save_survives_directory_fsync_failure(tmp_path, monkeypatch):
# On Windows and some virtual filesystems the directory fsync fails — the
# save must still complete because the file and atomic rename are already
# durable on their own. (FP4b)
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path))
real_open = os.open
def _failing_open(target, *args, **kwargs):
if str(target) == str(path.parent):
raise OSError("directory fsync unsupported")
return real_open(target, *args, **kwargs)
monkeypatch.setattr(savings_tracker_module.os, "open", _failing_open)
tracker.record_request(
model="gpt-4o",
input_tokens=120,
tokens_saved=10,
timestamp="2026-03-27T09:00:00Z",
)
persisted = json.loads(path.read_text(encoding="utf-8"))
assert persisted["lifetime"]["tokens_saved"] == 10
def test_litellm_resolution_and_savings_estimation_fallbacks(monkeypatch):
def fake_cost_per_token(*, model, prompt_tokens, completion_tokens):
if model in {"gpt-4o", "anthropic/claude-sonnet-4-6"}:
return {
"model": model,
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
}
raise RuntimeError("unknown model")
fake_litellm = SimpleNamespace(
cost_per_token=fake_cost_per_token,
model_cost={
"anthropic/claude-sonnet-4-6": {"input_cost_per_token": 0.002},
"gpt-4o": {"input_cost_per_token": 0.001},
},
)
monkeypatch.setattr(savings_tracker_module, "LITELLM_AVAILABLE", True)
monkeypatch.setattr(savings_tracker_module, "litellm", fake_litellm)
assert savings_tracker_module._resolve_litellm_model("gpt-4o") == "gpt-4o"
assert (
savings_tracker_module._resolve_litellm_model("claude-sonnet-4-6")
== "anthropic/claude-sonnet-4-6"
)
assert savings_tracker_module._estimate_compression_savings_usd(
"claude-sonnet-4-6", 100
) == pytest.approx(0.2)
assert savings_tracker_module._estimate_input_cost_usd(
"claude-sonnet-4-6",
100,
cache_read_tokens=10,
cache_write_tokens=5,
uncached_input_tokens=85,
) == pytest.approx(0.2)
fake_litellm.model_cost = {}
assert savings_tracker_module._estimate_compression_savings_usd("gpt-4o", 100) == pytest.approx(
100 * savings_tracker_module.DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN
)
assert savings_tracker_module._estimate_input_cost_usd("gpt-4o", 100) == pytest.approx(
100 * savings_tracker_module.DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN
)
monkeypatch.setattr(
fake_litellm,
"cost_per_token",
lambda **kwargs: (_ for _ in ()).throw(RuntimeError("boom")),
)
assert savings_tracker_module._resolve_litellm_model("mystery-model") == "mystery-model"
assert savings_tracker_module._estimate_compression_savings_usd(
"mystery-model", 100
) == pytest.approx(100 * savings_tracker_module.DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN)
assert savings_tracker_module._estimate_input_cost_usd("mystery-model", 100) == pytest.approx(
100 * savings_tracker_module.DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN
)
# Explicitly force the unavailable path for the whole tracker.
monkeypatch.setattr(savings_tracker_module, "LITELLM_AVAILABLE", False)
assert savings_tracker_module._estimate_compression_savings_usd("gpt-4o", 100) == pytest.approx(
100 * savings_tracker_module.DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN
)
assert savings_tracker_module._estimate_input_cost_usd("gpt-4o", 100) == pytest.approx(
100 * savings_tracker_module.DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN
)
def test_fallback_request_pricing_stays_nonzero_with_litellm_unavailable_and_preserves_historic_zeros(
tmp_path, monkeypatch
):
# Legacy proxy_savings rows can legitimately store zero-dollar values.
savings_path = tmp_path / "proxy_savings.json"
savings_path.write_text(
json.dumps(
{
"schema_version": 3,
"lifetime": {
"requests": 1,
"tokens_saved": 10,
"compression_savings_usd": 0.0,
"total_input_tokens": 120,
"total_input_cost_usd": 0.0,
},
"display_session": {},
"history": [
{
"timestamp": "2026-03-27T09:00:00Z",
"provider": "openai",
"model": "gpt-4o",
"total_tokens_saved": 10,
"compression_savings_usd": 0.0,
"total_input_tokens": 120,
"total_input_cost_usd": 0.0,
}
],
"projects": {
"fallback-demo": {
"requests": 1,
"tokens_saved": 10,
"compression_savings_usd": 0.0,
"total_input_tokens": 120,
"total_input_cost_usd": 0.0,
"last_activity_at": "2026-03-27T09:00:00Z",
}
},
}
),
encoding="utf-8",
)
tracker = SavingsTracker(path=str(savings_path))
initial_snapshot = tracker.snapshot()
assert initial_snapshot["lifetime"]["compression_savings_usd"] == 0.0
assert initial_snapshot["display_session"]["compression_savings_usd"] == 0.0
assert initial_snapshot["projects"]["fallback-demo"]["compression_savings_usd"] == 0.0
assert initial_snapshot["history"][-1]["compression_savings_usd"] == 0.0
monkeypatch.setattr(savings_tracker_module, "LITELLM_AVAILABLE", False)
monkeypatch.setattr(savings_tracker_module, "litellm", None)
assert tracker.record_request(
model="gpt-4o",
input_tokens=100,
tokens_saved=50,
project="fallback-demo",
timestamp="2026-03-27T09:10:00Z",
)
monkeypatch.setattr(
savings_tracker_module,
"_utc_now",
lambda: datetime(2026, 3, 27, 9, 10, 30, tzinfo=timezone.utc),
)
snapshot = tracker.snapshot()
expected_savings_fallback = 50 * savings_tracker_module.DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN
expected_input_fallback = 100 * savings_tracker_module.DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN
assert snapshot["lifetime"]["compression_savings_usd"] == pytest.approx(
expected_savings_fallback
)
assert snapshot["lifetime"]["total_input_cost_usd"] == pytest.approx(expected_input_fallback)
assert snapshot["display_session"]["compression_savings_usd"] == pytest.approx(
expected_savings_fallback
)
assert snapshot["display_session"]["total_input_cost_usd"] == pytest.approx(
expected_input_fallback
)
assert snapshot["projects"]["fallback-demo"]["compression_savings_usd"] == pytest.approx(
expected_savings_fallback
)
assert snapshot["projects"]["fallback-demo"]["total_input_cost_usd"] == pytest.approx(
expected_input_fallback
)
assert snapshot["history"][-1]["compression_savings_usd"] == pytest.approx(
expected_savings_fallback
)
persisted = json.loads(savings_path.read_text(encoding="utf-8"))
assert persisted["history"][0]["compression_savings_usd"] == 0.0
assert persisted["history"][-1]["compression_savings_usd"] == pytest.approx(
expected_savings_fallback
)
def test_input_cost_counts_cache_reads_when_uncached_input_is_zero(monkeypatch):
# Anthropic reports cache reads/writes separately from `input_tokens` (the
# uncached portion). A fully prefix-cached request has input_tokens == 0 but
# cache_read_tokens > 0 -- it still cost money and must not be priced at 0,
# otherwise the day shows compression savings with zero recorded spend.
def fake_cost_per_token(*, model, prompt_tokens, completion_tokens):
if model == "anthropic/claude-sonnet-4-6":
return {"model": model}
raise RuntimeError("unknown model")
fake_litellm = SimpleNamespace(
cost_per_token=fake_cost_per_token,
model_cost={
"anthropic/claude-sonnet-4-6": {
"input_cost_per_token": 0.003,
"cache_read_input_token_cost": 0.0003,
"cache_creation_input_token_cost": 0.00375,
},
},
)
monkeypatch.setattr(savings_tracker_module, "LITELLM_AVAILABLE", True)
monkeypatch.setattr(savings_tracker_module, "litellm", fake_litellm)
cost = savings_tracker_module._estimate_input_cost_usd(
"claude-sonnet-4-6",
0,
cache_read_tokens=1000,
)
assert cost == pytest.approx(0.3)
def test_fallback_input_cost_uses_breakdown_sum_not_input_tokens_when_litellm_unavailable(
monkeypatch,
):
# Regression: when both `input_tokens` and a nonzero cache breakdown are
# present and LiteLLM is unavailable, the fallback must price only the
# breakdown sum — never input_tokens + breakdown_sum — to avoid
# double-counting the tokens that the breakdown already covers.
monkeypatch.setattr(savings_tracker_module, "LITELLM_AVAILABLE", False)
monkeypatch.setattr(savings_tracker_module, "litellm", None)
input_tokens = 1000
cache_read = 200
cache_write = 100
uncached = 300
breakdown_sum = cache_read + cache_write + uncached # 600
result = savings_tracker_module._estimate_input_cost_usd(
"gpt-4o",
input_tokens,
cache_read_tokens=cache_read,
cache_write_tokens=cache_write,
uncached_input_tokens=uncached,
)
fallback_rate = savings_tracker_module.DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN
expected = breakdown_sum * fallback_rate
double_counted = (input_tokens + breakdown_sum) * fallback_rate
assert result == pytest.approx(expected)
assert result != pytest.approx(double_counted)
def test_display_session_rolls_after_inactivity_and_counts_zero_savings_requests(
tmp_path, monkeypatch
):
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path), display_session_inactivity_minutes=30)
monkeypatch.setattr(
savings_tracker_module,
"_estimate_compression_savings_usd",
lambda model, tokens_saved: tokens_saved / 1000.0,
)
monkeypatch.setattr(
savings_tracker_module,
"_estimate_input_cost_usd",
lambda model, input_tokens, **kwargs: input_tokens / 1000.0,
)
tracker.record_request(
model="gpt-4o",
input_tokens=120,
tokens_saved=0,
timestamp="2026-03-27T09:00:00Z",
)
tracker.record_request(
model="gpt-4o",
input_tokens=80,
tokens_saved=20,
timestamp="2026-03-27T09:10:00Z",
)
monkeypatch.setattr(
savings_tracker_module,
"_utc_now",
lambda: datetime(2026, 3, 27, 9, 15, tzinfo=timezone.utc),
)
active_session = tracker.snapshot()["display_session"]
assert active_session == {
"requests": 2,
"tokens_saved": 20,
"compression_savings_usd": pytest.approx(0.02),
"cache_read_tokens": 0,
"cache_savings_usd": 0.0,
"total_input_tokens": 200,
"total_input_cost_usd": pytest.approx(0.2),
"savings_percent": pytest.approx(9.09),
"started_at": "2026-03-27T09:00:00Z",
"last_activity_at": "2026-03-27T09:10:00Z",
}
monkeypatch.setattr(
savings_tracker_module,
"_utc_now",
lambda: datetime(2026, 3, 27, 9, 45, tzinfo=timezone.utc),
)
assert tracker.snapshot()["display_session"] == savings_tracker_module._empty_display_session()
tracker.record_request(
model="gpt-4o",
input_tokens=50,
tokens_saved=5,
timestamp="2026-03-27T10:05:00Z",
)
monkeypatch.setattr(
savings_tracker_module,
"_utc_now",
lambda: datetime(2026, 3, 27, 10, 10, tzinfo=timezone.utc),
)
rolled = tracker.snapshot()
assert rolled["lifetime"]["requests"] == 3
assert rolled["display_session"] == {
"requests": 1,
"tokens_saved": 5,
"compression_savings_usd": pytest.approx(0.005),
"cache_read_tokens": 0,
"cache_savings_usd": 0.0,
"total_input_tokens": 50,
"total_input_cost_usd": pytest.approx(0.05),
"savings_percent": pytest.approx(9.09),
"started_at": "2026-03-27T10:05:00Z",
"last_activity_at": "2026-03-27T10:05:00Z",
}
def test_savings_tracker_rollups_preserve_spend_and_input_history(tmp_path, monkeypatch):
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(
path=str(path),
max_history_points=100,
max_history_age_days=30,
)
monkeypatch.setattr(
"headroom.proxy.savings_tracker._estimate_compression_savings_usd",
lambda model, tokens_saved: tokens_saved / 1000.0,
)
tracker.record_compression_savings(
model="gpt-4o",
tokens_saved=100,
total_input_tokens=120,
total_input_cost_usd=0.24,
timestamp="2026-03-27T09:10:00Z",
)
tracker.record_compression_savings(
model="gpt-4o",
tokens_saved=50,
total_input_tokens=210,
total_input_cost_usd=0.42,
timestamp="2026-03-27T09:40:00Z",
)
tracker.record_compression_savings(
model="gpt-4o",
tokens_saved=25,
total_input_tokens=300,
total_input_cost_usd=0.63,
timestamp="2026-03-27T10:05:00Z",
)
tracker.record_compression_savings(
model="gpt-4o",
tokens_saved=10,
total_input_tokens=360,
total_input_cost_usd=0.75,
timestamp="2026-03-28T08:00:00Z",
)
tracker.record_compression_savings(
model="gpt-4o",
tokens_saved=20,
total_input_tokens=450,
total_input_cost_usd=0.93,
timestamp="2026-04-02T14:00:00Z",
)
response = tracker.history_response()
assert response["lifetime"]["tokens_saved"] == 205
assert response["lifetime"]["compression_savings_usd"] == pytest.approx(0.205)
assert response["lifetime"]["total_input_tokens"] == 450
assert response["lifetime"]["total_input_cost_usd"] == pytest.approx(0.93)
assert len(response["history"]) == 5
hourly = response["series"]["hourly"]
assert [point["timestamp"] for point in hourly] == [
"2026-03-27T09:00:00Z",
"2026-03-27T10:00:00Z",
"2026-03-28T08:00:00Z",
"2026-04-02T14:00:00Z",
]
assert hourly[0]["tokens_saved"] == 150
assert hourly[0]["total_tokens_saved"] == 150
assert hourly[0]["total_input_tokens_delta"] == 210
assert hourly[0]["total_input_tokens"] == 210
assert hourly[0]["total_input_cost_usd_delta"] == pytest.approx(0.42)
assert hourly[0]["total_input_cost_usd"] == pytest.approx(0.42)
assert hourly[1]["tokens_saved"] == 25
assert hourly[1]["total_tokens_saved"] == 175
assert hourly[1]["total_input_tokens_delta"] == 90
assert hourly[1]["total_input_tokens"] == 300
assert hourly[1]["total_input_cost_usd_delta"] == pytest.approx(0.21)
assert hourly[1]["total_input_cost_usd"] == pytest.approx(0.63)
assert hourly[2]["tokens_saved"] == 10
assert hourly[2]["total_tokens_saved"] == 185
assert hourly[2]["total_input_tokens_delta"] == 60
assert hourly[2]["total_input_tokens"] == 360
assert hourly[2]["total_input_cost_usd_delta"] == pytest.approx(0.12)
assert hourly[2]["total_input_cost_usd"] == pytest.approx(0.75)
assert hourly[3]["tokens_saved"] == 20
assert hourly[3]["total_tokens_saved"] == 205
assert hourly[3]["total_input_tokens_delta"] == 90
assert hourly[3]["total_input_tokens"] == 450
assert hourly[3]["total_input_cost_usd_delta"] == pytest.approx(0.18)
assert hourly[3]["total_input_cost_usd"] == pytest.approx(0.93)
daily = response["series"]["daily"]
assert [point["timestamp"] for point in daily] == [
"2026-03-27T00:00:00Z",
"2026-03-28T00:00:00Z",
"2026-04-02T00:00:00Z",
]
assert daily[0]["tokens_saved"] == 175
assert daily[0]["total_tokens_saved"] == 175
assert daily[0]["total_input_tokens_delta"] == 300
assert daily[0]["total_input_tokens"] == 300
assert daily[0]["total_input_cost_usd_delta"] == pytest.approx(0.63)
assert daily[0]["total_input_cost_usd"] == pytest.approx(0.63)
assert daily[1]["tokens_saved"] == 10
assert daily[1]["total_tokens_saved"] == 185
assert daily[1]["total_input_tokens_delta"] == 60
assert daily[1]["total_input_tokens"] == 360
assert daily[1]["total_input_cost_usd_delta"] == pytest.approx(0.12)
assert daily[1]["total_input_cost_usd"] == pytest.approx(0.75)
assert daily[2]["tokens_saved"] == 20
assert daily[2]["total_tokens_saved"] == 205
assert daily[2]["total_input_tokens_delta"] == 90
assert daily[2]["total_input_tokens"] == 450
assert daily[2]["total_input_cost_usd_delta"] == pytest.approx(0.18)
assert daily[2]["total_input_cost_usd"] == pytest.approx(0.93)
weekly = response["series"]["weekly"]
assert [point["timestamp"] for point in weekly] == [
"2026-03-23T00:00:00Z",
"2026-03-30T00:00:00Z",
]
assert weekly[0]["tokens_saved"] == 185
assert weekly[0]["total_tokens_saved"] == 185
assert weekly[1]["tokens_saved"] == 20
assert weekly[1]["total_tokens_saved"] == 205
monthly = response["series"]["monthly"]
assert [point["timestamp"] for point in monthly] == [
"2026-03-01T00:00:00Z",
"2026-04-01T00:00:00Z",
]
assert monthly[0]["tokens_saved"] == 185
assert monthly[0]["total_tokens_saved"] == 185
assert monthly[1]["tokens_saved"] == 20
assert monthly[1]["total_tokens_saved"] == 205
assert response["exports"]["available_formats"] == ["json", "csv"]
assert response["exports"]["available_series"] == [
"history",
"hourly",
"daily",
"weekly",
"monthly",
]
def test_savings_tracker_rollup_attributes_savings_per_provider(tmp_path, monkeypatch):
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(
path=str(path),
max_history_points=100,
max_history_age_days=30,
)
monkeypatch.setattr(
"headroom.proxy.savings_tracker._estimate_compression_savings_usd",
lambda model, tokens_saved: tokens_saved / 1000.0,
)
# Two providers active in the same hour bucket.
tracker.record_compression_savings(
model="claude-3-5-sonnet",
tokens_saved=100,
provider="anthropic",
total_input_tokens=120,
total_input_cost_usd=0.24,
timestamp="2026-03-27T09:10:00Z",
)
tracker.record_compression_savings(
model="gpt-4o",
tokens_saved=40,
provider="openai",
total_input_tokens=200,
total_input_cost_usd=0.40,
timestamp="2026-03-27T09:40:00Z",
)
# Only anthropic active in the next hour bucket.
tracker.record_compression_savings(
model="claude-3-5-sonnet",
tokens_saved=25,
provider="anthropic",
total_input_tokens=260,
total_input_cost_usd=0.52,
timestamp="2026-03-27T10:05:00Z",
)
# A legacy-style record with no provider collapses into "unknown".
tracker.record_compression_savings(
model="gpt-4o",
tokens_saved=15,
total_input_tokens=320,
total_input_cost_usd=0.64,
timestamp="2026-03-27T11:00:00Z",
)
hourly = tracker.history_response()["series"]["hourly"]
first = hourly[0]
assert first["tokens_saved"] == 140
assert set(first["by_provider"]) == {"anthropic", "openai"}
assert first["by_provider"]["anthropic"]["tokens_saved"] == 100
assert first["by_provider"]["anthropic"]["total_input_tokens_delta"] == 120
assert first["by_provider"]["anthropic"]["compression_savings_usd_delta"] == pytest.approx(0.1)
assert first["by_provider"]["anthropic"]["total_input_cost_usd_delta"] == pytest.approx(0.24)
assert first["by_provider"]["openai"]["tokens_saved"] == 40
assert first["by_provider"]["openai"]["total_input_tokens_delta"] == 80
assert first["by_provider"]["openai"]["compression_savings_usd_delta"] == pytest.approx(0.04)
assert first["by_provider"]["openai"]["total_input_cost_usd_delta"] == pytest.approx(0.16)
# Per-provider deltas sum back to the bucket total.
assert (
first["by_provider"]["anthropic"]["tokens_saved"]
+ first["by_provider"]["openai"]["tokens_saved"]
== first["tokens_saved"]
)
second = hourly[1]
assert set(second["by_provider"]) == {"anthropic"}
assert second["by_provider"]["anthropic"]["tokens_saved"] == 25
assert second["by_provider"]["anthropic"]["total_input_tokens_delta"] == 60
third = hourly[2]
assert set(third["by_provider"]) == {"unknown"}
assert third["by_provider"]["unknown"]["tokens_saved"] == 15
def test_savings_tracker_rollup_attributes_savings_per_model(tmp_path, monkeypatch):
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(
path=str(path),
max_history_points=100,
max_history_age_days=30,
)
monkeypatch.setattr(
"headroom.proxy.savings_tracker._estimate_compression_savings_usd",
lambda model, tokens_saved: tokens_saved / 1000.0,
)
# Two models from the same provider land in the same bucket.
tracker.record_compression_savings(
model="claude-sonnet-4-6",
tokens_saved=100,
provider="anthropic",
total_input_tokens=120,
total_input_cost_usd=0.24,
timestamp="2026-03-27T09:10:00Z",
)
tracker.record_compression_savings(
model="claude-opus-4-8",
tokens_saved=40,
provider="anthropic",
total_input_tokens=200,
total_input_cost_usd=0.40,
timestamp="2026-03-27T09:40:00Z",
)
tracker.record_compression_savings(
model="claude-sonnet-4-6",
tokens_saved=25,
provider="anthropic",
total_input_tokens=260,
total_input_cost_usd=0.52,
timestamp="2026-03-27T10:05:00Z",
)
response = tracker.history_response()
# Checkpoints persist the model alongside the provider.
assert [point["model"] for point in response["history"]] == [
"claude-sonnet-4-6",
"claude-opus-4-8",
"claude-sonnet-4-6",
]
hourly = response["series"]["hourly"]
first = hourly[0]
assert set(first["by_model"]) == {"claude-sonnet-4-6", "claude-opus-4-8"}
assert first["by_model"]["claude-sonnet-4-6"]["tokens_saved"] == 100
assert first["by_model"]["claude-sonnet-4-6"]["total_input_tokens_delta"] == 120
assert first["by_model"]["claude-sonnet-4-6"]["compression_savings_usd_delta"] == pytest.approx(
0.1
)
assert first["by_model"]["claude-sonnet-4-6"]["total_input_cost_usd_delta"] == pytest.approx(
0.24
)
assert first["by_model"]["claude-opus-4-8"]["tokens_saved"] == 40
# Per-model deltas sum back to the bucket total.
assert (
first["by_model"]["claude-sonnet-4-6"]["tokens_saved"]
+ first["by_model"]["claude-opus-4-8"]["tokens_saved"]
== first["tokens_saved"]
)
second = hourly[1]
assert set(second["by_model"]) == {"claude-sonnet-4-6"}
assert second["by_model"]["claude-sonnet-4-6"]["tokens_saved"] == 25
# The expected no-headroom cost is derivable per bucket: actual input cost
# delta plus the compression savings delta.
sonnet = first["by_model"]["claude-sonnet-4-6"]
assert sonnet["total_input_cost_usd_delta"] + sonnet["compression_savings_usd_delta"] == (
pytest.approx(0.34)
)
def test_legacy_checkpoints_without_model_collapse_into_unknown(tmp_path):
path = tmp_path / "proxy_savings.json"
legacy_state = {
"schema_version": 2,
"lifetime": {
"requests": 1,
"tokens_saved": 50,
"compression_savings_usd": 0.05,
"total_input_tokens": 100,
"total_input_cost_usd": 0.2,
},
"history": [
{
"timestamp": "2026-03-27T09:10:00Z",
"provider": "anthropic",
"total_tokens_saved": 50,
"compression_savings_usd": 0.05,
"total_input_tokens": 100,
"total_input_cost_usd": 0.2,
}
],
}
path.write_text(json.dumps(legacy_state), encoding="utf-8")
tracker = SavingsTracker(path=str(path))
response = tracker.history_response()
assert response["history"][0]["model"] == "unknown"
hourly = response["series"]["hourly"]
assert set(hourly[0]["by_model"]) == {"unknown"}
assert hourly[0]["by_model"]["unknown"]["tokens_saved"] == 50
def test_stats_history_defaults_to_compact_history_but_can_return_full_history(
tmp_path, monkeypatch
):
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(
path=str(path),
max_history_points=100,
max_history_age_days=30,
max_response_history_points=5,
)
monkeypatch.setattr(
"headroom.proxy.savings_tracker._estimate_compression_savings_usd",
lambda model, tokens_saved: tokens_saved / 1000.0,
)
for i in range(8):
tracker.record_compression_savings(
model="gpt-4o",
tokens_saved=10,
total_input_tokens=(i + 1) * 100,
total_input_cost_usd=(i + 1) * 0.1,
timestamp=f"2026-03-27T09:{i:02d}:00Z",
)
compact = tracker.history_response()
assert compact["history_summary"] == {
"mode": "compact",
"stored_points": 8,
"returned_points": 5,
"compacted": True,
}
assert len(compact["history"]) == 5
assert compact["history"][0]["timestamp"] == "2026-03-27T09:00:00Z"
assert compact["history"][-1]["timestamp"] == "2026-03-27T09:07:00Z"
full = tracker.history_response(history_mode="full")
assert full["history_summary"] == {
"mode": "full",
"stored_points": 8,
"returned_points": 8,
"compacted": False,
}
assert len(full["history"]) == 8
none = tracker.history_response(history_mode="none")
assert none["history"] == []
assert none["history_summary"] == {
"mode": "none",
"stored_points": 8,
"returned_points": 0,
"compacted": True,
}
def test_stats_history_persists_across_restarts_and_stats_stays_compatible(tmp_path, monkeypatch):
savings_path = tmp_path / "proxy_savings.json"
monkeypatch.setenv("HEADROOM_SAVINGS_PATH", str(savings_path))
monkeypatch.setattr(
"headroom.proxy.server.CostTracker._get_cache_prices",
lambda self, model: (0.001, 0.0015, 0.002),
)
config = ProxyConfig(
cache_enabled=False,
rate_limit_enabled=False,
log_requests=False,
)
with TestClient(create_app(config)) as client:
_record_request(client, model="gpt-4o", tokens_saved=40)
stats = client.get("/stats")
assert stats.status_code == 200
stats_data = stats.json()
assert "savings_history" in stats_data
assert "persistent_savings" in stats_data
assert all(len(point) == 2 for point in stats_data["savings_history"])
assert stats_data["persistent_savings"]["lifetime"]["tokens_saved"] == 40
assert stats_data["persistent_savings"]["storage_path"] == str(savings_path)
metrics = client.get("/metrics")
assert metrics.status_code == 200
assert "headroom_tokens_saved_total 40" in metrics.text
assert "headroom_persistent_savings_tokens_saved_total 40" in metrics.text
assert "headroom_persistent_savings_requests_total 1" in metrics.text
history = client.get("/stats-history")
assert history.status_code == 200
history_data = history.json()
assert history_data["schema_version"] == 5
assert history_data["storage_path"] == str(savings_path)
assert history_data["lifetime"]["tokens_saved"] == 40
assert history_data["lifetime"]["total_input_tokens"] == 120
assert history_data["lifetime"]["total_input_cost_usd"] == pytest.approx(0.24)
assert history_data["display_session"]["requests"] == 1
assert history_data["display_session"]["tokens_saved"] == 40
assert history_data["display_session"]["total_input_tokens"] == 120
assert history_data["display_session"]["savings_percent"] == pytest.approx(25.0)
assert list(history_data["series"].keys()) == [
"hourly",
"daily",
"weekly",
"monthly",
]
assert history_data["exports"]["available_series"][-2:] == ["weekly", "monthly"]
assert history_data["series"]["hourly"][0]["total_input_tokens_delta"] == 120
assert history_data["series"]["hourly"][0]["total_input_cost_usd_delta"] == pytest.approx(
0.24
)
assert history_data["history_summary"] == {
"mode": "compact",
"stored_points": 1,
"returned_points": 1,
"compacted": False,
}
assert stats_data["display_session"] == history_data["display_session"]
assert (
stats_data["persistent_savings"]["display_session"] == history_data["display_session"]
)
with TestClient(create_app(config)) as client:
history = client.get("/stats-history")
assert history.status_code == 200
assert history.json()["lifetime"]["tokens_saved"] == 40
assert history.json()["display_session"]["requests"] == 1
metrics = client.get("/metrics")
assert metrics.status_code == 200
assert "headroom_tokens_saved_total 0" in metrics.text
assert "headroom_persistent_savings_tokens_saved_total 40" in metrics.text
assert "headroom_persistent_savings_requests_total 1" in metrics.text
_record_request(client, model="gpt-4o", tokens_saved=15)
updated = client.get("/stats-history").json()
assert updated["lifetime"]["tokens_saved"] == 55
assert updated["lifetime"]["total_input_tokens"] == 240
assert updated["lifetime"]["total_input_cost_usd"] == pytest.approx(0.48)
assert updated["lifetime"]["requests"] == 2
assert len(updated["history"]) == 2
assert updated["display_session"]["requests"] == 2
assert updated["display_session"]["tokens_saved"] == 55
assert updated["display_session"]["total_input_tokens"] == 240
assert updated["display_session"]["savings_percent"] == pytest.approx(18.64)
assert updated["series"]["daily"][0]["total_input_tokens_delta"] == 240
assert updated["series"]["daily"][0]["total_input_cost_usd_delta"] == pytest.approx(0.48)
metrics = client.get("/metrics")
assert metrics.status_code == 200
assert "headroom_tokens_saved_total 15" in metrics.text
assert "headroom_persistent_savings_tokens_saved_total 55" in metrics.text
assert "headroom_persistent_savings_requests_total 2" in metrics.text
full = client.get("/stats-history?history_mode=full").json()
assert full["history_summary"]["mode"] == "full"
assert full["history_summary"]["stored_points"] == 2
assert full["history_summary"]["returned_points"] == 2
# The proxy batches savings writes, so force a flush before reading the
# file directly mid-session (a graceful shutdown flushes automatically).
client.app.state.proxy.metrics.savings_tracker.flush()
persisted = json.loads(savings_path.read_text())
assert persisted["lifetime"]["tokens_saved"] == 55
assert persisted["lifetime"]["total_input_tokens"] == 240
assert persisted["lifetime"]["total_input_cost_usd"] == pytest.approx(0.48)
assert persisted["display_session"]["requests"] == 2
def test_savings_tracker_batches_saves_and_matches_immediate(tmp_path):
"""save_flush_every batches disk writes; the threshold and flush() together
produce the exact on-disk state an immediate (flush_every=1) tracker would.
Proves the batch boundary drops no data — the correctness half of the perf
fix, independent of timing.
"""
events = [
{
"model": "gpt-4o",
"input_tokens": 120,
"tokens_saved": 10,
"timestamp": "2026-03-27T09:00:00Z",
},
{
"model": "gpt-4o",
"input_tokens": 80,
"tokens_saved": 5,
"timestamp": "2026-03-27T09:01:00Z",
},
{
"model": "gpt-4o",
"input_tokens": 200,
"tokens_saved": 25,
"timestamp": "2026-03-27T09:02:00Z",
},
]
# Baseline: persists on every call (default save_flush_every=1).
immediate_path = tmp_path / "immediate.json"
immediate = SavingsTracker(path=str(immediate_path))
for event in events:
immediate.record_request(**event)
# Batched: writes only every 2 records; the tail lands on flush().
batched_path = tmp_path / "batched.json"
batched = SavingsTracker(path=str(batched_path), save_flush_every=2)
batched.record_request(**events[0])
assert not batched_path.exists() # buffered, below threshold
batched.record_request(**events[1])
assert batched_path.exists() # threshold reached, written
batched.record_request(**events[2]) # buffered again
batched.flush() # tail persisted
batched_payload = json.loads(batched_path.read_text(encoding="utf-8"))
immediate_payload = json.loads(immediate_path.read_text(encoding="utf-8"))
for payload in (batched_payload, immediate_payload):
payload["lifetime_metrics"]["persistence"].pop("last_saved_at", None)
assert batched_payload == immediate_payload
def test_failed_save_retries_on_next_record_not_after_full_window(tmp_path, monkeypatch):
"""A transient write failure must not consume the flush window.
The counter only resets after a durable write, so a save that raises leaves
it untouched and the next record retries immediately, rather than waiting
another save_flush_every calls.
"""
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path), save_flush_every=5)
calls = {"n": 0}
real_mkstemp = tempfile.mkstemp
def flaky_mkstemp(*args, **kwargs):
calls["n"] += 1
if calls["n"] == 1:
raise OSError("simulated transient write failure")
return real_mkstemp(*args, **kwargs)
monkeypatch.setattr(savings_tracker_module.tempfile, "mkstemp", flaky_mkstemp)
for _ in range(5):
tracker.record_request(model="gpt-4o", input_tokens=10, tokens_saved=5)
assert not path.exists() # 5th call reached the threshold; its save failed
# The 6th call must retry the save, not wait until the 10th.
tracker.record_request(model="gpt-4o", input_tokens=10, tokens_saved=5)
assert path.exists()
assert json.loads(path.read_text(encoding="utf-8"))["lifetime"]["requests"] == 6
def test_stats_history_csv_export_is_frontend_friendly(tmp_path, monkeypatch):
savings_path = tmp_path / "proxy_savings.json"
monkeypatch.setenv("HEADROOM_SAVINGS_PATH", str(savings_path))
monkeypatch.setattr(
"headroom.proxy.server.CostTracker._get_cache_prices",
lambda self, model: (0.001, 0.0015, 0.002),
)
config = ProxyConfig(
cache_enabled=False,
rate_limit_enabled=False,
log_requests=False,
)
with TestClient(create_app(config)) as client:
_record_request(client, model="gpt-4o", tokens_saved=40)
_record_request(client, model="gpt-4o", tokens_saved=10)
response = client.get("/stats-history?format=csv&series=daily")
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/csv")
assert (
'attachment; filename="headroom-stats-history-daily.csv"'
== response.headers["content-disposition"]
)
lines = response.text.strip().splitlines()
assert lines[0] == (
"timestamp,tokens_saved,compression_savings_usd_delta,total_tokens_saved,"
"compression_savings_usd,total_input_tokens_delta,total_input_tokens,"
"total_input_cost_usd_delta,total_input_cost_usd,"
"output_tokens_saved_delta,output_savings_usd_delta"
)
assert len(lines) >= 2
assert "total_tokens_saved" in lines[0]
assert "total_input_cost_usd" in lines[0]
def test_malformed_savings_state_is_ignored_safely(tmp_path, monkeypatch):
savings_path = tmp_path / "proxy_savings.json"
savings_path.write_text("{not valid json", encoding="utf-8")
monkeypatch.setenv("HEADROOM_SAVINGS_PATH", str(savings_path))
config = ProxyConfig(
cache_enabled=False,
rate_limit_enabled=False,
log_requests=False,
)
with TestClient(create_app(config)) as client:
response = client.get("/stats-history")
assert response.status_code == 200
data = response.json()
assert data["lifetime"]["tokens_saved"] == 0
assert data["history"] == []
def test_dashboard_includes_history_toggle_and_endpoint(tmp_path, monkeypatch):
savings_path = tmp_path / "proxy_savings.json"
monkeypatch.setenv("HEADROOM_SAVINGS_PATH", str(savings_path))
config = ProxyConfig(
cache_enabled=False,
rate_limit_enabled=False,
log_requests=False,
)
with TestClient(create_app(config)) as client:
response = client.get("/dashboard")
assert response.status_code == 200
html = response.text
assert "Session" in html
assert "Historical" in html
assert "fetch('/stats-history')" in html
assert "Export CSV" in html
assert "Weekly Savings" in html
assert "Monthly Savings" in html
assert "Per-Model Breakdown" in html
assert "historyChartModeOptions" in html
assert "Expected cost (without Headroom)" in html
assert "toggleHistoryModel" in html
# Checkpoint view plots no per-model lines, so an active model
# filter must not suppress the aggregate line there.
assert "if (this.historySelectedSeriesKey === 'history') return null;" in html
# Breakdown header labels the effective (substituted) series.
assert "historyModelSourceSeriesLabel + ' buckets'" in html
# Non-top-5 breakdown rows swap into the last chart slot when selected.
assert "topModels[topModels.length - 1] = selected;" in html
def test_coercion_helpers_reject_non_finite_values():
"""Non-finite inputs fail open to the default -- never raise, never leak NaN/inf.
_coerce_int raised OverflowError on inf; _coerce_float returned NaN/inf
verbatim, poisoning arithmetic and emitting JSON the dashboard's JSON.parse
rejects.
"""
ci = savings_tracker_module._coerce_int
cf = savings_tracker_module._coerce_float
# _coerce_int: every non-finite / overflowing input collapses to default.
assert ci(float("inf")) == 0
assert ci(float("-inf")) == 0
assert ci(float("nan")) == 0
assert ci(float("inf"), default=7) == 7
# _coerce_float: nan/inf never raise on float() and must be rejected;
# an int too large to convert raises OverflowError and must be caught.
assert cf(float("nan")) == 0.0
assert math.isfinite(cf(float("nan")))
assert cf(float("inf")) == 0.0
assert cf(float("-inf")) == 0.0
assert cf(10**400) == 0.0
assert cf(float("nan"), default=1.5) == 1.5
# Regression: finite values still coerce unchanged.
assert ci(5) == 5
assert cf(3.5) == pytest.approx(3.5)
def test_savings_tracker_loads_non_finite_persisted_state_without_crashing(tmp_path):
"""A proxy_savings.json holding NaN/Infinity must not crash construction.
json.loads accepts bare NaN/Infinity, so a prior bad write leaves them on
disk. Before the fix, _coerce_int(inf) in _sanitize_state raised
OverflowError out of __init__ and the proxy failed to start.
"""
path = tmp_path / "proxy_savings.json"
# json.dumps emits bare NaN/Infinity literals (allow_nan default) -- exactly
# what a prior non-finite write would leave on disk.
path.write_text(
json.dumps(
{
"schema_version": 3,
"lifetime": {
"requests": 1,
"tokens_saved": float("inf"),
"compression_savings_usd": float("nan"),
"total_input_tokens": float("inf"),
"total_input_cost_usd": float("nan"),
},
"history": [
{
"timestamp": "2026-03-27T09:00:00Z",
"total_tokens_saved": float("inf"),
"compression_savings_usd": float("nan"),
}
],
}
),
encoding="utf-8",
)
tracker = SavingsTracker(path=str(path))
lifetime = tracker.snapshot()["lifetime"]
# Non-finite fields fail open to safe defaults, not crash or NaN.
for key, value in lifetime.items():
assert isinstance(value, int | float)
assert math.isfinite(value), f"{key} is non-finite: {value}"
assert lifetime["tokens_saved"] == 0
assert lifetime["total_input_tokens"] == 0
def test_cache_read_savings_accumulate_and_survive_restart(tmp_path, monkeypatch):
path = tmp_path / "proxy_savings.json"
monkeypatch.setattr(
savings_tracker_module,
"_estimate_cache_savings_usd",
lambda model, cache_read_tokens: cache_read_tokens / 1_000_000.0,
raising=False,
)
# Pin "now" just after the recorded timestamps so the display session
# reads as active at snapshot time.
monkeypatch.setattr(
savings_tracker_module,
"_utc_now",
lambda: datetime(2026, 7, 1, 9, 5, tzinfo=timezone.utc),
)
tracker = SavingsTracker(path=str(path))
tracker.record_request(
model="claude-opus-4-8",
input_tokens=1_000,
tokens_saved=0,
cache_read_tokens=800_000,
timestamp="2026-07-01T09:00:00Z",
)
tracker.record_request(
model="claude-opus-4-8",
input_tokens=1_000,
tokens_saved=0,
cache_read_tokens=800_000,
timestamp="2026-07-01T09:01:00Z",
)
snapshot = tracker.snapshot()
assert snapshot["lifetime"]["cache_read_tokens"] == 1_600_000
assert snapshot["lifetime"]["cache_savings_usd"] == pytest.approx(1.6)
assert snapshot["display_session"]["cache_read_tokens"] == 1_600_000
assert snapshot["display_session"]["cache_savings_usd"] == pytest.approx(1.6)
# Restart: a fresh tracker on the same file sees the persisted totals (AE1).
reloaded = SavingsTracker(path=str(path))
assert reloaded.snapshot()["lifetime"]["cache_read_tokens"] == 1_600_000
assert reloaded.snapshot()["lifetime"]["cache_savings_usd"] == pytest.approx(1.6)
assert reloaded.stats_preview()["lifetime"]["cache_read_tokens"] == 1_600_000
assert reloaded.history_response()["lifetime"]["cache_read_tokens"] == 1_600_000
def test_by_model_savings_accumulate_and_survive_restart(tmp_path):
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path))
tracker.record_request(
model="gpt-4o",
input_tokens=100,
tokens_saved=40,
timestamp="2026-07-01T09:00:00Z",
)
tracker.record_request(
model="claude-sonnet-4-6",
input_tokens=300,
tokens_saved=60,
timestamp="2026-07-01T09:01:00Z",
)
persisted = json.loads(path.read_text(encoding="utf-8"))
assert set(persisted["by_model"]) == {"gpt-4o", "claude-sonnet-4-6"}
assert persisted["by_model"]["gpt-4o"]["tokens_saved"] == 40
assert persisted["by_model"]["gpt-4o"]["total_input_tokens"] == 100
reloaded = SavingsTracker(path=str(path))
stats_by_model = reloaded.stats_preview()["by_model"]
assert set(stats_by_model) == {"gpt-4o", "claude-sonnet-4-6"}
assert stats_by_model["claude-sonnet-4-6"]["tokens_saved"] == 60
assert stats_by_model["claude-sonnet-4-6"]["total_input_tokens"] == 300
assert stats_by_model["claude-sonnet-4-6"]["savings_percent"] == 16.67
assert reloaded.history_response()["by_model"] == stats_by_model
def test_v3_state_without_cache_fields_loads_clean_and_saves_v4(tmp_path):
path = tmp_path / "proxy_savings.json"
path.write_text(
json.dumps(
{
"schema_version": 3,
"lifetime": {
"requests": 6088,
"tokens_saved": 42181,
"compression_savings_usd": 0.5,
"total_input_tokens": 1_294_591_655,
"total_input_cost_usd": 12.5,
},
"history": [],
"projects": {},
}
),
encoding="utf-8",
)
tracker = SavingsTracker(path=str(path))
snapshot = tracker.snapshot()
# AE2: missing cache fields read as zero; compression data intact.
assert snapshot["lifetime"]["cache_read_tokens"] == 0
assert snapshot["lifetime"]["cache_savings_usd"] == 0.0
assert snapshot["lifetime"]["tokens_saved"] == 42181
assert snapshot["lifetime"]["total_input_tokens"] == 1_294_591_655
tracker.record_request(
model="unknown-model",
input_tokens=10,
tokens_saved=0,
cache_read_tokens=5,
timestamp="2026-07-02T00:00:00Z",
)
persisted = json.loads(path.read_text(encoding="utf-8"))
assert persisted["schema_version"] == 5
assert persisted["lifetime"]["cache_read_tokens"] == 5
assert persisted["lifetime"]["tokens_saved"] == 42181
def test_stateless_tracker_accumulates_cache_savings_in_memory_only(tmp_path):
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path), stateless=True)
tracker.record_request(
model="unknown-model",
input_tokens=100,
tokens_saved=0,
cache_read_tokens=1_234,
timestamp="2026-07-02T00:00:00Z",
)
# AE3: in-memory totals update; nothing is written.
assert tracker.snapshot()["lifetime"]["cache_read_tokens"] == 1_234
assert not path.exists()
def test_active_display_session_without_cache_fields_reloads_safely(tmp_path, monkeypatch):
# Pin "now" so the display session reads as active regardless of when the
# suite runs (snapshot() expiry-checks against _utc_now).
monkeypatch.setattr(
savings_tracker_module,
"_utc_now",
lambda: datetime(2026, 7, 2, 0, 10, tzinfo=timezone.utc),
)
path = tmp_path / "proxy_savings.json"
path.write_text(
json.dumps(
{
"schema_version": 3,
"lifetime": {
"requests": 1,
"tokens_saved": 0,
"compression_savings_usd": 0.0,
"total_input_tokens": 100,
"total_input_cost_usd": 0.0,
},
"display_session": {
"requests": 1,
"tokens_saved": 0,
"compression_savings_usd": 0.0,
"total_input_tokens": 100,
"total_input_cost_usd": 0.0,
"savings_percent": 0.0,
"started_at": "2026-07-02T00:00:00Z",
"last_activity_at": "2026-07-02T00:00:00Z",
},
"history": [],
"projects": {},
}
),
encoding="utf-8",
)
tracker = SavingsTracker(path=str(path))
# Guards the _normalize_display_session whitelist rebuild (R2): a reload
# within the inactivity window must not KeyError and must accumulate from 0.
tracker.record_request(
model="unknown-model",
input_tokens=10,
tokens_saved=0,
cache_read_tokens=7,
timestamp="2026-07-02T00:05:00Z",
)
session = tracker.snapshot()["display_session"]
assert session["cache_read_tokens"] == 7
assert session["requests"] == 2
def test_cache_savings_edge_cases_zero_and_unpriced(tmp_path, monkeypatch):
# Pin a litellm whose price table doesn't know the model, so this stays a
# test of the unpriced-model path on every environment — on installs
# without litellm (e.g. Python 3.14) the blended-rate fallback would
# otherwise kick in and produce a nonzero estimate.
fake_litellm = SimpleNamespace(model_cost={})
monkeypatch.setattr(savings_tracker_module, "_get_litellm_module", lambda: fake_litellm)
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path))
tracker.record_request(
model="unknown-model",
input_tokens=10,
tokens_saved=0,
cache_read_tokens=0,
timestamp="2026-07-02T00:00:00Z",
)
snapshot = tracker.snapshot()
assert snapshot["lifetime"]["cache_read_tokens"] == 0
assert snapshot["lifetime"]["cache_savings_usd"] == 0.0
# Unpriced model: tokens accumulate, USD stays 0.0 (fail-open pricing).
tracker.record_request(
model="unknown-model",
input_tokens=10,
tokens_saved=0,
cache_read_tokens=50,
timestamp="2026-07-02T00:01:00Z",
)
snapshot = tracker.snapshot()
assert snapshot["lifetime"]["cache_read_tokens"] == 50
assert snapshot["lifetime"]["cache_savings_usd"] == 0.0
def test_display_session_rollover_resets_cache_fields(tmp_path, monkeypatch):
# Pin "now" just after the second request so the 1-minute window judges
# the rolled session active regardless of when the suite runs.
monkeypatch.setattr(
savings_tracker_module,
"_utc_now",
lambda: datetime(2026, 7, 2, 2, 0, 30, tzinfo=timezone.utc),
)
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path), display_session_inactivity_minutes=1)
tracker.record_request(
model="unknown-model",
input_tokens=10,
tokens_saved=0,
cache_read_tokens=100,
timestamp="2026-07-02T00:00:00Z",
)
tracker.record_request(
model="unknown-model",
input_tokens=10,
tokens_saved=0,
cache_read_tokens=25,
timestamp="2026-07-02T02:00:00Z",
)
snapshot = tracker.snapshot()
assert snapshot["display_session"]["cache_read_tokens"] == 25
assert snapshot["lifetime"]["cache_read_tokens"] == 125
def test_cache_savings_usd_uses_litellm_discount_delta(tmp_path, monkeypatch):
fake_litellm = SimpleNamespace(
model_cost={
"priced-model": {
"input_cost_per_token": 3e-06,
"cache_read_input_token_cost": 3e-07,
},
"no-discount-model": {"input_cost_per_token": 3e-06},
"inverted-model": {
"input_cost_per_token": 3e-06,
"cache_read_input_token_cost": 5e-06,
},
}
)
monkeypatch.setattr(savings_tracker_module, "_get_litellm_module", lambda: fake_litellm)
monkeypatch.setattr(savings_tracker_module, "_resolve_litellm_model", lambda model: model)
# Real discount delta: 1M reads x (3e-06 - 3e-07) = $2.70.
assert savings_tracker_module._estimate_cache_savings_usd(
"priced-model", 1_000_000
) == pytest.approx(2.7)
# Missing cache_read_input_token_cost falls back to list price: discount 0.
assert savings_tracker_module._estimate_cache_savings_usd("no-discount-model", 1_000_000) == 0.0
# A non-positive discount never produces negative savings.
assert savings_tracker_module._estimate_cache_savings_usd("inverted-model", 1_000_000) == 0.0
tracker = SavingsTracker(path=str(tmp_path / "proxy_savings.json"))
tracker.record_request(
model="priced-model",
input_tokens=1_000,
tokens_saved=0,
cache_read_tokens=1_000_000,
timestamp="2026-07-02T00:00:00Z",
)
assert tracker.snapshot()["lifetime"]["cache_savings_usd"] == pytest.approx(2.7)
def test_cache_savings_usd_falls_back_when_litellm_unavailable(tmp_path, monkeypatch):
# Regression: on any install without litellm (e.g. Python 3.14, where
# headroom-ai's own dependency spec excludes it), cache_savings_usd must
# use the same DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN estimate that
# _estimate_input_cost_usd already falls back to — not silently read as
# $0 forever while cache_read_tokens and total_input_cost_usd keep
# accumulating normally.
monkeypatch.setattr(savings_tracker_module, "LITELLM_AVAILABLE", False)
monkeypatch.setattr(savings_tracker_module, "litellm", None)
fallback_rate = savings_tracker_module.DEFAULT_FALLBACK_INPUT_COST_PER_TOKEN
assert savings_tracker_module._estimate_cache_savings_usd(
"claude-sonnet-4-6", 1_000_000
) == pytest.approx(1_000_000 * fallback_rate)
tracker = SavingsTracker(path=str(tmp_path / "proxy_savings.json"))
tracker.record_request(
model="claude-sonnet-4-6",
input_tokens=1_000,
tokens_saved=0,
cache_read_tokens=1_000_000,
timestamp="2026-07-02T00:00:00Z",
)
snapshot = tracker.snapshot()
assert snapshot["lifetime"]["cache_read_tokens"] == 1_000_000
assert snapshot["lifetime"]["cache_savings_usd"] > 0.0
assert snapshot["lifetime"]["cache_savings_usd"] == pytest.approx(1_000_000 * fallback_rate)
def test_non_finite_state_values_coerce_to_defaults(tmp_path):
path = tmp_path / "proxy_savings.json"
# json accepts bare Infinity/NaN literals; a corrupted file must not crash
# startup or poison accumulators (NaN is absorbing under +=).
path.write_text(
'{"schema_version": 4, "lifetime": {"requests": 1, "tokens_saved": 2, '
'"compression_savings_usd": NaN, "cache_read_tokens": Infinity, '
'"cache_savings_usd": NaN, "total_input_tokens": 100, '
'"total_input_cost_usd": 0.5}, "history": [], "projects": {}}',
encoding="utf-8",
)
tracker = SavingsTracker(path=str(path))
lifetime = tracker.snapshot()["lifetime"]
assert lifetime["cache_read_tokens"] == 0
assert lifetime["cache_savings_usd"] == 0.0
assert lifetime["compression_savings_usd"] == 0.0
assert lifetime["tokens_saved"] == 2
tracker.record_request(
model="unknown-model",
input_tokens=10,
tokens_saved=0,
cache_read_tokens=5,
timestamp="2026-07-02T00:00:00Z",
)
assert tracker.snapshot()["lifetime"]["cache_read_tokens"] == 5
def test_cache_only_request_still_appends_a_history_point(tmp_path):
# Regression: in --mode cache, tokens_saved is ~always 0 (the frozen
# prefix is byte-replayed, not lossy-compressed, to keep the provider's
# prompt cache warm). The history-append guard used to gate on
# tokens_saved alone, so a cache-only deployment silently wrote zero
# history points regardless of real cache_read_tokens/cache_savings_usd —
# making headroom-monthly-style tooling read as a total collapse.
path = tmp_path / "proxy_savings.json"
tracker = SavingsTracker(path=str(path))
assert tracker.snapshot()["history"] == []
tracker.record_request(
model="claude-sonnet-5",
input_tokens=60_000,
tokens_saved=0,
cache_read_tokens=50_000,
timestamp="2026-07-13T22:15:00Z",
)
history = tracker.snapshot()["history"]
assert len(history) == 1
entry = history[0]
assert entry["total_tokens_saved"] == 0
assert entry["cache_read_tokens"] == 50_000
assert entry["cache_savings_usd"] > 0.0
# A request with neither compression nor cache savings still appends
# nothing — this isn't "always append," only "append on either saving."
tracker.record_request(
model="claude-sonnet-5",
input_tokens=60_000,
tokens_saved=0,
cache_read_tokens=0,
timestamp="2026-07-13T22:16:00Z",
)
assert len(tracker.snapshot()["history"]) == 1
def test_normalize_history_entry_defaults_missing_cache_fields(tmp_path):
# Regression: history points written before cache-savings tracking existed
# have no cache_read_tokens/cache_savings_usd keys at all. Loading them
# back must default to 0/0.0, not raise or silently drop the entry.
path = tmp_path / "proxy_savings.json"
path.write_text(
json.dumps(
{
"schema_version": 4,
"lifetime": {
"requests": 1,
"tokens_saved": 40,
"compression_savings_usd": 0.04,
"total_input_tokens": 200,
"total_input_cost_usd": 0.4,
},
"history": [
{
"timestamp": "2026-07-01T00:00:00Z",
"provider": "anthropic",
"model": "claude-sonnet-5",
"total_tokens_saved": 40,
"compression_savings_usd": 0.04,
"total_input_tokens": 200,
"total_input_cost_usd": 0.4,
}
],
"projects": {},
}
),
encoding="utf-8",
)
tracker = SavingsTracker(path=str(path))
history = tracker.snapshot()["history"]
assert len(history) == 1
assert history[0]["cache_read_tokens"] == 0
assert history[0]["cache_savings_usd"] == 0.0
assert history[0]["total_tokens_saved"] == 40