diff --git a/skills/model-usage/scripts/model_usage.py b/skills/model-usage/scripts/model_usage.py index b78fb980155..ba6851735c9 100644 --- a/skills/model-usage/scripts/model_usage.py +++ b/skills/model-usage/scripts/model_usage.py @@ -9,6 +9,7 @@ from __future__ import annotations import argparse import json +import math import subprocess import sys from dataclasses import dataclass @@ -107,6 +108,30 @@ def filter_by_days(entries: List[Dict[str, Any]], days: Optional[int]) -> List[D return filtered +def coerce_finite_cost(value: Any) -> Optional[float]: + """Coerce a cost field to a finite float, or None if it is not usable. + + Accepts native numbers and numeric strings (for example "1.75"), since cost + payloads sometimes serialize numbers as strings. Rejects booleans (they are + ints in Python but never a valid cost) and non-finite values (NaN/Infinity), + which would otherwise silently corrupt aggregated totals. + """ + if isinstance(value, bool): + return None + if isinstance(value, (int, float)): + number = float(value) + elif isinstance(value, str): + try: + number = float(value.strip()) + except ValueError: + return None + else: + return None + if not math.isfinite(number): + return None + return number + + def aggregate_costs(entries: Iterable[Dict[str, Any]]) -> Dict[str, float]: totals: Dict[str, float] = {} for entry in entries: @@ -119,12 +144,12 @@ def aggregate_costs(entries: Iterable[Dict[str, Any]]) -> Dict[str, float]: if not isinstance(item, dict): continue model = item.get("modelName") - cost = item.get("cost") if not isinstance(model, str): continue - if not isinstance(cost, (int, float)): + cost = coerce_finite_cost(item.get("cost")) + if cost is None: continue - totals[model] = totals.get(model, 0.0) + float(cost) + totals[model] = totals.get(model, 0.0) + cost return totals @@ -143,9 +168,9 @@ def pick_current_model(entries: List[Dict[str, Any]]) -> Tuple[Optional[str], Op if not isinstance(item, dict): continue model = item.get("modelName") - cost = item.get("cost") - if isinstance(model, str) and isinstance(cost, (int, float)): - scored.append(ModelCost(model=model, cost=float(cost))) + cost = coerce_finite_cost(item.get("cost")) + if isinstance(model, str) and cost is not None: + scored.append(ModelCost(model=model, cost=cost)) if scored: scored.sort(key=lambda item: item.cost, reverse=True) return scored[0].model, entry.get("date") if isinstance(entry.get("date"), str) else None @@ -178,9 +203,9 @@ def latest_day_cost(entries: List[Dict[str, Any]], model: str) -> Tuple[Optional if not isinstance(item, dict): continue if item.get("modelName") == model: - cost = item.get("cost") if isinstance(item.get("cost"), (int, float)) else None + cost = coerce_finite_cost(item.get("cost")) day = entry.get("date") if isinstance(entry.get("date"), str) else None - return day, float(cost) if cost is not None else None + return day, cost return None, None diff --git a/skills/model-usage/scripts/test_model_usage.py b/skills/model-usage/scripts/test_model_usage.py index 4d5273401de..9d63466b162 100644 --- a/skills/model-usage/scripts/test_model_usage.py +++ b/skills/model-usage/scripts/test_model_usage.py @@ -7,7 +7,14 @@ import argparse from datetime import date, timedelta from unittest import TestCase, main -from model_usage import filter_by_days, positive_int +from model_usage import ( + aggregate_costs, + coerce_finite_cost, + filter_by_days, + latest_day_cost, + pick_current_model, + positive_int, +) class TestModelUsage(TestCase): @@ -35,6 +42,111 @@ class TestModelUsage(TestCase): self.assertEqual(filtered[0]["date"], (today - timedelta(days=1)).strftime("%Y-%m-%d")) self.assertEqual(filtered[1]["date"], today.strftime("%Y-%m-%d")) + def test_coerce_finite_cost_accepts_numbers_and_numeric_strings(self): + self.assertEqual(coerce_finite_cost(2), 2.0) + self.assertEqual(coerce_finite_cost(1.75), 1.75) + self.assertEqual(coerce_finite_cost("1.75"), 1.75) + self.assertEqual(coerce_finite_cost(" 2.5 "), 2.5) + + def test_coerce_finite_cost_rejects_booleans(self): + # bool is a subclass of int in Python, but is never a valid cost. + self.assertIsNone(coerce_finite_cost(True)) + self.assertIsNone(coerce_finite_cost(False)) + + def test_coerce_finite_cost_rejects_non_finite(self): + self.assertIsNone(coerce_finite_cost(float("nan"))) + self.assertIsNone(coerce_finite_cost(float("inf"))) + self.assertIsNone(coerce_finite_cost(float("-inf"))) + self.assertIsNone(coerce_finite_cost("NaN")) + self.assertIsNone(coerce_finite_cost("Infinity")) + + def test_coerce_finite_cost_rejects_unusable_values(self): + self.assertIsNone(coerce_finite_cost("not-a-number")) + self.assertIsNone(coerce_finite_cost("")) + self.assertIsNone(coerce_finite_cost(None)) + self.assertIsNone(coerce_finite_cost({})) + + def test_aggregate_costs_includes_numeric_strings(self): + entries = [ + { + "date": "2026-05-25", + "modelBreakdowns": [ + {"modelName": "claude-sonnet-4-6", "cost": 1.50}, + {"modelName": "claude-sonnet-4-6", "cost": "1.75"}, + ], + } + ] + self.assertEqual(aggregate_costs(entries), {"claude-sonnet-4-6": 3.25}) + + def test_aggregate_costs_ignores_bool_and_non_finite(self): + entries = [ + { + "date": "2026-05-25", + "modelBreakdowns": [ + {"modelName": "claude-sonnet-4-6", "cost": 1.50}, + {"modelName": "claude-sonnet-4-6", "cost": "1.75"}, + {"modelName": "claude-sonnet-4-6", "cost": True}, + {"modelName": "claude-sonnet-4-6", "cost": float("nan")}, + {"modelName": "claude-sonnet-4-6", "cost": float("inf")}, + ], + } + ] + totals = aggregate_costs(entries) + # NaN/Infinity must not poison the total; bool must not add 1.0. + self.assertEqual(totals, {"claude-sonnet-4-6": 3.25}) + + def test_pick_current_model_scores_numeric_string_costs(self): + # model-b's cost is a numeric string; it must still win on highest cost. + entries = [ + { + "date": "2026-05-25", + "modelBreakdowns": [ + {"modelName": "model-a", "cost": 1.0}, + {"modelName": "model-b", "cost": "5.0"}, + ], + } + ] + model, day = pick_current_model(entries) + self.assertEqual(model, "model-b") + self.assertEqual(day, "2026-05-25") + + def test_pick_current_model_ignores_bool_and_non_finite(self): + # Only model-a has a usable cost; bool and NaN must not be scored. + entries = [ + { + "date": "2026-05-25", + "modelBreakdowns": [ + {"modelName": "model-a", "cost": 2.0}, + {"modelName": "model-b", "cost": True}, + {"modelName": "model-c", "cost": float("nan")}, + ], + } + ] + model, _day = pick_current_model(entries) + self.assertEqual(model, "model-a") + + def test_latest_day_cost_accepts_numeric_string(self): + entries = [ + { + "date": "2026-05-25", + "modelBreakdowns": [{"modelName": "model-a", "cost": "2.50"}], + } + ] + day, cost = latest_day_cost(entries, "model-a") + self.assertEqual(day, "2026-05-25") + self.assertEqual(cost, 2.50) + + def test_latest_day_cost_rejects_non_finite(self): + entries = [ + { + "date": "2026-05-25", + "modelBreakdowns": [{"modelName": "model-a", "cost": float("inf")}], + } + ] + day, cost = latest_day_cost(entries, "model-a") + self.assertEqual(day, "2026-05-25") + self.assertIsNone(cost) + if __name__ == "__main__": main()