Merge branch 'main' into feat/upstream-certification-harness

This commit is contained in:
9qeklajc
2026-10-02 20:09:53 +02:00
2 changed files with 95 additions and 4 deletions
+13 -4
View File
@@ -38,6 +38,8 @@ names do not collide, so a single union parser is safe; a vendor whose fields
would genuinely conflict needs a dedicated branch here.
"""
import math
from pydantic.v1 import BaseModel
@@ -51,18 +53,25 @@ class NormalizedUsage(BaseModel):
def parse_token_count(value: object) -> int:
"""Parse a token count from various formats (int, float, str, bool)."""
"""Parse a token count from various formats (int, float, str, bool).
``json.loads`` accepts bare ``Infinity``/``NaN`` and overflows ``1e999`` to
``inf``, so an upstream can put them on the wire. ``int()`` raises on both,
which would turn a billing path into a 500; reject them like
``is_usable_rate`` does instead.
"""
if isinstance(value, bool):
return 0
if isinstance(value, int):
return max(0, value)
if isinstance(value, float):
return max(0, int(value))
return max(0, int(value)) if math.isfinite(value) else 0
if isinstance(value, str):
try:
return max(0, int(float(value)))
except ValueError:
parsed = float(value)
except (ValueError, OverflowError):
return 0
return max(0, int(parsed)) if math.isfinite(parsed) else 0
return 0
@@ -0,0 +1,82 @@
"""Non-finite token counts must not crash the billing path.
``json.loads`` accepts bare ``Infinity``/``NaN`` and overflows ``1e999`` to
``inf``, so an upstream can put them on the wire. ``int()`` raises on both.
"""
import json
import os
from typing import Any
from unittest.mock import patch
import pytest
os.environ.setdefault("UPSTREAM_BASE_URL", "http://test")
os.environ.setdefault("UPSTREAM_API_KEY", "test")
os.environ.setdefault("LIGHTNING_ADDRESS", "test@stm.to")
from routstr.core.settings import settings
from routstr.payment.cost_calculation import CostData, calculate_cost
from routstr.payment.usage import parse_token_count
NON_FINITE = [
float("inf"),
float("-inf"),
float("nan"),
1e999,
"Infinity",
"NaN",
"-Infinity",
"1e999",
]
@pytest.fixture(autouse=True)
def _fixed_pricing(monkeypatch: pytest.MonkeyPatch) -> Any:
monkeypatch.setattr(settings, "fixed_pricing", True)
monkeypatch.setattr(settings, "fixed_per_1k_input_tokens", 0.001)
monkeypatch.setattr(settings, "fixed_per_1k_output_tokens", 0.001)
with patch("routstr.payment.cost_calculation.sats_usd_price", return_value=5.0e-5):
yield
@pytest.mark.parametrize("value", NON_FINITE)
def test_parse_token_count_rejects_non_finite(value: Any) -> None:
assert parse_token_count(value) == 0
def test_parse_token_count_still_parses_ordinary_values() -> None:
assert parse_token_count(42) == 42
assert parse_token_count("42") == 42
assert parse_token_count(42.9) == 42
assert parse_token_count("42.9") == 42
assert parse_token_count(True) == 0
assert parse_token_count(-5) == 0
assert parse_token_count("not a number") == 0
assert parse_token_count(None) == 0
@pytest.mark.asyncio
@pytest.mark.parametrize(
"raw",
[
'{"usage": {"prompt_tokens": Infinity, "completion_tokens": 10}}',
'{"usage": {"prompt_tokens": 1e999, "completion_tokens": 10}}',
'{"usage": {"prompt_tokens": 1000, "completion_tokens": NaN}}',
'{"usage": {"prompt_tokens": "Infinity", "completion_tokens": "10"}}',
],
)
async def test_billing_settles_a_response_with_non_finite_usage(raw: str) -> None:
"""The billing entry point parses the decoded wire body without raising
and bills only the finite component."""
response = json.loads(raw)
result = await calculate_cost(response, max_cost=100_000)
assert isinstance(result, CostData)
usage = response["usage"]
finite_input = parse_token_count(usage["prompt_tokens"])
finite_output = parse_token_count(usage["completion_tokens"])
assert result.input_tokens == finite_input
assert result.output_tokens == finite_output
assert 0 <= result.total_msats <= 100_000