"""Tests for `raycast_api.ai.models.ModelsAPI` and the typed response. The `/ai/models` endpoint returns a large payload; we only exercise the shape parsing (`ModelsResponse.from_wire`) and the `by_id` lookup plus the header that distinguishes our request from older Raycast clients (`X-Raycast-Experimental: autoModels`). """ from __future__ import annotations from typing import Any import pytest from aioresponses import aioresponses from raycast_api.ai.models import ModelInfo, ModelsResponse from raycast_api.client import Client from raycast_api.config import Config from raycast_api.signing_spec import RotRange, SigningSpec def _config() -> Config: return Config( signature_secret="0" * 64, signing_spec=SigningSpec( rot_fn_name="Sur", signing_fn_name="Nkt", rot_ranges=[ RotRange(start=65, end=90, shift=13), RotRange(start=97, end=122, shift=13), RotRange(start=48, end=57, shift=5), ], ), app_version="0.60.1.0", user_agent="Raycast/0.60.1.0 (x-macOS Version 26.3.1)", bundle_hash="0" * 64, launcher_hash="0" * 64, ) def _client(**kwargs: Any) -> Client: return Client( config=_config(), bearer_token="rca_test", device_id="a" * 64, clock=lambda: 0, **kwargs, ) SAMPLE_RESPONSE = { "models": [ { "id": "openai-gpt-5", "name": "GPT-5", "model": "gpt-5", "provider": "openai", "provider_name": "OpenAI", "provider_brand": "openai", "context": 200000, "description": "Most capable.", "status": "public", "availability": "public", "features": ["chat", "quick_ai", "commands"], "abilities": { "system_message": {"supported": True}, "temperature": {"supported": True}, "reasoning_effort": { "supported": True, "options": ["minimal", "low", "medium", "high"], }, }, "in_better_ai_subscription": True, "requires_better_ai": True, }, { "id": "anthropic-claude-sonnet-4-6", "name": "Claude Sonnet 4.6", "model": "claude-sonnet-4-6", "provider": "anthropic", "context": 200000, "features": ["chat"], "abilities": {"temperature": {"supported": True}}, "in_better_ai_subscription": True, }, ], "default_models": {"chat": "openai-gpt-5", "quick_ai": "openai-gpt-5"}, "free_models": ["openai-gpt-4o-mini", "anthropic-claude-haiku-4-5"], } class TestModelsResponse: def test_parses_model_list(self) -> None: r = ModelsResponse.from_wire(SAMPLE_RESPONSE) assert len(r.models) == 2 assert r.models[0].id == "openai-gpt-5" assert r.models[0].provider == "openai" assert r.default_models == {"chat": "openai-gpt-5", "quick_ai": "openai-gpt-5"} assert r.free_model_ids == ["openai-gpt-4o-mini", "anthropic-claude-haiku-4-5"] def test_free_models_tolerates_object_shape(self) -> None: """If the server ever switches back to emitting full objects, parse those too.""" r = ModelsResponse.from_wire( { "models": [], "default_models": {}, "free_models": [{"id": "a"}, {"id": "b"}], } ) assert r.free_model_ids == ["a", "b"] def test_by_id_lookup(self) -> None: r = ModelsResponse.from_wire(SAMPLE_RESPONSE) info = r.by_id("openai-gpt-5") assert info is not None assert info.name == "GPT-5" assert r.by_id("missing") is None def test_abilities_helpers(self) -> None: r = ModelsResponse.from_wire(SAMPLE_RESPONSE) gpt5 = r.by_id("openai-gpt-5") assert gpt5 is not None assert gpt5.supports_temperature is True assert gpt5.supports_reasoning_effort is True assert gpt5.reasoning_effort_options == ["minimal", "low", "medium", "high"] claude = r.by_id("anthropic-claude-sonnet-4-6") assert claude is not None assert claude.supports_reasoning_effort is False assert claude.reasoning_effort_options == [] class TestModelsAPI: @pytest.mark.asyncio async def test_list_sends_experimental_header_and_no_signature(self) -> None: captured: dict[str, Any] = {} def _cb(url: Any, **kwargs: Any) -> Any: captured["headers"] = dict(kwargs.get("headers") or {}) from aioresponses import CallbackResult return CallbackResult(status=200, payload=SAMPLE_RESPONSE) with aioresponses() as mocked: mocked.get("https://backend.raycast.com/api/v1/ai/models", callback=_cb) async with _client() as client: resp = await client.models.list() assert resp.by_id("openai-gpt-5") is not None assert captured["headers"]["X-Raycast-Experimental"] == "autoModels" assert "X-Raycast-Signature-v2" not in captured["headers"] assert "X-Raycast-Timestamp" not in captured["headers"] assert "X-Raycast-DeviceId" not in captured["headers"] assert captured["headers"]["Authorization"] == "Bearer rca_test"