From 8a0547a8a07f0d4b5faed5625ee441d789579273 Mon Sep 17 00:00:00 2001 From: Jeroen Ubbink Date: Tue, 25 Aug 2026 15:42:15 +0200 Subject: [PATCH] feat(pricing): add is_usable_rate, one definition of a billable rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A rate is usable only when it is finite and non-negative. Zero is usable — "free" is a real price — but NaN, ±inf and negatives are not prices at all. Python's truthiness cannot answer this question: inf and NaN are both truthy, and NaN > 0, NaN < 0 and NaN == 0 are all False, so a malformed rate passes every `if rate:` and every comparison-based guard. Stating the test once means every caller answers it identically. Co-Authored-By: Claude Opus 5 --- routstr/payment/models.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/routstr/payment/models.py b/routstr/payment/models.py index 3deedb5a..3cd4de83 100644 --- a/routstr/payment/models.py +++ b/routstr/payment/models.py @@ -1,5 +1,6 @@ import asyncio import json +import math import random import httpx @@ -58,6 +59,29 @@ class Pricing(BaseModel): max_cost: float = 0.0 # in sats not msats +def is_usable_rate(rate: float) -> bool: + """True if a single billable rate is a number a request could be billed on. + + The one definition of a usable rate, so every guard that asks the question + answers it identically. A rate qualifies only when it is finite and + non-negative; zero is usable (it means "free", which is a real price) but + ``NaN``, ``±inf`` and negatives are not prices at all. + + Non-finite: ``inf > 0`` is True, so an infinite rate reads as chargeable and + would be served, routed and billed as ``inf``; ``NaN`` poisons every total it + enters and defeats ordinary comparisons, since ``NaN > 0``, ``NaN < 0`` and + ``NaN == 0`` are all False. Negative: a negative rate produces a negative + cost, which the settlement path subtracts from the balance — it pays the + caller to make requests. + + Both reach a stored row from upstream catalogs as well as the admin edge + (``json.loads`` accepts the bare ``NaN``/``Infinity`` literals and overflows + ``1e999`` to ``inf``), so the check belongs in one shared place rather than + at each writer. + """ + return math.isfinite(rate) and rate >= 0.0 + + class TopProvider(BaseModel): context_length: int | None = None max_completion_tokens: int | None = None