test(pricing): pin rate validation to every billable rate, not the token pair

The write-edge validator and the served-catalog backstop both iterate the full
billable-rate tuple, but every test drove them through `prompt` alone — the
tuple could be narrowed to the two token rates and the suite stayed green,
while a malformed image/search/reasoning/cache rate walked in.

Covers each remaining rate at both surfaces. `request` is deliberately absent
from the catalog list and pinned by its own test instead: the row-to-model
conversion clamps a negative stored `request` to zero before the price is
built, so the backstop never sees one.

Also covers the exchange-quote reader's widened error handling, which had no
test: a body that never yields JSON raises before any number is read, and
unhandled it abandoned the whole aggregation with two healthy quotes in hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011cKHVF5LA7TR5QuYi6ErLM
This commit is contained in:
Jeroen Ubbink
2026-08-25 20:28:16 +02:00
co-authored by Claude Opus 5
parent 86be12f4be
commit b68086c592
3 changed files with 185 additions and 3 deletions
@@ -308,3 +308,85 @@ async def test_admin_model_listing_shows_a_non_finite_stored_rate(
assert resp.status_code == 200
listed = {m["id"]: m for m in resp.json()["db_models"]}
assert listed["inf-rate"]["pricing"]["prompt"] == "inf"
@pytest.mark.integration
@pytest.mark.asyncio
async def test_malformed_auxiliary_rate_is_rejected(
integration_client: AsyncClient, integration_session: AsyncSession
) -> None:
"""Validation spans every billable rate, not just the token rates.
``prompt``/``completion`` are the rates most prices are built from, but the
request, image, search, reasoning and cache rates are billed too. A negative
or non-finite value in any of them is the same defect and must be answered
the same way.
"""
provider_id = await _make_provider(integration_session)
for field, bad in (
("request", -1.0),
("image", -0.5),
("web_search", float("inf")),
("internal_reasoning", float("nan")),
("input_cache_read", -1e-06),
("input_cache_write", float("-inf")),
("completion", -1.0),
):
resp = await integration_client.post(
f"/admin/api/upstream-providers/{provider_id}/models",
headers=_admin_headers(),
json=_payload(
provider_id, model_id="aux-rate", pricing=_pricing(**{field: bad})
),
)
assert resp.status_code == 422, field
assert (
await integration_session.get(ModelRow, ("aux-rate", provider_id)) is None
), field
@pytest.mark.integration
@pytest.mark.asyncio
async def test_admin_single_model_shows_a_non_finite_stored_rate(
integration_client: AsyncClient, integration_session: AsyncSession
) -> None:
"""The single-model view answers like the listing it is opened from.
It is the other view of a row the served-catalog backstop holds back, so it
has the same duty to name the rate that needs fixing rather than rendering
it as ``null``.
"""
provider_id = await _make_provider(integration_session)
integration_session.add(
ModelRow(
id="inf-one",
name="inf-one",
description="d",
created=0,
context_length=8192,
architecture=json.dumps(
{
"modality": "text",
"input_modalities": ["text"],
"output_modalities": ["text"],
"tokenizer": "unknown",
"instruct_type": None,
}
),
pricing=json.dumps({"prompt": float("inf"), "completion": 2e-06}),
upstream_provider_id=provider_id,
enabled=True,
forwarded_model_id="inf-one",
)
)
await integration_session.commit()
resp = await integration_client.get(
f"/admin/api/upstream-providers/{provider_id}/models/inf-one",
headers=_admin_headers(),
)
assert resp.status_code == 200
assert resp.json()["pricing"]["prompt"] == "inf"
@@ -187,3 +187,71 @@ async def test_one_unreadable_stored_price_does_not_blank_the_catalog(
served = {m.id for m in await list_models(integration_session, provider_id)}
assert served == {"good"}
@pytest.mark.integration
@pytest.mark.asyncio
@pytest.mark.parametrize(
"field",
[
"image",
"web_search",
"internal_reasoning",
"input_cache_read",
"input_cache_write",
],
)
async def test_served_catalog_excludes_a_malformed_auxiliary_rate(
integration_session: AsyncSession, field: str
) -> None:
"""The backstop covers every billable rate, not only the token rates.
A price whose ``prompt``/``completion`` are sound can still carry a
malformed request, image, search, reasoning or cache rate — the catalog
import filter never inspects those — and the request that hits one is billed
against it just the same.
"""
provider_id = await _make_provider(integration_session)
await _insert_row(
integration_session,
provider_id,
model_id="good",
pricing={"prompt": 1e-06, "completion": 2e-06},
)
await _insert_row(
integration_session,
provider_id,
model_id="bad-aux",
pricing={"prompt": 1e-06, "completion": 2e-06, field: -1.0},
)
served = {m.id for m in await list_models(integration_session, provider_id)}
assert served == {"good"}
@pytest.mark.integration
@pytest.mark.asyncio
async def test_a_negative_request_rate_is_clamped_on_read_and_still_served(
integration_session: AsyncSession,
) -> None:
"""``request`` is the one billable rate the row-to-model conversion repairs.
It clamps a negative stored ``request`` to zero before the price is built,
so the backstop never sees one and the row is served at a zero request rate
— money-safe, and the reason ``request`` is absent from the list of rates
above. Pinned here so that if the clamp goes, this rate joins that list
rather than quietly becoming the one unguarded field.
"""
provider_id = await _make_provider(integration_session)
await _insert_row(
integration_session,
provider_id,
model_id="neg-request",
pricing={"prompt": 1e-06, "completion": 2e-06, "request": -1.0},
)
served = await list_models(integration_session, provider_id)
assert [m.id for m in served] == ["neg-request"]
assert served[0].pricing.request == 0.0
+35 -3
View File
@@ -189,6 +189,12 @@ class _ExchangeResponse:
self._payload = payload
def json(self) -> dict[str, Any]:
# A quote given as an exception stands for a response body that never
# produced one: an exchange answering with an HTML error page raises
# out of `.json()` before any price is read.
quote = next(iter(self._payload.values()))
if isinstance(quote, BaseException):
raise quote
return self._payload
@@ -200,9 +206,10 @@ class _ExchangeClient:
async def get(self, url: str) -> _ExchangeResponse:
if "kraken" in url:
return _ExchangeResponse(
{"result": {"XXBTZUSD": {"c": [self._quotes["kraken"]]}}}
)
quote = self._quotes["kraken"]
if isinstance(quote, BaseException):
return _ExchangeResponse({"error": quote})
return _ExchangeResponse({"result": {"XXBTZUSD": {"c": [quote]}}})
if "coinbase" in url:
return _ExchangeResponse({"data": {"amount": self._quotes["coinbase"]}})
return _ExchangeResponse({"price": self._quotes["binance"]})
@@ -297,6 +304,31 @@ async def test_boolean_exchange_quote_does_not_set_the_node_price(
assert btc_usd_price() == pytest.approx(100000.0)
@pytest.mark.asyncio
async def test_an_unreadable_exchange_response_drops_only_that_quote(
refresh_price_with: Any,
) -> None:
"""An exchange whose response never yields a quote costs one quote.
The price is aggregated across three exchanges precisely so that one of them
having a bad day is survivable. A body that is not JSON, or whose shape moved
under the reader, raises before any number is seen; unhandled, it aborted the
whole aggregation and left the node on a stale rate even though two healthy
quotes were already in hand.
"""
from routstr.payment.price import btc_usd_price
await refresh_price_with(
{
"kraken": ValueError("Expecting value: line 1 column 1 (char 0)"),
"coinbase": "100000.0",
"binance": "100000.0",
}
)
assert btc_usd_price() == pytest.approx(100000.0)
@pytest.mark.asyncio
async def test_all_quotes_unusable_keeps_the_last_good_price(
refresh_price_with: Any,