diff --git a/langextract/providers/openai.py b/langextract/providers/openai.py index 8f45c77f..7d78f30b 100644 --- a/langextract/providers/openai.py +++ b/langextract/providers/openai.py @@ -122,11 +122,18 @@ def _normalize_reasoning_params(self, config: dict) -> dict: """ result = config.copy() - if 'reasoning_effort' in result: - effort = result.pop('reasoning_effort') - reasoning = result.get('reasoning', {}) or {} - reasoning.setdefault('effort', effort) - result['reasoning'] = reasoning + # Check if this is a GPT-5 model that supports reasoning_effort + is_gpt5_model = self.model_id.lower().startswith(('gpt-5', 'gpt5')) + + if 'reasoning_effort' in result and is_gpt5_model: + # For GPT-5 models, pass reasoning_effort as-is + # Remove any existing reasoning dict to avoid conflicts + if 'reasoning' in result: + del result['reasoning'] + + elif 'reasoning_effort' in result and not is_gpt5_model: + # For non-GPT-5 models, remove reasoning_effort (not supported) + result.pop('reasoning_effort') return result @@ -176,7 +183,9 @@ def _process_single_prompt( 'logprobs', 'top_logprobs', 'reasoning', + 'reasoning_effort', # Add this to pass it through 'response_format', + 'verbosity', # Add verbosity for GPT-5 models ]: if (v := normalized_config.get(key)) is not None: api_params[key] = v @@ -227,6 +236,7 @@ def infer( 'reasoning_effort', 'reasoning', 'response_format', + 'verbosity', # Add verbosity for GPT-5 models ]: if key in merged_kwargs: config[key] = merged_kwargs[key] diff --git a/tests/test_gpt5_reasoning_fix.py b/tests/test_gpt5_reasoning_fix.py new file mode 100644 index 00000000..8cbb2189 --- /dev/null +++ b/tests/test_gpt5_reasoning_fix.py @@ -0,0 +1,88 @@ +"""Tests for GPT-5 reasoning_effort parameter fix.""" + +from unittest.mock import MagicMock, patch +from langextract.providers.openai import OpenAILanguageModel + + +class TestGPT5ReasoningEffort: + """Test class for GPT-5 reasoning effort parameter handling.""" + + def test_gpt5_reasoning_effort_preserved(self): + """Test that reasoning_effort is preserved for GPT-5 models.""" + model = OpenAILanguageModel( + model_id="gpt-5-mini", + api_key="test-key" + ) + + config = {"reasoning_effort": "minimal", "temperature": 0.5} + normalized = model._normalize_reasoning_params(config) + + # Should preserve reasoning_effort for GPT-5 models + assert "reasoning_effort" in normalized + assert normalized["reasoning_effort"] == "minimal" + assert "reasoning" not in normalized + + def test_gpt4_reasoning_effort_removed(self): + """Test that reasoning_effort is removed for non-GPT-5 models.""" + model = OpenAILanguageModel( + model_id="gpt-4o-mini", + api_key="test-key" + ) + + config = {"reasoning_effort": "minimal", "temperature": 0.5} + normalized = model._normalize_reasoning_params(config) + + # Should remove reasoning_effort for non-GPT-5 models + assert "reasoning_effort" not in normalized + assert normalized["temperature"] == 0.5 + + def test_gpt5_variants_supported(self): + """Test all GPT-5 variants support reasoning_effort.""" + variants = ["gpt-5", "gpt-5-mini", "gpt-5-nano", "GPT-5-MINI"] + + for variant in variants: + model = OpenAILanguageModel( + model_id=variant, + api_key="test-key" + ) + + config = {"reasoning_effort": "low"} + normalized = model._normalize_reasoning_params(config) + + assert "reasoning_effort" in normalized + assert normalized["reasoning_effort"] == "low" + + @patch('openai.OpenAI') + def test_api_call_with_reasoning_effort(self, mock_openai_class): + """Test that reasoning_effort is passed to OpenAI API correctly.""" + # Setup mock + mock_client = MagicMock() + mock_openai_class.return_value = mock_client + + mock_response = MagicMock() + mock_response.choices = [MagicMock()] + mock_response.choices[0].message.content = "Test response" + mock_client.chat.completions.create.return_value = mock_response + + # Create model and make inference + model = OpenAILanguageModel( + model_id="gpt-5-mini", + api_key="test-key" + ) + + # Process with reasoning_effort + list(model.infer( + ["Test prompt"], + reasoning_effort="minimal", + verbosity="low" + )) + + # Verify API was called with correct parameters + mock_client.chat.completions.create.assert_called_once() + call_kwargs = mock_client.chat.completions.create.call_args[1] + + assert "reasoning_effort" in call_kwargs + assert call_kwargs["reasoning_effort"] == "minimal" + assert "verbosity" in call_kwargs + assert call_kwargs["verbosity"] == "low" + assert "reasoning" not in call_kwargs # Should not have nested reasoning diff --git a/tests/test_integration_fix.py b/tests/test_integration_fix.py new file mode 100644 index 00000000..66fa10aa --- /dev/null +++ b/tests/test_integration_fix.py @@ -0,0 +1,135 @@ +"""Integration test to verify the fix works.""" + +import os +import sys +from langextract import factory +import langextract as lx + + +def create_extract_example(): + """Create a sample extraction example as required by LangExtract.""" + examples = [ + lx.data.ExampleData( + text="iPhone 14 Pro Max costs $1099 and has 256GB storage capacity.", + extractions=[ + lx.data.Extraction( + extraction_class="product_info", + extraction_text="iPhone 14 Pro Max costs $1099 and has 256GB storage capacity.", + attributes={ + "product_name": "iPhone 14 Pro Max", + "price": "$1099", + "storage": "256GB" + }, + ) + ], + ) + ] + return examples + + +def test_fixed_reasoning_effort(): + """Test the original failing case now works.""" + + # Your original configuration that was failing + config = factory.ModelConfig( + model_id="gpt-5-mini", + provider_kwargs={ + "api_key": os.getenv("OPENAI_API_KEY"), + "temperature": 0.3, + "verbosity": "low", + "reasoning_effort": "minimal", # This should now work + } + ) + + # Create required examples + examples = create_extract_example() + + try: + # This was the failing call from the issue - now with examples + lx.extract( + text_or_documents="iPhone 15 Pro costs $999 and has 128GB storage", + prompt_description="Extract product information including name, price, and storage", + examples=examples, # Now providing required examples + config=config, + fence_output=True, + use_schema_constraints=False + ) + + print("✅ SUCCESS: reasoning_effort parameter now works correctly!") + return True + + except Exception as exc: + if "unexpected keyword argument 'reasoning'" in str(exc): + print("❌ FAILED: Original issue still exists") + elif "Examples are required" in str(exc): + print("❌ FAILED: Examples issue (but reasoning_effort fix is working)") + else: + print(f"❌ FAILED: Different error - {exc}") + return False + + +def test_without_reasoning_effort(): + """Test that the same call works without reasoning_effort (control test).""" + + config = factory.ModelConfig( + model_id="gpt-5-mini", + provider_kwargs={ + "api_key": os.getenv("OPENAI_API_KEY"), + "temperature": 0.3, + "verbosity": "low", + # No reasoning_effort - this should work + } + ) + + examples = create_extract_example() + + try: + lx.extract( + text_or_documents="Samsung Galaxy S24 costs $799 with 128GB storage", + prompt_description="Extract product information", + examples=examples, + config=config, + fence_output=True, + use_schema_constraints=False + ) + + print("✅ SUCCESS: Control test (without reasoning_effort) works") + return True + + except Exception as exc: + print(f"❌ FAILED: Control test failed - {exc}") + return False + + +def main(): + """Main function to run tests.""" + print("Testing GPT-5 reasoning_effort fix...") + print("=" * 50) + + # Check if API key is set + if not os.getenv("OPENAI_API_KEY"): + print("⚠️ WARNING: OPENAI_API_KEY not set. Set it to run integration tests.") + print(" export OPENAI_API_KEY='your-api-key-here'") + sys.exit(1) + + # Test the fix + print("1. Testing WITH reasoning_effort (the original failing case):") + success1 = test_fixed_reasoning_effort() + + print("\n2. Testing WITHOUT reasoning_effort (control test):") + success2 = test_without_reasoning_effort() + + print("\n" + "=" * 50) + if success1: + print("🎉 FIX CONFIRMED: reasoning_effort parameter now works!") + else: + print("❌ Fix may need more work") + + if success2: + print("✅ Control test passed - basic functionality intact") + else: + print("⚠️ Control test failed - check basic setup") + + +if __name__ == "__main__": + main()