7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
287 lines
11 KiB
Python
287 lines
11 KiB
Python
"""
|
|
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
|