""" Comprehensive test for OpenAI API key configuration and usage. This test specifically verifies that OpenAI API keys can be: 1. Configured through settings 2. Properly passed to the OpenAI client 3. Used for actual research operations """ import pytest from unittest.mock import Mock, patch import os from local_deep_research.config.llm_config import get_llm @pytest.mark.requires_llm class TestOpenAIAPIKeyUsage: """Test OpenAI API key configuration and usage throughout the system.""" @pytest.fixture def openai_settings_snapshot(self): """Create settings snapshot with OpenAI configuration. Uses simplified format (raw values) to bypass get_typed_setting_value type coercion in get_setting_from_snapshot(). """ return { "search.tool": "searxng", "llm.provider": "openai", "llm.model": "gpt-3.5-turbo", "llm.temperature": 0.7, "llm.openai.api_key": "sk-test-1234567890abcdef", "llm.openai.api_base": None, "llm.openai.organization": None, "llm.streaming": None, "llm.max_retries": None, "llm.request_timeout": None, "llm.context_window_unrestricted": False, "llm.context_window_size": 128000, "llm.supports_max_tokens": True, "llm.max_tokens": 100000, "llm.provider.openai.context_window": 4096, "research.iterations": 2, "research.questions_per_iteration": 3, "research.search_engines": ["wikipedia"], "research.local_context": 2000, "research.web_context": 2000, "rate_limiting.llm_enabled": False, } def test_openai_api_key_in_llm_config(self, openai_settings_snapshot): """Test that OpenAI API key is properly passed to ChatOpenAI.""" with patch( "local_deep_research.llm.providers.implementations.openai.ChatOpenAI" ) as mock_openai: # Mock the LLM instance mock_llm_instance = Mock() mock_openai.return_value = mock_llm_instance # Get LLM with settings get_llm(settings_snapshot=openai_settings_snapshot) # Verify ChatOpenAI was called with correct API key mock_openai.assert_called_once() call_args = mock_openai.call_args # Check that API key was passed assert call_args is not None assert len(call_args) > 1 assert call_args[1]["api_key"] == "sk-test-1234567890abcdef" assert call_args[1]["model"] == "gpt-3.5-turbo" assert call_args[1]["temperature"] == 0.7 @pytest.mark.skipif( os.environ.get("CI") == "true" or os.environ.get("GITHUB_ACTIONS") == "true", reason="Skipped in CI - requires environment variable configuration", ) def test_openai_api_key_from_environment(self, openai_settings_snapshot): """Test fallback to environment variable if API key not in settings.""" # Modify settings to have no API key settings_no_key = openai_settings_snapshot.copy() settings_no_key["llm.openai.api_key"] = None # Set LDR-prefixed environment variable with patch.dict( os.environ, {"LDR_LLM_OPENAI_API_KEY": "sk-env-test-key"} ): with patch( "local_deep_research.llm.providers.implementations.openai.ChatOpenAI" ) as mock_openai: mock_llm_instance = Mock() mock_openai.return_value = mock_llm_instance get_llm(settings_snapshot=settings_no_key) # Should use environment variable call_args = mock_openai.call_args assert call_args is not None assert len(call_args) > 1 assert call_args[1]["api_key"] == "sk-env-test-key" def test_openai_api_key_in_research_flow(self, openai_settings_snapshot): """Test that API key is properly passed through research flow to OpenAI.""" # Mock the ChatOpenAI class to verify API key is passed with patch( "local_deep_research.llm.providers.implementations.openai.ChatOpenAI" ) as mock_openai: # Create a mock LLM instance mock_llm = Mock() mock_openai.return_value = mock_llm # Simply get the LLM to verify the API key is passed from local_deep_research.config.llm_config import get_llm llm = get_llm(settings_snapshot=openai_settings_snapshot) # Verify API key was passed correctly assert mock_openai.called assert ( mock_openai.call_args[1]["api_key"] == "sk-test-1234567890abcdef" ) assert mock_openai.call_args[1]["model"] == "gpt-3.5-turbo" assert llm is not None def test_openai_with_custom_endpoint(self, openai_settings_snapshot): """Test OpenAI with custom API endpoint (e.g., Azure OpenAI).""" # Add custom endpoint to settings custom_settings = openai_settings_snapshot.copy() custom_settings["llm.openai.api_base"] = ( "https://custom-openai.azure.com" ) custom_settings["llm.openai.api_key"] = "custom-azure-key" with patch( "local_deep_research.llm.providers.implementations.openai.ChatOpenAI" ) as mock_openai: mock_llm_instance = Mock() mock_openai.return_value = mock_llm_instance get_llm(settings_snapshot=custom_settings) # Verify custom endpoint and key were used call_args = mock_openai.call_args assert call_args is not None assert len(call_args) > 1 assert call_args[1]["api_key"] == "custom-azure-key" assert ( call_args[1].get("openai_api_base") == "https://custom-openai.azure.com" ) def test_openai_error_handling_invalid_key(self, openai_settings_snapshot): """Test error handling when OpenAI API key is invalid.""" with patch( "local_deep_research.llm.providers.implementations.openai.ChatOpenAI" ) as mock_openai: # Simulate OpenAI authentication error mock_openai.side_effect = Exception("Invalid API key provided") # Should raise exception with clear message with pytest.raises(Exception, match="Invalid API key"): get_llm(settings_snapshot=openai_settings_snapshot) def test_openai_model_selection(self, openai_settings_snapshot): """Test different OpenAI model selections.""" models_to_test = [ "gpt-4", "gpt-4-turbo-preview", "gpt-3.5-turbo-16k", "gpt-4-1106-preview", ] for model in models_to_test: settings = openai_settings_snapshot.copy() settings["llm.model"] = model with patch( "local_deep_research.llm.providers.implementations.openai.ChatOpenAI" ) as mock_openai: mock_llm_instance = Mock() mock_openai.return_value = mock_llm_instance get_llm(settings_snapshot=settings) # Verify correct model was selected assert mock_openai.call_args[1]["model"] == model def test_openai_streaming_configuration(self, openai_settings_snapshot): """Test OpenAI streaming configuration.""" # Add streaming setting streaming_settings = openai_settings_snapshot.copy() streaming_settings["llm.streaming"] = True with patch( "local_deep_research.llm.providers.implementations.openai.ChatOpenAI" ) as mock_openai: mock_llm_instance = Mock() mock_openai.return_value = mock_llm_instance get_llm(settings_snapshot=streaming_settings) # Verify streaming was enabled assert mock_openai.call_args[1].get("streaming") is True def test_openai_retry_configuration(self, openai_settings_snapshot): """Test OpenAI retry and timeout configuration.""" # Add retry settings retry_settings = openai_settings_snapshot.copy() retry_settings["llm.max_retries"] = 3 retry_settings["llm.request_timeout"] = 60 with patch( "local_deep_research.llm.providers.implementations.openai.ChatOpenAI" ) as mock_openai: mock_llm_instance = Mock() mock_openai.return_value = mock_llm_instance get_llm(settings_snapshot=retry_settings) # Verify retry configuration call_args = mock_openai.call_args assert call_args[1].get("max_retries") == 3 assert call_args[1].get("request_timeout") == 60 def test_openai_organization_id(self, openai_settings_snapshot): """Test OpenAI organization ID configuration.""" # Add organization ID org_settings = openai_settings_snapshot.copy() org_settings["llm.openai.organization"] = "org-test123" with patch( "local_deep_research.llm.providers.implementations.openai.ChatOpenAI" ) as mock_openai: mock_llm_instance = Mock() mock_openai.return_value = mock_llm_instance get_llm(settings_snapshot=org_settings) # Verify organization ID was passed assert ( mock_openai.call_args[1].get("openai_organization") == "org-test123" ) def test_openai_full_integration(self, openai_settings_snapshot): """Full integration test verifying all OpenAI configuration parameters.""" # Test full OpenAI configuration full_settings = openai_settings_snapshot.copy() full_settings["llm.streaming"] = True full_settings["llm.max_retries"] = 5 full_settings["llm.request_timeout"] = 120 full_settings["llm.openai.organization"] = "org-123" with patch( "local_deep_research.llm.providers.implementations.openai.ChatOpenAI" ) as mock_openai: # Create mock LLM mock_llm = Mock() mock_openai.return_value = mock_llm from local_deep_research.config.llm_config import get_llm # Get LLM with full configuration get_llm(settings_snapshot=full_settings) # Verify all parameters were passed correctly assert mock_openai.called call_kwargs = mock_openai.call_args[1] # Core parameters assert call_kwargs["api_key"] == "sk-test-1234567890abcdef" assert call_kwargs["model"] == "gpt-3.5-turbo" assert call_kwargs["temperature"] == 0.7 # Additional parameters assert call_kwargs["streaming"] is True assert call_kwargs["max_retries"] == 5 assert call_kwargs["request_timeout"] == 120 assert call_kwargs["openai_organization"] == "org-123" # Verify max_tokens was set assert "max_tokens" in call_kwargs assert call_kwargs["max_tokens"] > 0