Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions langextract/providers/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
88 changes: 88 additions & 0 deletions tests/test_gpt5_reasoning_fix.py
Original file line number Diff line number Diff line change
@@ -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
135 changes: 135 additions & 0 deletions tests/test_integration_fix.py
Original file line number Diff line number Diff line change
@@ -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()
Loading