pompom / tests /test_usage.py
tinjyuu's picture
Add USD estimates, configurable sampled reactions, and in-memory action photos
13759b5
Raw History Blame Contribute Delete
6.26 kB
import json
import stat
import threading
from concurrent.futures import ThreadPoolExecutor
import httpx
from support import Clock
from test_api import decision
from pompom.api import ApiError, DirectApi
from pompom.config import Settings
from pompom.usage import COST_COUNTERS, UsageMeter
def test_usage_accumulates_real_reported_values_and_survives_restart(tmp_path):
path = tmp_path / "usage.json"
meter = UsageMeter(path)
meter.record(True, {"input_tokens": 100, "output_tokens": 2, "total_tokens": 102})
meter.record(True, {"input_tokens": 30, "output_tokens": 0, "total_tokens": 30})
meter.record(False, None)
result = UsageMeter(path).snapshot()
assert result["requests"] == 3 and result["successful_requests"] == 2
assert result["failed_requests"] == 1 and result["unreported_requests"] == 1
assert (
result["input_tokens"],
result["output_tokens"],
result["total_tokens"],
) == (130, 2, 132)
assert stat.S_IMODE(path.stat().st_mode) == 0o600
assert set(json.loads(path.read_text())) == set(COST_COUNTERS) | {
"since",
"requests",
"successful_requests",
"failed_requests",
"usage_reported_requests",
"input_tokens",
"output_tokens",
"total_tokens",
}
def test_missing_or_invalid_usage_is_not_mistaken_for_zero_report(tmp_path):
m = UsageMeter(tmp_path / "usage.json")
for value in [
None,
{},
{"input_tokens": True, "output_tokens": 0},
{"input_tokens": -1, "output_tokens": 0},
{"input_tokens": 1, "output_tokens": 2, "total_tokens": 1},
]:
m.record(True, value)
assert m.snapshot()["usage_reported_requests"] == 0
assert m.snapshot()["unreported_requests"] == 5
def test_usage_counts_paid_late_reply_even_after_end(tmp_path):
entered, release = threading.Event(), threading.Event()
def handler(_):
entered.set()
assert release.wait(3)
return httpx.Response(
200,
json=decision()
| {"usage": {"input_tokens": 42, "output_tokens": 0, "total_tokens": 42}},
)
api = DirectApi(
httpx.MockTransport(handler), Clock(), UsageMeter(tmp_path / "usage.json")
)
s = Settings(api_key="secret")
sid = api.start(s)["session_id"]
with ThreadPoolExecutor(1) as pool:
pending = pool.submit(api.frame, s, sid, "one", b"private-image")
assert entered.wait(2)
api.control(s, sid, "end")
release.set()
assert not pending.result(timeout=2)["notify"]
assert api.usage.snapshot()["requests"] == 1
assert api.usage.snapshot()["total_tokens"] == 42
assert "secret" not in (tmp_path / "usage.json").read_text()
assert "private-image" not in (tmp_path / "usage.json").read_text()
def test_failures_count_and_local_rejections_do_not():
api = DirectApi(httpx.MockTransport(lambda _: httpx.Response(401)))
s = Settings(api_key="test")
sid = api.start(s)["session_id"]
for settings, image in [(s, b"jpeg"), (Settings(), b"jpeg"), (s, b"x" * 1048577)]:
try:
api.frame(settings, sid, "one", image)
except ApiError:
pass
assert api.usage.snapshot()["requests"] == 1
assert api.usage.snapshot()["failed_requests"] == 1
def test_damaged_or_unwritable_usage_file_does_not_stop_app(tmp_path):
path = tmp_path / "usage.json"
path.write_text("invalid")
m = UsageMeter(path)
m.record(False, None)
assert m.snapshot()["storage_error"]
assert path.read_text() == "invalid"
blocked = tmp_path / "file"
blocked.write_text("blocked")
m = UsageMeter(blocked / "usage.json")
m.record(True, None)
assert m.snapshot()["requests"] == 1 and m.snapshot()["storage_error"]
def test_decisions_cost_cache_breakdown_output_free_and_restart(tmp_path):
import pytest
path = tmp_path / "usage.json"
m = UsageMeter(path)
m.record(
True,
{
"input_tokens": 10000,
"output_tokens": 123,
"input_tokens_details": {"cached_tokens": 2000, "cache_write_tokens": 3000},
},
)
# 5k uncached * $0.10/M + 2k cache reads * $0.01/M + 3k writes * $0.125/M.
assert m.snapshot()["estimated_cost_usd"] == pytest.approx(0.000895)
m.record(
False,
{
"input_tokens": 1000,
"output_tokens": 0,
"input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0},
},
)
result = UsageMeter(path).snapshot()
assert result["estimated_cost_usd"] == pytest.approx(0.000995)
assert result["cached_input_tokens"] == 2000
assert result["cache_write_input_tokens"] == 3000
assert result["estimated_without_cache_tokens"] == 0
def test_legacy_totals_are_migrated_without_resetting_counts(tmp_path):
path = tmp_path / "usage.json"
original = {
"since": "2026-11-05T00:00:00+00:00",
"requests": 215,
"successful_requests": 179,
"failed_requests": 36,
"usage_reported_requests": 185,
"input_tokens": 212915,
"output_tokens": 0,
"total_tokens": 212915,
}
path.write_text(json.dumps(original))
m = UsageMeter(path)
s = m.snapshot()
assert s["estimated_cost_usd"] == 0.0212915
assert s["estimated_without_cache_tokens"] == 212915
assert s["unreported_requests"] == 30
assert not s["storage_error"]
m.record(True, {"input_tokens": 10, "output_tokens": 0})
s = UsageMeter(path).snapshot()
assert s["requests"] == 216 and s["since"] == original["since"]
assert s["estimated_without_cache_tokens"] == 212925
assert s["estimated_cost_usd"] == 0.0212925
def test_cost_invalid_cache_details_fall_back_and_unknown_usage_is_not_free():
m = UsageMeter()
m.record(
True,
{
"input_tokens": 100,
"output_tokens": 0,
"input_tokens_details": {"cached_tokens": 100, "cache_write_tokens": 100},
},
)
m.record(False, None)
assert m.snapshot()["estimated_cost_usd"] == 0.00001
assert m.snapshot()["estimated_without_cache_tokens"] == 100
assert m.snapshot()["unreported_requests"] == 1