""" Tests for the OpenAI-compatible base provider. Tests cover: - Provider class attributes - LLM creation - Model listing - Availability checking """ from unittest.mock import Mock, patch import pytest class TestOpenAICompatibleProviderAttributes: """Tests for OpenAICompatibleProvider class attributes.""" def test_has_provider_name(self): """Provider has provider_name attribute.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) assert hasattr(OpenAICompatibleProvider, "provider_name") assert OpenAICompatibleProvider.provider_name == "openai_endpoint" def test_has_default_base_url(self): """Provider has default_base_url attribute.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) assert hasattr(OpenAICompatibleProvider, "default_base_url") assert "api.openai.com" in OpenAICompatibleProvider.default_base_url def test_has_default_model(self): """Provider has default_model attribute.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) assert hasattr(OpenAICompatibleProvider, "default_model") class TestOpenAICompatibleProviderCreateLLM: """Tests for OpenAICompatibleProvider.create_llm method.""" def test_create_llm_raises_without_api_key(self): """create_llm raises ValueError when API key not configured.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot", return_value=None, ): try: OpenAICompatibleProvider.create_llm() assert False, "Expected ValueError" except ValueError as e: assert "API key not configured" in str(e) def test_create_llm_uses_default_model(self): """Raises ValueError when no model name is provided (no silent default).""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) def mock_get_setting(key, default=None, **kwargs): if key == "llm.openai_endpoint.api_key": return "test-api-key" if key == "llm.max_tokens": return None return default with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot", side_effect=mock_get_setting, ): with pytest.raises(ValueError, match="model not configured"): OpenAICompatibleProvider.create_llm() def test_create_llm_uses_specified_model(self): """create_llm uses specified model.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) def mock_get_setting(key, default=None, **kwargs): if key == "llm.openai_endpoint.api_key": return "test-api-key" if key == "llm.max_tokens": return None return default with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot", side_effect=mock_get_setting, ): with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: mock_chat.return_value = Mock() OpenAICompatibleProvider.create_llm(model_name="gpt-4") call_kwargs = mock_chat.call_args[1] assert call_kwargs["model"] == "gpt-4" def test_create_llm_uses_specified_temperature(self): """create_llm uses specified temperature.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) def mock_get_setting(key, default=None, **kwargs): if key == "llm.openai_endpoint.api_key": return "test-api-key" if key == "llm.max_tokens": return None return default with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot", side_effect=mock_get_setting, ): with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: mock_chat.return_value = Mock() OpenAICompatibleProvider.create_llm( model_name="test-model", temperature=0.5 ) call_kwargs = mock_chat.call_args[1] assert call_kwargs["temperature"] == 0.5 class TestOpenAICompatibleProviderIsAvailable: """Tests for OpenAICompatibleProvider.is_available method.""" def test_is_available_with_api_key(self): """is_available returns True when API key is configured.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot", return_value="test-api-key", ): result = OpenAICompatibleProvider.is_available() assert result is True def test_is_available_without_api_key(self): """is_available returns False when API key not configured.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot", return_value=None, ): result = OpenAICompatibleProvider.is_available() assert result is False def test_is_available_handles_exception(self): """is_available returns False on exception.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot", side_effect=Exception("Test error"), ): result = OpenAICompatibleProvider.is_available() assert result is False class TestOpenAICompatibleProviderRequiresAuthForModels: """Tests for OpenAICompatibleProvider.requires_auth_for_models method.""" def test_requires_auth_for_models_default(self): """requires_auth_for_models returns True by default.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) result = OpenAICompatibleProvider.requires_auth_for_models() assert result is True class TestOpenAICompatibleProviderListModels: """Tests for OpenAICompatibleProvider.list_models method.""" def test_list_models_returns_list(self): """list_models returns a list.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) with patch.object( OpenAICompatibleProvider, "list_models_for_api", return_value=[] ): with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot", return_value="test-key", ): result = OpenAICompatibleProvider.list_models() assert isinstance(result, list) def test_list_models_handles_exception(self): """list_models returns empty list on exception.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) with patch( "local_deep_research.config.thread_settings.get_setting_from_snapshot", side_effect=Exception("Test error"), ): result = OpenAICompatibleProvider.list_models() assert result == [] class TestOpenAICompatibleProviderListModelsForApi: """Tests for OpenAICompatibleProvider.list_models_for_api method.""" def test_list_models_for_api_without_api_key(self): """list_models_for_api returns empty list when auth required but no key.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) result = OpenAICompatibleProvider.list_models_for_api(api_key=None) assert result == [] def test_list_models_for_api_with_api_key(self): """list_models_for_api calls OpenAI client with provided key.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) mock_model = Mock() mock_model.id = "test-model" mock_models_response = Mock() mock_models_response.data = [mock_model] with patch("openai.OpenAI") as mock_openai: mock_client = Mock() mock_client.models.list.return_value = mock_models_response mock_openai.return_value = mock_client result = OpenAICompatibleProvider.list_models_for_api( api_key="test-key" ) assert len(result) == 1 assert result[0]["value"] == "test-model" def test_list_models_for_api_handles_exception(self): """list_models_for_api returns empty list on exception.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) with patch("openai.OpenAI") as mock_openai: mock_openai.side_effect = Exception("Connection error") result = OpenAICompatibleProvider.list_models_for_api( api_key="test-key" ) assert result == [] class TestOpenAICompatibleProviderGetBaseUrlForModels: """Tests for OpenAICompatibleProvider._get_base_url_for_models method.""" def test_get_base_url_uses_default(self): """_get_base_url_for_models uses default when no url_setting.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) result = OpenAICompatibleProvider._get_base_url_for_models() assert result == OpenAICompatibleProvider.default_base_url class TestOpenAICompatibleProviderCreateLLMInstance: """Tests for OpenAICompatibleProvider._create_llm_instance method.""" def test_create_llm_instance_uses_dummy_key(self): """_create_llm_instance uses dummy key by default.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: mock_chat.return_value = Mock() OpenAICompatibleProvider._create_llm_instance( model_name="test-model" ) call_kwargs = mock_chat.call_args[1] assert call_kwargs["api_key"] == "not-required" def test_create_llm_instance_uses_provided_key(self): """_create_llm_instance uses provided API key.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) with patch( "local_deep_research.llm.providers.openai_base.ChatOpenAI" ) as mock_chat: mock_chat.return_value = Mock() OpenAICompatibleProvider._create_llm_instance( model_name="test-model", api_key="custom-key" ) call_kwargs = mock_chat.call_args[1] assert call_kwargs["api_key"] == "custom-key" class TestCustomProvider: """Tests for creating custom providers inheriting from OpenAICompatibleProvider.""" def test_custom_provider_inherits_attributes(self): """Custom provider inherits base attributes.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) class CustomProvider(OpenAICompatibleProvider): provider_name = "Custom" api_key_setting = None # No API key needed default_base_url = "http://localhost:8000" assert CustomProvider.provider_name == "Custom" assert CustomProvider.default_base_url == "http://localhost:8000" def test_custom_provider_no_api_key_available(self): """Custom provider without api_key_setting is always available.""" from local_deep_research.llm.providers.openai_base import ( OpenAICompatibleProvider, ) class NoAuthProvider(OpenAICompatibleProvider): provider_name = "NoAuth" api_key_setting = None default_base_url = "http://localhost:8000" result = NoAuthProvider.is_available() assert result is True