mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-25 06:39:30 +00:00
fix(model-usage): coerce numeric-string costs and ignore non-finite values (#87861)
Merged via squash.
Prepared head SHA: 11bb5719ca
Co-authored-by: coder999999999 <83845889+coder999999999@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user