From 5bb2d11b079b1a1fb1a3d480536cf3ae8ed2a3d6 Mon Sep 17 00:00:00 2001 From: jerilynzheng Date: Mon, 20 Apr 2026 00:18:51 -0700 Subject: [PATCH] feat: auto-promote free Moonshot models to top of ai-gateway picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the live Vercel AI Gateway catalog exposes a Moonshot model with zero input AND output pricing, it's promoted to position #1 as the recommended default — even if the exact ID isn't in the curated AI_GATEWAY_MODELS list. This enables dynamic discovery of new free Moonshot variants without requiring a PR to update curation. Paid Moonshot models are unaffected; falls back to the normal curated recommended tag when no free Moonshot is live. --- hermes_cli/models.py | 20 ++++++++++++-- tests/hermes_cli/test_ai_gateway_models.py | 32 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/hermes_cli/models.py b/hermes_cli/models.py index b592b65056..5428fa5d82 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -812,8 +812,24 @@ def fetch_ai_gateway_models( if not curated: return list(_ai_gateway_catalog_cache or fallback) - first_id, _ = curated[0] - curated[0] = (first_id, "recommended") + # If the live catalog offers a free Moonshot model, auto-promote it to + # position #1 as "recommended" — dynamic discovery without a PR. + free_moonshot = next( + ( + mid + for mid, item in live_by_id.items() + if mid.startswith("moonshotai/") + and _ai_gateway_model_is_free(item.get("pricing")) + ), + None, + ) + if free_moonshot: + curated = [(mid, desc) for mid, desc in curated if mid != free_moonshot] + curated.insert(0, (free_moonshot, "recommended")) + else: + first_id, _ = curated[0] + curated[0] = (first_id, "recommended") + _ai_gateway_catalog_cache = curated return list(curated) diff --git a/tests/hermes_cli/test_ai_gateway_models.py b/tests/hermes_cli/test_ai_gateway_models.py index 0a175b8344..236060870d 100644 --- a/tests/hermes_cli/test_ai_gateway_models.py +++ b/tests/hermes_cli/test_ai_gateway_models.py @@ -122,6 +122,38 @@ def test_fetch_ai_gateway_models_tags_free_models(): assert by_id[second_id] == "free" +def test_free_moonshot_model_auto_promoted_to_top_even_if_not_curated(): + _reset_caches() + first_curated = AI_GATEWAY_MODELS[0][0] + unlisted_free_moonshot = "moonshotai/kimi-coder-free-preview" + payload = { + "data": [ + {"id": first_curated, "pricing": {"input": "0.001", "output": "0.002"}}, + {"id": unlisted_free_moonshot, "pricing": {"input": "0", "output": "0"}}, + ] + } + with patch("urllib.request.urlopen", return_value=_mock_urlopen(payload)): + result = fetch_ai_gateway_models(force_refresh=True) + + assert result[0] == (unlisted_free_moonshot, "recommended") + assert any(mid == first_curated for mid, _ in result) + + +def test_paid_moonshot_does_not_get_auto_promoted(): + _reset_caches() + first_curated = AI_GATEWAY_MODELS[0][0] + payload = { + "data": [ + {"id": first_curated, "pricing": {"input": "0.001", "output": "0.002"}}, + {"id": "moonshotai/some-paid-variant", "pricing": {"input": "0.001", "output": "0.002"}}, + ] + } + with patch("urllib.request.urlopen", return_value=_mock_urlopen(payload)): + result = fetch_ai_gateway_models(force_refresh=True) + + assert result[0][0] == first_curated + + def test_fetch_ai_gateway_models_falls_back_on_error(): _reset_caches() with patch("urllib.request.urlopen", side_effect=OSError("network")):