From dea5e8a96df4191abd15a82ab5d06b2be67db747 Mon Sep 17 00:00:00 2001 From: igorroncevic <57319163+igorroncevic@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:18:13 +0200 Subject: [PATCH] fix: fail closed on incomplete OpenRouter pricing --- api/config.py | 22 +++++---- ...sue1426_openrouter_free_tier_live_fetch.py | 48 +++++++++++++++++++ 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/api/config.py b/api/config.py index 84f286263..e5d63dc38 100644 --- a/api/config.py +++ b/api/config.py @@ -6890,14 +6890,20 @@ def get_available_models(*, prefer_cache: bool = False, force_refresh: bool = Fa _mid = str(_item.get("id") or "").strip() if not _mid or _mid in seen_ids: continue - _pricing = _item.get("pricing") or {} - try: - _is_free = ( - float(_pricing.get("prompt", "0") or "0") == 0 - and float(_pricing.get("completion", "0") or "0") == 0 - ) - except (TypeError, ValueError): - _is_free = False + _pricing = _item.get("pricing") + _is_free = False + if ( + isinstance(_pricing, dict) + and "prompt" in _pricing + and "completion" in _pricing + ): + try: + _is_free = ( + float(_pricing["prompt"]) == 0 + and float(_pricing["completion"]) == 0 + ) + except (TypeError, ValueError): + _is_free = False # Also include explicit `:free` suffix variants _is_free = _is_free or _mid.endswith(":free") if not _is_free: diff --git a/tests/test_issue1426_openrouter_free_tier_live_fetch.py b/tests/test_issue1426_openrouter_free_tier_live_fetch.py index 7e25d25e7..90f2a553c 100644 --- a/tests/test_issue1426_openrouter_free_tier_live_fetch.py +++ b/tests/test_issue1426_openrouter_free_tier_live_fetch.py @@ -138,6 +138,54 @@ def test_openrouter_group_uses_live_fetch_when_available(monkeypatch): "free pricing model must surface even without :free suffix" +@pytest.mark.parametrize( + ("pricing", "case_name"), + [ + ({}, "missing"), + ({"prompt": "0"}, "completion missing"), + ({"completion": "0"}, "prompt missing"), + ({"prompt": None, "completion": None}, "null"), + ({"prompt": "n/a", "completion": "0"}, "malformed"), + ], +) +def test_openrouter_free_tier_pricing_fails_closed( + monkeypatch, + pricing, + case_name, +): + """Unknown pricing must not be presented as free without a :free ID.""" + candidate_id = f"vendor/{case_name.replace(' ', '-')}-pricing" + explicit_free_id = "vendor/explicit-free:free" + fake_payload = _make_or_payload( + {"id": candidate_id, "name": case_name, "pricing": pricing}, + {"id": explicit_free_id, "name": "Explicit Free", "pricing": pricing}, + ) + + monkeypatch.setattr( + urllib.request, + "urlopen", + lambda *_args, **_kwargs: _FakeResponse(fake_payload), + ) + + from hermes_cli import models as hermes_models + + monkeypatch.setattr(hermes_models, "fetch_openrouter_models", lambda **_kwargs: []) + monkeypatch.setattr(hermes_models, "provider_model_ids", lambda *_args, **_kwargs: []) + + grouped = _get_grouped_models() + openrouter_group = next( + group for group in grouped if group.get("provider_id") == "openrouter" + ) + model_ids = { + model["id"] + for bucket_name in ("models", "extra_models") + for model in openrouter_group.get(bucket_name, []) + } + + assert candidate_id not in model_ids + assert explicit_free_id in model_ids + + def test_openrouter_falls_back_to_static_when_live_fails(monkeypatch): """If both hermes_cli.fetch and the direct urlopen raise, the picker must fall back to the hardcoded `_FALLBACK_MODELS` list — never empty."""