test(integration): add model price propagation regression tests

Three tests against the path POST admin models -> GET /v1/models: price edit,
fee adjustment, and disable propagation. Each reaches the served model by id
rather than iterating the catalogue, so an empty catalogue fails them instead
of skipping them.
This commit is contained in:
Jeroen Ubbink
2026-09-01 15:17:13 +02:00
parent 1715ff38fe
commit 109b3c9a0b
@@ -0,0 +1,221 @@
"""Cover that an admin price write reaches the served catalogue (GET /v1/models).
A price edit changes the served price; the served price is fee-adjusted while the
admin read-back is raw; a disabled model leaves the catalogue but keeps its row.
Each test reaches the served model by id rather than iterating ``data["data"]``,
so an empty catalogue fails these tests instead of skipping them.
"""
from __future__ import annotations
from collections.abc import Iterator
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
import pytest
from httpx import AsyncClient
from sqlmodel.ext.asyncio.session import AsyncSession
from routstr.core.admin import admin_sessions
from routstr.core.db import ModelRow, UpstreamProviderRow
from routstr.proxy import reinitialize_upstreams
# The conftest patches ``routstr.payment.price.sats_usd_price``, but
# ``models.py`` imports it as ``from .price import sats_usd_price`` — a
# local binding the conftest-level patch cannot reach. Pin it here so
# every test that goes through ``_row_to_model`` gets a real sats price.
@pytest.fixture(autouse=True)
def _pin_sats_usd() -> Iterator[None]:
with patch("routstr.payment.models.sats_usd_price", return_value=0.0005):
yield
def _admin_headers() -> dict[str, str]:
token = "test-propagation-token"
admin_sessions[token] = int(
(datetime.now(timezone.utc) + timedelta(minutes=5)).timestamp()
)
return {"Authorization": f"Bearer {token}"}
def _model_payload(prompt: float, provider_id: int, enabled: bool = True) -> dict:
return {
"id": "propagation-test-model",
"name": "Propagation Test Model",
"description": "model used to verify price propagation",
"created": 0,
"context_length": 128000,
"architecture": {
"modality": "text",
"input_modalities": ["text"],
"output_modalities": ["text"],
"tokenizer": "unknown",
"instruct_type": None,
},
"pricing": {
"prompt": prompt,
"completion": prompt * 2,
"input_cache_read": 0.0,
"input_cache_write": 0.0,
"request": 0.0,
"image": 0.0,
"web_search": 0.0,
"internal_reasoning": 0.0,
},
"per_request_limits": None,
"top_provider": None,
"upstream_provider_id": provider_id,
"canonical_slug": None,
"alias_ids": [],
"enabled": enabled,
"forwarded_model_id": "propagation-test-model",
}
async def _seed_provider(session: AsyncSession, *, fee: float = 1.0) -> int:
"""Insert a provider, refresh the upstream map, and return its primary key."""
provider = UpstreamProviderRow(
provider_type="generic",
base_url="https://propagation-test.example/v1",
api_key="test-key",
provider_fee=fee,
)
session.add(provider)
await session.commit()
await session.refresh(provider)
await reinitialize_upstreams()
assert provider.id is not None
return provider.id
@pytest.mark.integration
@pytest.mark.asyncio
async def test_price_edit_propagates_to_served_catalogue(
integration_client: AsyncClient,
integration_session: AsyncSession,
) -> None:
"""A price edit through the admin API must change the served /v1/models price."""
provider_id = await _seed_provider(integration_session)
headers = _admin_headers()
r = await integration_client.post(
f"/admin/api/upstream-providers/{provider_id}/models",
headers=headers,
json=_model_payload(prompt=1.0e-7, provider_id=provider_id),
)
assert r.status_code == 200
# -- record the served price before edit -----------------------------------
public = await integration_client.get("/v1/models")
assert public.status_code == 200
public_data = public.json()
assert len(public_data["data"]) > 0, "catalogue must not be empty"
served_before = {
m["id"]: m.get("pricing", {}).get("prompt") for m in public_data["data"]
}
assert "propagation-test-model" in served_before
before = served_before["propagation-test-model"]
# -- edit the price and re-check -------------------------------------------
r = await integration_client.post(
f"/admin/api/upstream-providers/{provider_id}/models",
headers=headers,
json=_model_payload(prompt=5.0e-7, provider_id=provider_id),
)
assert r.status_code == 200
public = await integration_client.get("/v1/models")
assert public.status_code == 200
served_after = {
m["id"]: m.get("pricing", {}).get("prompt") for m in public.json()["data"]
}
after = served_after["propagation-test-model"]
assert before != after, "served price did not change after admin edit"
# With provider_fee=1.0 the served price equals the stored raw price.
assert after == pytest.approx(5.0e-7)
@pytest.mark.integration
@pytest.mark.asyncio
async def test_admin_readback_is_raw_served_is_fee_adjusted(
integration_client: AsyncClient,
integration_session: AsyncSession,
) -> None:
"""Admin read-back returns the raw price; /v1/models returns the fee-adjusted one."""
provider_id = await _seed_provider(integration_session, fee=1.05)
headers = _admin_headers()
model_payload = _model_payload(prompt=1.0e-7, provider_id=provider_id)
r = await integration_client.post(
f"/admin/api/upstream-providers/{provider_id}/models",
headers=headers,
json=model_payload,
)
assert r.status_code == 200
# Admin read-back: apply_provider_fee=False
admin_r = await integration_client.get(
f"/admin/api/upstream-providers/{provider_id}/models/propagation-test-model",
headers=headers,
)
assert admin_r.status_code == 200
admin_body = admin_r.json()
raw_prompt = admin_body["pricing"]["prompt"]
assert raw_prompt == pytest.approx(1.0e-7)
# Public /v1/models: fee-adjusted
public = await integration_client.get("/v1/models")
assert public.status_code == 200
served = {
m["id"]: m.get("pricing", {}).get("prompt") for m in public.json()["data"]
}
assert served["propagation-test-model"] == pytest.approx(1.0e-7 * 1.05)
@pytest.mark.integration
@pytest.mark.asyncio
async def test_disabled_model_not_served(
integration_client: AsyncClient,
integration_session: AsyncSession,
) -> None:
"""A disabled model must be absent from /v1/models but still present in the DB."""
provider_id = await _seed_provider(integration_session)
headers = _admin_headers()
r = await integration_client.post(
f"/admin/api/upstream-providers/{provider_id}/models",
headers=headers,
json=_model_payload(prompt=1.0e-7, provider_id=provider_id, enabled=True),
)
assert r.status_code == 200
# Confirm it appears in the public catalogue.
public = await integration_client.get("/v1/models")
served_ids = {m["id"] for m in public.json()["data"]}
assert "propagation-test-model" in served_ids
# -- disable via upsert ----------------------------------------------------
r = await integration_client.post(
f"/admin/api/upstream-providers/{provider_id}/models",
headers=headers,
json=_model_payload(prompt=1.0e-7, provider_id=provider_id, enabled=False),
)
assert r.status_code == 200
# Public catalogue must no longer list it.
public = await integration_client.get("/v1/models")
served_ids = {m["id"] for m in public.json()["data"]}
assert "propagation-test-model" not in served_ids
# DB row must still exist.
row = await integration_session.get(
ModelRow, ("propagation-test-model", provider_id)
)
assert row is not None
assert row.enabled is False