"""AI Gateway model list and pricing translation. Vercel AI Gateway exposes ``/v1/models`` with a richer shape than OpenAI's spec (type, tags, pricing). The pricing object uses ``input`` / ``output`` where hermes's shared picker expects ``prompt`` / ``completion``; these tests pin the translation and the curated-list filtering. """ import json from unittest.mock import patch, MagicMock from hermes_cli import models as models_module from hermes_cli.models import ( VERCEL_AI_GATEWAY_MODELS, _ai_gateway_model_is_free, fetch_ai_gateway_models, fetch_ai_gateway_pricing, ) def _mock_urlopen(payload): """Build a urlopen() context manager mock returning the given payload.""" resp = MagicMock() resp.read.return_value = json.dumps(payload).encode() ctx = MagicMock() ctx.__enter__.return_value = resp ctx.__exit__.return_value = False return ctx def _reset_caches(): models_module._ai_gateway_catalog_cache = None models_module._pricing_cache.clear() def test_ai_gateway_pricing_translates_input_output_to_prompt_completion(): _reset_caches() payload = { "data": [ { "id": "moonshotai/kimi-k2.5", "type": "language", "pricing": { "input": "0.0000006", "output": "0.0000025", "input_cache_read": "0.00000015", "input_cache_write": "0.0000006", }, } ] } with patch("urllib.request.urlopen", return_value=_mock_urlopen(payload)): result = fetch_ai_gateway_pricing(force_refresh=True) entry = result["moonshotai/kimi-k2.5"] assert entry["prompt"] == "0.0000006" assert entry["completion"] == "0.0000025" assert entry["input_cache_read"] == "0.00000015" assert entry["input_cache_write"] == "0.0000006" def test_ai_gateway_pricing_returns_empty_on_fetch_failure(): _reset_caches() with patch("urllib.request.urlopen", side_effect=OSError("network down")): result = fetch_ai_gateway_pricing(force_refresh=True) assert result == {} def test_ai_gateway_pricing_skips_entries_without_pricing_dict(): _reset_caches() payload = { "data": [ {"id": "x/y", "pricing": None}, {"id": "a/b", "pricing": {"input": "0", "output": "0"}}, ] } with patch("urllib.request.urlopen", return_value=_mock_urlopen(payload)): result = fetch_ai_gateway_pricing(force_refresh=True) assert "x/y" not in result assert result["a/b"] == {"prompt": "0", "completion": "0"} def test_ai_gateway_free_detector(): assert _ai_gateway_model_is_free({"input": "0", "output": "0"}) is True assert _ai_gateway_model_is_free({"input": "0", "output": "0.01"}) is False assert _ai_gateway_model_is_free({"input": "0.01", "output": "0"}) is False assert _ai_gateway_model_is_free(None) is False assert _ai_gateway_model_is_free({"input": "not a number"}) is False def test_fetch_ai_gateway_models_filters_against_live_catalog(): _reset_caches() preferred = [mid for mid, _ in VERCEL_AI_GATEWAY_MODELS] live_ids = preferred[:3] # only first three exist live payload = { "data": [ {"id": mid, "pricing": {"input": "0.001", "output": "0.002"}} for mid in live_ids ] } with patch("urllib.request.urlopen", return_value=_mock_urlopen(payload)): result = fetch_ai_gateway_models(force_refresh=True) assert [mid for mid, _ in result] == live_ids assert result[0][1] == "recommended" def test_fetch_ai_gateway_models_tags_free_models(): _reset_caches() first_id = VERCEL_AI_GATEWAY_MODELS[0][0] second_id = VERCEL_AI_GATEWAY_MODELS[1][0] payload = { "data": [ {"id": first_id, "pricing": {"input": "0.001", "output": "0.002"}}, {"id": second_id, "pricing": {"input": "0", "output": "0"}}, ] } with patch("urllib.request.urlopen", return_value=_mock_urlopen(payload)): result = fetch_ai_gateway_models(force_refresh=True) by_id = dict(result) assert by_id[first_id] == "recommended" assert by_id[second_id] == "free" def test_free_moonshot_model_auto_promoted_to_top_even_if_not_curated(): _reset_caches() first_curated = VERCEL_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 = VERCEL_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")): result = fetch_ai_gateway_models(force_refresh=True) assert result == list(VERCEL_AI_GATEWAY_MODELS)