Files
routstr-core/routstr/payment/rates.py
Jeroen UbbinkandClaude Opus 5 dff164b980 fix(pricing): a rate spelled as a boolean is not a rate
`isinstance(True, int)` is True and `float(True)` is `1.0`, so a JSON `true`
in a catalog was a finite, positive rate that passed every numeric guard: a
dollar per token. It reached a stored price through the OpenRouter feed filter,
the LiteLLM cost-map rung and the OpenRouter resolver rung.

The write edge and the exchange feed already rejected booleans explicitly,
which is the shape of the real problem: four readers were each parsing a rate
for themselves and disagreeing about what a rate is. Give them one coercion —
`coerce_rate` — and let it answer for all of them.

A numeric string stays a rate, because feeds report prices as strings; that now
holds on the LiteLLM rung too, which previously required a float outright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014X8RZzzbAuQCbavhFjTvJ4
2026-08-26 15:44:29 +02:00

72 lines
3.2 KiB
Python

"""The one definition of a billable rate, with no dependencies of its own.
A rate reaches the node from an upstream catalog, the LiteLLM cost map, an
operator's admin edit, a legacy database row and the BTC/USD feed. Each of those
readers needs the same two questions answered — is this value a rate at all, and
is it a rate a request can be billed on — so both answers live here, in a module
that imports nothing from the package. Every guard then shares one definition
instead of drifting, and no caller needs a deferred import to reach it.
"""
import math
# The rates a request can bill on. Derived fields (``max_*_cost``) are excluded
# — they are computed carriers, not charged rates. One definition, shared by the
# admin write edge and the served/routed guards, so they all cover the same set.
BILLABLE_PRICING_FIELDS = (
"prompt",
"completion",
"request",
"image",
"web_search",
"internal_reasoning",
"input_cache_read",
"input_cache_write",
)
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``).
This is the rationale for every guard that calls it; the call sites say what
they do with the answer, not why the answer matters.
"""
return math.isfinite(rate) and rate >= 0.0
def coerce_rate(value: object) -> float | None:
"""Coerce a value from outside the node to a usable rate, or ``None``.
The one coercion, shared by every reader of a rate the node did not compute
itself: an upstream catalog, the LiteLLM cost map, the exchange feed and the
admin write edge. Each of them was parsing for itself, and they disagreed —
which is how a boolean became a price on some paths and not others.
A boolean is rejected outright: it is a change of shape, not a rate, and
Python would make ``True`` a finite, positive ``1.0`` that passes every
numeric guard downstream — a dollar per token. A numeric string is accepted,
because feeds report prices as strings. An oversized integer raises
``OverflowError`` rather than ``ValueError``, so that is caught too.
"""
if isinstance(value, bool) or not isinstance(value, (int, float, str)):
return None
try:
rate = float(value)
except (TypeError, ValueError, OverflowError):
return None
return rate if is_usable_rate(rate) else None