"""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