markitect-main/tests/unit/llm/test_openrouter.py

144 lines
5.3 KiB
Python
Raw Normal View History

"""Tests for markitect.llm.openrouter."""
from unittest import mock
import pytest
from markitect.llm.openrouter import OpenRouterAdapter
from markitect.llm.config import LLMConfig
from markitect.llm.exceptions import LLMRateLimitError, LLMAPIError
from markitect.prompts.execution.models import RunConfig, LLMResponse
def _make_api_response(content="hello world", model="test-model"):
return {
"id": "gen-abc123",
"model": model,
"choices": [
{
"message": {"role": "assistant", "content": content},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15,
},
}
class TestOpenRouterAdapter:
def _adapter(self, **kwargs):
defaults = {"api_key": "sk-test", "model": "test/model"}
defaults.update(kwargs)
return OpenRouterAdapter(**defaults)
@mock.patch("markitect.llm.openrouter.post_json")
def test_execute_prompt_success(self, mock_post):
mock_post.return_value = _make_api_response("Generated text")
adapter = self._adapter()
config = RunConfig(model_name="test/model", temperature=0.5, max_tokens=100)
resp = adapter.execute_prompt("Write something", config)
assert isinstance(resp, LLMResponse)
assert resp.content == "Generated text"
assert resp.usage["prompt_tokens"] == 10
assert resp.usage["completion_tokens"] == 5
assert resp.metadata["provider"] == "openrouter"
assert "latency_seconds" in resp.metadata
@mock.patch("markitect.llm.openrouter.post_json")
def test_payload_structure(self, mock_post):
mock_post.return_value = _make_api_response()
adapter = self._adapter(system_prompt="You are helpful")
config = RunConfig(temperature=0.3, max_tokens=500)
adapter.execute_prompt("Test prompt", config)
call_args = mock_post.call_args
payload = call_args[0][1] # second positional arg
assert payload["model"] == "test/model"
assert payload["temperature"] == 0.3
assert payload["max_tokens"] == 500
assert len(payload["messages"]) == 2
assert payload["messages"][0]["role"] == "system"
assert payload["messages"][1]["role"] == "user"
@mock.patch("markitect.llm.openrouter.post_json")
def test_no_system_prompt(self, mock_post):
mock_post.return_value = _make_api_response()
adapter = self._adapter()
config = RunConfig()
adapter.execute_prompt("Test", config)
payload = mock_post.call_args[0][1]
assert len(payload["messages"]) == 1
assert payload["messages"][0]["role"] == "user"
@mock.patch("markitect.llm.openrouter.post_json")
@mock.patch("markitect.llm.openrouter.time.sleep")
def test_retry_on_429(self, mock_sleep, mock_post):
mock_post.side_effect = [
LLMRateLimitError("rate limited", status_code=429),
_make_api_response("after retry"),
]
adapter = self._adapter(max_retries=2)
config = RunConfig()
resp = adapter.execute_prompt("Test", config)
assert resp.content == "after retry"
assert mock_sleep.call_count == 1
@mock.patch("markitect.llm.openrouter.post_json")
@mock.patch("markitect.llm.openrouter.time.sleep")
def test_retry_on_5xx(self, mock_sleep, mock_post):
mock_post.side_effect = [
LLMAPIError("server error", status_code=502),
_make_api_response("recovered"),
]
adapter = self._adapter(max_retries=2)
config = RunConfig()
resp = adapter.execute_prompt("Test", config)
assert resp.content == "recovered"
@mock.patch("markitect.llm.openrouter.post_json")
def test_no_retry_on_4xx(self, mock_post):
mock_post.side_effect = LLMAPIError("bad request", status_code=400)
adapter = self._adapter(max_retries=2)
config = RunConfig()
with pytest.raises(LLMAPIError) as exc_info:
adapter.execute_prompt("Test", config)
assert exc_info.value.status_code == 400
@mock.patch("markitect.llm.openrouter.post_json")
@mock.patch("markitect.llm.openrouter.time.sleep")
def test_exhausted_retries_raises(self, mock_sleep, mock_post):
mock_post.side_effect = LLMRateLimitError("rate limited", status_code=429)
adapter = self._adapter(max_retries=1)
config = RunConfig()
with pytest.raises(LLMRateLimitError):
adapter.execute_prompt("Test", config)
def test_validate_config_no_key(self):
adapter = OpenRouterAdapter(api_key=None, model="test")
# Force key to None
adapter._api_key = None
config = RunConfig()
assert adapter.validate_config(config) is False
def test_validate_config_ok(self):
adapter = self._adapter()
config = RunConfig(temperature=0.5)
assert adapter.validate_config(config) is True
def test_validate_config_bad_temperature(self):
adapter = self._adapter()
config = RunConfig(temperature=3.0)
assert adapter.validate_config(config) is False