diff --git a/src/copilot_usage/models.py b/src/copilot_usage/models.py index 8ff71044..d8d20f9d 100644 --- a/src/copilot_usage/models.py +++ b/src/copilot_usage/models.py @@ -6,6 +6,7 @@ """ import builtins +import math from datetime import UTC, datetime from enum import StrEnum from pathlib import Path @@ -220,6 +221,7 @@ def parse_token_int(raw: object) -> int | None: Rules: - ``bool`` / ``str`` → ``None`` (invalid, not coerced) + - IEEE 754 special ``float`` (``inf``, ``-inf``, ``nan``) → ``None`` - non-whole ``float`` → ``None`` - zero or negative ``int`` / ``float`` → ``None`` - positive whole-number ``float`` → coerced to ``int`` @@ -229,6 +231,8 @@ def parse_token_int(raw: object) -> int | None: if isinstance(raw, (bool, str)): return None if isinstance(raw, float): + if not math.isfinite(raw): + return None if not raw.is_integer(): return None tokens = int(raw) diff --git a/tests/copilot_usage/test_parser.py b/tests/copilot_usage/test_parser.py index 95a3a91f..76568f72 100644 --- a/tests/copilot_usage/test_parser.py +++ b/tests/copilot_usage/test_parser.py @@ -4948,6 +4948,11 @@ def test_returns_none_for_missing_key(self) -> None: ) assert _extract_output_tokens(ev) is None + @pytest.mark.parametrize("special", [float("inf"), float("nan"), float("-inf")]) + def test_returns_none_for_ieee_special_floats(self, special: float) -> None: + """IEEE 754 special floats (inf, nan, -inf) are not valid token counts.""" + assert _extract_output_tokens(_make_assistant_event(special)) is None + _EXTRACT_TOKENS_CASES: list[tuple[str, object, int | None]] = [ ("valid_int", 42, 42), @@ -5021,6 +5026,9 @@ def test_missing_key_no_pydantic(self) -> None: ("large_positive_int", 1234), ("whole_float", 1234.0), ("fractional_float", 3.14), + ("float_inf", float("inf")), + ("float_neg_inf", float("-inf")), + ("float_nan", float("nan")), ]