"""Tests for llama.cpp LLM provider.""" from unittest.mock import Mock, patch from local_deep_research.llm.providers.implementations.llamacpp import ( LlamaCppProvider, ) def _make_setting_side_effect(url_value, api_key_value=""): """Create a side_effect function that returns url_value for llm.llamacpp.url, api_key_value for llm.llamacpp.api_key, and default for everything else. Uses *args, **kwargs to handle variable call patterns from get_setting_from_snapshot. """ def _setting_side_effect(*args, **kwargs): key = args[0] if args else kwargs.get("key", "") default = args[1] if len(args) > 1 else kwargs.get("default", None) if key == "llm.llamacpp.url": return url_value if key == "llm.llamacpp.api_key": return api_key_value return default return _setting_side_effect class TestLlamaCppProviderMetadata: """Tests for LlamaCppProvider class metadata.""" def test_provider_name(self): """Provider name is correct.""" assert LlamaCppProvider.provider_name == "llama.cpp" def test_provider_key(self): """Provider key is correct.""" assert LlamaCppProvider.provider_key == "LLAMACPP" def test_company_name(self): """Company name is llama.cpp.""" assert LlamaCppProvider.company_name == "llama.cpp" def test_is_not_cloud(self): """llama.cpp is a local provider.""" assert LlamaCppProvider.is_cloud is False def test_api_key_setting_is_optional(self): """llama.cpp declares its setting key but treats it as optional.""" assert LlamaCppProvider.api_key_setting == "llm.llamacpp.api_key" assert LlamaCppProvider.api_key_optional is True def test_url_setting(self): """URL setting is defined.""" assert LlamaCppProvider.url_setting == "llm.llamacpp.url" def test_default_model(self): """Default model is empty — user must specify.""" assert LlamaCppProvider.default_model == "" def test_default_base_url(self): """Default base URL is localhost on port 8080.""" assert "8080" in LlamaCppProvider.default_base_url class TestLlamaCppCreateLLM: """Tests for create_llm method.""" def test_create_llm_success(self): """Successfully creates ChatOpenAI instance.""" with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( "http://localhost:8080/v1" ) with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: mock_llm = Mock() mock_chat.return_value = mock_llm result = LlamaCppProvider.create_llm(model_name="test-model") assert result is mock_llm mock_chat.assert_called_once() def test_create_llm_uses_default_url(self): """Uses default URL when not configured.""" with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( LlamaCppProvider.default_base_url ) with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: LlamaCppProvider.create_llm(model_name="test-model") call_kwargs = mock_chat.call_args[1] assert "localhost" in call_kwargs["base_url"] assert "8080" in call_kwargs["base_url"] def test_create_llm_uses_custom_url(self): """Uses custom URL from settings. Uses ``localhost`` (legitimate Llama.cpp host) rather than the placeholder ``custom`` so the SSRF guard added in PR-B doesn't trip on DNS resolution. The test is checking propagation of a custom URL into ChatOpenAI, not SSRF semantics. """ with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( "http://localhost:5000/v1" ) with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: LlamaCppProvider.create_llm(model_name="test-model") call_kwargs = mock_chat.call_args[1] assert "localhost" in call_kwargs["base_url"] assert "5000" in call_kwargs["base_url"] def test_create_llm_placeholder_api_key_when_unset(self): """Falls back to placeholder API key when none configured.""" with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( "http://localhost:8080/v1", api_key_value="" ) with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: LlamaCppProvider.create_llm(model_name="test-model") call_kwargs = mock_chat.call_args[1] assert call_kwargs["api_key"] == "not-required" def test_create_llm_uses_real_api_key(self): """Uses configured API key when llm.llamacpp.api_key is set.""" with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( "http://localhost:8080/v1", api_key_value="my-secret-key" ) with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: LlamaCppProvider.create_llm(model_name="test-model") call_kwargs = mock_chat.call_args[1] assert call_kwargs["api_key"] == "my-secret-key" def test_create_llm_with_custom_model(self): """Uses custom model when specified.""" with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( "http://localhost:8080/v1" ) with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: LlamaCppProvider.create_llm(model_name="test-model") call_kwargs = mock_chat.call_args[1] assert call_kwargs["model"] == "test-model" def test_create_llm_with_custom_temperature(self): """Uses custom temperature.""" with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( "http://localhost:8080/v1" ) with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: LlamaCppProvider.create_llm( model_name="test-model", temperature=0.3 ) call_kwargs = mock_chat.call_args[1] assert call_kwargs["temperature"] == 0.3 class TestLlamaCppIsAvailable: """Tests for is_available method.""" def test_true_when_server_responds_200(self): """Returns True when llama-server responds with 200.""" with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( "http://localhost:8080/v1" ) with patch("local_deep_research.security.safe_get") as mock_get: mock_response = Mock() mock_response.status_code = 200 mock_get.return_value = mock_response result = LlamaCppProvider.is_available() assert result is True def test_false_when_server_returns_500(self): """Returns False when server returns 500.""" with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( "http://localhost:8080/v1" ) with patch("local_deep_research.security.safe_get") as mock_get: mock_response = Mock() mock_response.status_code = 500 mock_get.return_value = mock_response result = LlamaCppProvider.is_available() assert result is False def test_false_on_connection_error(self): """Returns False when connection fails.""" with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( "http://localhost:8080/v1" ) with patch("local_deep_research.security.safe_get") as mock_get: mock_get.side_effect = Exception("Connection refused") result = LlamaCppProvider.is_available() assert result is False def test_sends_bearer_header_when_api_key_configured(self): """Authenticated llama-server (proxy) instances are detected as available. Regression: previously is_available() probed without Authorization, so a 401 from an auth proxy made the provider report unavailable even though create_llm() was perfectly capable of using the key. """ with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = _make_setting_side_effect( "http://localhost:8080/v1", api_key_value="my-secret-key" ) with patch("local_deep_research.security.safe_get") as mock_get: mock_response = Mock() mock_response.status_code = 200 mock_get.return_value = mock_response LlamaCppProvider.is_available() # Assert headers were forwarded call_kwargs = mock_get.call_args[1] assert call_kwargs.get("headers") == { "Authorization": "Bearer my-secret-key" } class TestLlamaCppRequiresAuth: """Tests for requires_auth_for_models method.""" def test_does_not_require_auth(self): """llama.cpp doesn't require authentication for listing models.""" assert LlamaCppProvider.requires_auth_for_models() is False