From 9f04525c826f2bc37b65efe5dc7f405a8cfe0a84 Mon Sep 17 00:00:00 2001 From: Jeroen Ubbink Date: Sun, 5 Jul 2026 15:39:03 +0200 Subject: [PATCH] fix(upstream): reject unusable litellm prices instead of resolving them A litellm cost entry that lists a model but prices both tokens at 0 (free moderation/rerank tiers) was resolved as a real price, importing the model enabled and served for free. Reject a both-zero (or negative) litellm hit so the caller falls through to the next source or fails closed, mirroring the _has_valid_pricing check the OpenRouter feed already applies. Co-Authored-By: Claude Opus 4.8 --- routstr/upstream/pricing_resolver.py | 6 +++++ tests/unit/test_upstream_generic.py | 39 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/routstr/upstream/pricing_resolver.py b/routstr/upstream/pricing_resolver.py index 4b06febe..80b27fc8 100644 --- a/routstr/upstream/pricing_resolver.py +++ b/routstr/upstream/pricing_resolver.py @@ -89,6 +89,12 @@ def _from_litellm(model_id: str) -> ResolvedPricing | None: completion = info.get("output_cost_per_token") if not isinstance(prompt, (int, float)) or not isinstance(completion, (int, float)): return None + # A both-zero entry is litellm listing a model without a real price (free + # moderation/rerank tiers do this) — treating 0/0 as resolved would serve + # the model for free. Reject it (and any negative) so the caller falls + # through, mirroring async_fetch_openrouter_models' _has_valid_pricing. + if prompt < 0 or completion < 0 or (prompt == 0 and completion == 0): + return None input_modalities = ["text"] if info.get("supports_vision"): diff --git a/tests/unit/test_upstream_generic.py b/tests/unit/test_upstream_generic.py index 3c312460..edc448a6 100644 --- a/tests/unit/test_upstream_generic.py +++ b/tests/unit/test_upstream_generic.py @@ -145,6 +145,45 @@ async def test_bare_deepseek_resolves_via_litellm() -> None: or_feed.assert_not_awaited() +@pytest.mark.asyncio +async def test_litellm_zero_price_entry_fails_closed( + caplog: pytest.LogCaptureFixture, +) -> None: + """A litellm entry that lists a model but prices it at 0/0 (free-tier + moderation/rerank models do this) is not a real price — treating it as one + would silently serve the model for free. The resolver must reject a both-zero + litellm hit and fall through, so the model imports disabled, not at $0.""" + payload = { + "data": [ + {"id": "omni-moderation-latest", "object": "model", "owned_by": "openai"}, + ] + } + + gen_logger = logging.getLogger("routstr.upstream.generic") + gen_logger.addHandler(caplog.handler) + try: + with _patch_models_endpoint(payload): + or_feed = AsyncMock(return_value=[]) + with patch( + "routstr.payment.models.async_fetch_openrouter_models", or_feed + ): + models = await GenericUpstreamProvider( + base_url="http://x" + ).fetch_models() + finally: + gen_logger.removeHandler(caplog.handler) + + model = _model_by_id(models, "omni-moderation-latest") + assert model.enabled is False + assert model.pricing.prompt == 0.0 + assert model.pricing.completion == 0.0 + assert any( + "omni-moderation-latest" in rec.getMessage() + for rec in caplog.records + if rec.levelno >= logging.WARNING + ) + + # --------------------------------------------------------------------------- # OpenRouter fallback — litellm misses, OR carries a full payload # ---------------------------------------------------------------------------