feat: vibed out some slop over here
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user