chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,550 @@
|
||||
"""
|
||||
Tests for API key configuration and LLM execution.
|
||||
Tests that API keys can be set and used properly for different LLM providers.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
|
||||
from local_deep_research.config.llm_config import (
|
||||
get_llm,
|
||||
)
|
||||
from local_deep_research.llm.providers.implementations.openai import (
|
||||
OpenAIProvider,
|
||||
)
|
||||
from local_deep_research.llm.providers.implementations.anthropic import (
|
||||
AnthropicProvider,
|
||||
)
|
||||
from local_deep_research.llm.providers.implementations.custom_openai_endpoint import (
|
||||
CustomOpenAIEndpointProvider,
|
||||
)
|
||||
from local_deep_research.settings import SettingsManager
|
||||
|
||||
|
||||
def _llm_mock(**kwargs):
|
||||
"""Build a Mock that satisfies the BaseChatModel isinstance check.
|
||||
|
||||
The registered-LLM branch in get_llm() validates that create_llm()
|
||||
returned a real BaseChatModel; with spec=BaseChatModel the Mock
|
||||
passes isinstance() while still capturing call args.
|
||||
"""
|
||||
return Mock(spec=BaseChatModel, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _restore_auto_discovered_providers():
|
||||
"""Make sure auto-discovered providers are registered before each test.
|
||||
|
||||
The previous version of this fixture cleared the registry to force the
|
||||
procedural ``if/elif`` chain in ``get_llm`` — that chain has been
|
||||
deleted, so tests now go through the live registered-LLM path. ChatXxx
|
||||
patches in each test target the provider class module (e.g.
|
||||
``...implementations.openai.ChatOpenAI``) which is where the LLM is
|
||||
actually constructed.
|
||||
|
||||
``force_refresh=True`` is required because the discovery singleton
|
||||
short-circuits subsequent calls otherwise — and other test modules in
|
||||
the suite may have called ``clear_llm_registry()`` between tests.
|
||||
"""
|
||||
from local_deep_research.llm.providers import discover_providers
|
||||
|
||||
discover_providers(force_refresh=True)
|
||||
yield
|
||||
|
||||
|
||||
class TestAPIKeyConfiguration:
|
||||
"""Test API key configuration for different providers."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_session(self):
|
||||
"""Create a mock database session."""
|
||||
session = Mock()
|
||||
return session
|
||||
|
||||
@pytest.fixture
|
||||
def settings_manager(self, mock_db_session):
|
||||
"""Create a settings manager with mock session."""
|
||||
return SettingsManager(mock_db_session)
|
||||
|
||||
def _get_base_settings(self):
|
||||
"""Get base settings that all tests need."""
|
||||
return {
|
||||
"search.tool": "searxng",
|
||||
"llm.supports_max_tokens": True,
|
||||
"llm.max_tokens": 4096,
|
||||
"llm.context_window_unrestricted": False,
|
||||
"llm.context_window_size": 8192,
|
||||
"llm.local_context_window_size": 4096,
|
||||
"rate_limiting.llm_enabled": False,
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def settings_snapshot_with_openai(self):
|
||||
"""Create a settings snapshot with OpenAI API key."""
|
||||
settings = self._get_base_settings()
|
||||
settings.update(
|
||||
{
|
||||
"llm.provider": "openai",
|
||||
"llm.model": "gpt-4",
|
||||
"llm.temperature": 0.7,
|
||||
"llm.openai.api_key": "test-openai-api-key",
|
||||
}
|
||||
)
|
||||
return settings
|
||||
|
||||
@pytest.fixture
|
||||
def settings_snapshot_with_anthropic(self):
|
||||
"""Create a settings snapshot with Anthropic API key."""
|
||||
settings = self._get_base_settings()
|
||||
settings.update(
|
||||
{
|
||||
"llm.provider": "anthropic",
|
||||
"llm.model": "claude-3-opus-20240229",
|
||||
"llm.temperature": 0.7,
|
||||
"llm.anthropic.api_key": "test-anthropic-api-key",
|
||||
"llm.context_window_size": 200000,
|
||||
}
|
||||
)
|
||||
return settings
|
||||
|
||||
@pytest.fixture
|
||||
def settings_snapshot_with_openai_endpoint(self):
|
||||
"""Create a settings snapshot with OpenAI endpoint API key."""
|
||||
settings = self._get_base_settings()
|
||||
settings.update(
|
||||
{
|
||||
"llm.provider": "openai_endpoint",
|
||||
"llm.model": "claude-3-opus",
|
||||
"llm.temperature": 0.5,
|
||||
"llm.openai_endpoint.api_key": "test-openrouter-api-key",
|
||||
"llm.openai_endpoint.url": "https://openrouter.ai/api/v1",
|
||||
"llm.context_window_unrestricted": True,
|
||||
"llm.context_window_size": 128000,
|
||||
# "library" is the only engine the factory can instantiate
|
||||
# from this minimal snapshot (searxng et al. need an instance
|
||||
# URL), and the research path does create the engine. But
|
||||
# "library" is a PRIVATE engine, so under the default adaptive
|
||||
# egress scope it would resolve to PRIVATE_ONLY and force
|
||||
# local LLM — denying the remote openai_endpoint provider this
|
||||
# test configures. Pin the scope to "unprotected" (the escape
|
||||
# hatch; "both" is retired per ADR-0007) so the test stays
|
||||
# about LLM-provider config, not egress.
|
||||
"search.tool": "library",
|
||||
"policy.egress_scope": "unprotected",
|
||||
"search.max_results": 10,
|
||||
"search.cross_engine_max_results": 100,
|
||||
"search.cross_engine_use_reddit": False,
|
||||
"search.cross_engine_min_date": None,
|
||||
"search.region": "us",
|
||||
"search.time_period": "y",
|
||||
"search.safe_search": True,
|
||||
"search.snippets_only": True,
|
||||
"search.search_language": "English",
|
||||
"search.max_filtered_results": 20,
|
||||
"research.iterations": 2,
|
||||
"research.questions_per_iteration": 3,
|
||||
"research.local_context": 2000,
|
||||
"research.web_context": 2000,
|
||||
}
|
||||
)
|
||||
return settings
|
||||
|
||||
def test_openai_api_key_configuration(self, settings_snapshot_with_openai):
|
||||
"""Test that OpenAI API key can be configured and used."""
|
||||
with patch(
|
||||
"local_deep_research.llm.providers.implementations.openai.ChatOpenAI"
|
||||
) as mock_openai:
|
||||
# Create a mock LLM instance
|
||||
mock_llm_instance = _llm_mock()
|
||||
mock_openai.return_value = mock_llm_instance
|
||||
|
||||
# Get LLM with OpenAI settings
|
||||
get_llm(settings_snapshot=settings_snapshot_with_openai)
|
||||
|
||||
# Verify ChatOpenAI was called with correct parameters
|
||||
mock_openai.assert_called_once()
|
||||
call_args = mock_openai.call_args
|
||||
|
||||
assert call_args.kwargs["model"] == "gpt-4"
|
||||
assert call_args.kwargs["api_key"] == "test-openai-api-key"
|
||||
assert call_args.kwargs["temperature"] == 0.7
|
||||
assert "max_tokens" in call_args.kwargs
|
||||
|
||||
def test_anthropic_api_key_configuration(
|
||||
self, settings_snapshot_with_anthropic
|
||||
):
|
||||
"""Test that Anthropic API key can be configured and used."""
|
||||
with patch(
|
||||
"local_deep_research.llm.providers.implementations.anthropic.ChatAnthropic"
|
||||
) as mock_anthropic:
|
||||
# Create a mock LLM instance
|
||||
mock_llm_instance = _llm_mock()
|
||||
mock_anthropic.return_value = mock_llm_instance
|
||||
|
||||
# Get LLM with Anthropic settings
|
||||
get_llm(settings_snapshot=settings_snapshot_with_anthropic)
|
||||
|
||||
# Verify ChatAnthropic was called with correct parameters
|
||||
mock_anthropic.assert_called_once()
|
||||
call_args = mock_anthropic.call_args
|
||||
|
||||
assert call_args.kwargs["model"] == "claude-3-opus-20240229"
|
||||
assert (
|
||||
call_args.kwargs["anthropic_api_key"]
|
||||
== "test-anthropic-api-key"
|
||||
)
|
||||
assert call_args.kwargs["temperature"] == 0.7
|
||||
assert "max_tokens" in call_args.kwargs
|
||||
|
||||
def test_openai_endpoint_configuration(
|
||||
self, settings_snapshot_with_openai_endpoint
|
||||
):
|
||||
"""Test that OpenAI endpoint (OpenRouter) API key can be configured and used."""
|
||||
# Patch the openai_base SSRF guard to a passthrough so this config
|
||||
# test does not depend on live DNS resolution of the endpoint host
|
||||
# (matches tests/llm/test_provider_base_url_ssrf.py convention).
|
||||
with (
|
||||
patch(
|
||||
"local_deep_research.llm.providers.openai_base.ChatOpenAI"
|
||||
) as mock_openai,
|
||||
patch(
|
||||
"local_deep_research.llm.providers.openai_base.assert_base_url_safe",
|
||||
side_effect=lambda url, **_kwargs: url,
|
||||
),
|
||||
):
|
||||
# Create a mock LLM instance
|
||||
mock_llm_instance = _llm_mock()
|
||||
mock_openai.return_value = mock_llm_instance
|
||||
|
||||
# Get LLM with OpenAI endpoint settings
|
||||
get_llm(settings_snapshot=settings_snapshot_with_openai_endpoint)
|
||||
|
||||
# Verify ChatOpenAI was called with correct parameters for endpoint
|
||||
mock_openai.assert_called_once()
|
||||
call_args = mock_openai.call_args
|
||||
|
||||
assert call_args.kwargs["model"] == "claude-3-opus"
|
||||
assert call_args.kwargs["api_key"] == "test-openrouter-api-key"
|
||||
assert (
|
||||
call_args.kwargs["base_url"] == "https://openrouter.ai/api/v1"
|
||||
)
|
||||
assert call_args.kwargs["temperature"] == 0.5
|
||||
|
||||
def test_llm_execution_with_api_key(self, settings_snapshot_with_openai):
|
||||
"""Test that LLM can actually be invoked with API key."""
|
||||
with patch(
|
||||
"local_deep_research.llm.providers.implementations.openai.ChatOpenAI"
|
||||
) as mock_openai:
|
||||
# Create a mock LLM instance with invoke method
|
||||
mock_llm_instance = _llm_mock()
|
||||
mock_response = Mock()
|
||||
mock_response.content = "Test response from OpenAI"
|
||||
mock_llm_instance.invoke.return_value = mock_response
|
||||
mock_openai.return_value = mock_llm_instance
|
||||
|
||||
# Get LLM and test invocation
|
||||
llm = get_llm(settings_snapshot=settings_snapshot_with_openai)
|
||||
response = llm.invoke("Test prompt")
|
||||
|
||||
# Verify the LLM was invoked
|
||||
mock_llm_instance.invoke.assert_called_once_with("Test prompt")
|
||||
assert "Test response from OpenAI" in response.content
|
||||
|
||||
def test_multiple_provider_switching(
|
||||
self,
|
||||
settings_snapshot_with_openai,
|
||||
settings_snapshot_with_anthropic,
|
||||
settings_snapshot_with_openai_endpoint,
|
||||
):
|
||||
"""Test switching between different providers with their API keys."""
|
||||
# Test OpenAI
|
||||
with patch(
|
||||
"local_deep_research.llm.providers.implementations.openai.ChatOpenAI"
|
||||
) as mock_openai:
|
||||
mock_openai.return_value = _llm_mock()
|
||||
get_llm(settings_snapshot=settings_snapshot_with_openai)
|
||||
assert mock_openai.called
|
||||
assert (
|
||||
mock_openai.call_args.kwargs["api_key"] == "test-openai-api-key"
|
||||
)
|
||||
|
||||
# Test Anthropic
|
||||
with patch(
|
||||
"local_deep_research.llm.providers.implementations.anthropic.ChatAnthropic"
|
||||
) as mock_anthropic:
|
||||
mock_anthropic.return_value = _llm_mock()
|
||||
get_llm(settings_snapshot=settings_snapshot_with_anthropic)
|
||||
assert mock_anthropic.called
|
||||
assert (
|
||||
mock_anthropic.call_args.kwargs["anthropic_api_key"]
|
||||
== "test-anthropic-api-key"
|
||||
)
|
||||
|
||||
# Test OpenAI Endpoint
|
||||
# Patch the openai_base SSRF guard to a passthrough so this config
|
||||
# test does not depend on live DNS resolution of the endpoint host.
|
||||
with (
|
||||
patch(
|
||||
"local_deep_research.llm.providers.openai_base.ChatOpenAI"
|
||||
) as mock_openai_endpoint,
|
||||
patch(
|
||||
"local_deep_research.llm.providers.openai_base.assert_base_url_safe",
|
||||
side_effect=lambda url, **_kwargs: url,
|
||||
),
|
||||
):
|
||||
mock_openai_endpoint.return_value = _llm_mock()
|
||||
get_llm(settings_snapshot=settings_snapshot_with_openai_endpoint)
|
||||
assert mock_openai_endpoint.called
|
||||
assert (
|
||||
mock_openai_endpoint.call_args.kwargs["api_key"]
|
||||
== "test-openrouter-api-key"
|
||||
)
|
||||
assert (
|
||||
mock_openai_endpoint.call_args.kwargs["base_url"]
|
||||
== "https://openrouter.ai/api/v1"
|
||||
)
|
||||
|
||||
def test_research_with_api_configured_llm(
|
||||
self, settings_snapshot_with_openai_endpoint
|
||||
):
|
||||
"""Test that research can use LLM with configured API key."""
|
||||
from local_deep_research.api.research_functions import quick_summary
|
||||
|
||||
# Patch the openai_base SSRF guard to a passthrough so this config
|
||||
# test does not depend on live DNS resolution of the endpoint host.
|
||||
with (
|
||||
patch(
|
||||
"local_deep_research.llm.providers.openai_base.ChatOpenAI"
|
||||
) as mock_openai,
|
||||
patch(
|
||||
"local_deep_research.llm.providers.openai_base.assert_base_url_safe",
|
||||
side_effect=lambda url, **_kwargs: url,
|
||||
),
|
||||
):
|
||||
# Setup mock LLM
|
||||
mock_llm_instance = _llm_mock()
|
||||
mock_response = Mock()
|
||||
mock_response.content = "Research summary about test topic"
|
||||
mock_llm_instance.invoke.return_value = mock_response
|
||||
mock_openai.return_value = mock_llm_instance
|
||||
|
||||
# Mock the search system to avoid network calls
|
||||
with patch(
|
||||
"local_deep_research.api.research_functions.AdvancedSearchSystem"
|
||||
) as mock_search_system:
|
||||
mock_system_instance = Mock()
|
||||
mock_system_instance.analyze_topic.return_value = {
|
||||
"current_knowledge": "Research summary about test topic",
|
||||
"sources": ["https://example.com/test-topic"],
|
||||
"all_links_of_system": ["https://example.com/test-topic"],
|
||||
}
|
||||
mock_search_system.return_value = mock_system_instance
|
||||
|
||||
# Run research with API-configured LLM
|
||||
quick_summary(
|
||||
query="Test research query",
|
||||
settings_snapshot=settings_snapshot_with_openai_endpoint,
|
||||
)
|
||||
|
||||
# Verify LLM was created with API key
|
||||
assert mock_openai.called
|
||||
assert (
|
||||
mock_openai.call_args.kwargs["api_key"]
|
||||
== "test-openrouter-api-key"
|
||||
)
|
||||
|
||||
def test_api_availability_checks(self):
|
||||
"""Test the availability check functions for different providers."""
|
||||
# Test OpenAI availability with settings snapshot
|
||||
settings_with_openai = {
|
||||
"llm.openai.api_key": {"value": "test-openai-key", "type": "str"}
|
||||
}
|
||||
assert (
|
||||
OpenAIProvider.is_available(settings_snapshot=settings_with_openai)
|
||||
is True
|
||||
)
|
||||
|
||||
settings_without_openai = {
|
||||
"llm.openai.api_key": {"value": None, "type": "str"}
|
||||
}
|
||||
assert (
|
||||
OpenAIProvider.is_available(
|
||||
settings_snapshot=settings_without_openai
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
# Test Anthropic availability
|
||||
settings_with_anthropic = {
|
||||
"llm.anthropic.api_key": {
|
||||
"value": "test-anthropic-key",
|
||||
"type": "str",
|
||||
}
|
||||
}
|
||||
assert (
|
||||
AnthropicProvider.is_available(
|
||||
settings_snapshot=settings_with_anthropic
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
settings_without_anthropic = {
|
||||
"llm.anthropic.api_key": {"value": None, "type": "str"}
|
||||
}
|
||||
assert (
|
||||
AnthropicProvider.is_available(
|
||||
settings_snapshot=settings_without_anthropic
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
# Test OpenAI endpoint availability
|
||||
settings_with_endpoint = {
|
||||
"llm.openai_endpoint.api_key": {
|
||||
"value": "test-endpoint-key",
|
||||
"type": "str",
|
||||
}
|
||||
}
|
||||
assert (
|
||||
CustomOpenAIEndpointProvider.is_available(
|
||||
settings_snapshot=settings_with_endpoint
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
settings_without_endpoint = {
|
||||
"llm.openai_endpoint.api_key": {"value": None, "type": "str"}
|
||||
}
|
||||
assert (
|
||||
CustomOpenAIEndpointProvider.is_available(
|
||||
settings_snapshot=settings_without_endpoint
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
class TestLLMIntegration:
|
||||
"""Integration tests for LLM execution with real-like scenarios."""
|
||||
|
||||
def _get_base_settings(self):
|
||||
"""Get base settings that all tests need."""
|
||||
return {
|
||||
"search.tool": "searxng",
|
||||
"llm.supports_max_tokens": True,
|
||||
"llm.max_tokens": 4096,
|
||||
"llm.context_window_unrestricted": False,
|
||||
"llm.context_window_size": 8192,
|
||||
"llm.local_context_window_size": 4096,
|
||||
"rate_limiting.llm_enabled": False,
|
||||
}
|
||||
|
||||
def test_llm_with_token_counting(self):
|
||||
"""Test LLM execution with token counting enabled."""
|
||||
settings_snapshot = self._get_base_settings()
|
||||
settings_snapshot.update(
|
||||
{
|
||||
"llm.provider": "openai",
|
||||
"llm.model": "gpt-4",
|
||||
"llm.temperature": 0.7,
|
||||
"llm.openai.api_key": "test-openai-api-key",
|
||||
}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"local_deep_research.llm.providers.implementations.openai.ChatOpenAI"
|
||||
) as mock_openai:
|
||||
# Setup mock LLM with callbacks
|
||||
mock_llm_instance = _llm_mock()
|
||||
mock_llm_instance.callbacks = []
|
||||
mock_response = Mock()
|
||||
mock_response.content = "Test response"
|
||||
mock_llm_instance.invoke.return_value = mock_response
|
||||
mock_openai.return_value = mock_llm_instance
|
||||
|
||||
# Get LLM with research_id for token counting
|
||||
get_llm(
|
||||
settings_snapshot=settings_snapshot,
|
||||
research_id="test-research-123",
|
||||
research_context={"phase": "testing"},
|
||||
)
|
||||
|
||||
# Verify LLM was created with research_id
|
||||
assert mock_openai.called
|
||||
|
||||
def test_llm_error_handling(self):
|
||||
"""Test LLM error handling when API calls fail."""
|
||||
settings_snapshot = self._get_base_settings()
|
||||
settings_snapshot.update(
|
||||
{
|
||||
"llm.provider": "openai",
|
||||
"llm.model": "gpt-4",
|
||||
"llm.temperature": 0.7,
|
||||
"llm.openai.api_key": "test-openai-api-key",
|
||||
}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"local_deep_research.llm.providers.implementations.openai.ChatOpenAI"
|
||||
) as mock_openai:
|
||||
# Setup mock LLM that raises error
|
||||
mock_llm_instance = _llm_mock()
|
||||
mock_llm_instance.invoke.side_effect = Exception(
|
||||
"API rate limit exceeded"
|
||||
)
|
||||
mock_openai.return_value = mock_llm_instance
|
||||
|
||||
# Get LLM and test error handling
|
||||
llm = get_llm(settings_snapshot=settings_snapshot)
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
llm.invoke("Test prompt")
|
||||
|
||||
assert "API rate limit exceeded" in str(exc_info.value)
|
||||
|
||||
def test_custom_endpoint_url_configuration(self):
|
||||
"""Test configuring custom endpoint URLs for different providers."""
|
||||
settings_snapshot = self._get_base_settings()
|
||||
settings_snapshot.update(
|
||||
{
|
||||
"llm.provider": "openai_endpoint",
|
||||
"llm.model": "custom-model",
|
||||
"llm.temperature": 0.7,
|
||||
"llm.openai_endpoint.api_key": "test-key",
|
||||
"llm.openai_endpoint.url": "https://custom-llm-provider.com/v1",
|
||||
"llm.max_tokens": 2048,
|
||||
}
|
||||
)
|
||||
|
||||
# This test verifies config passthrough (the configured URL/api_key
|
||||
# reach the ChatOpenAI constructor), not SSRF enforcement. The
|
||||
# ``custom-llm-provider.com`` placeholder host does not resolve, so
|
||||
# the openai_base SSRF guard (assert_base_url_safe) would fail-closed
|
||||
# on its live DNS lookup. Patch the guard to a passthrough — the same
|
||||
# convention as tests/llm/test_provider_base_url_ssrf.py — so the
|
||||
# passthrough assertions stay deterministic and offline. SSRF
|
||||
# enforcement itself is covered by test_provider_base_url_ssrf.py.
|
||||
with (
|
||||
patch(
|
||||
"local_deep_research.llm.providers.openai_base.ChatOpenAI"
|
||||
) as mock_openai,
|
||||
patch(
|
||||
"local_deep_research.llm.providers.openai_base.assert_base_url_safe",
|
||||
side_effect=lambda url, **_kwargs: url,
|
||||
),
|
||||
):
|
||||
mock_openai.return_value = _llm_mock()
|
||||
|
||||
get_llm(settings_snapshot=settings_snapshot)
|
||||
|
||||
# Verify custom URL was used
|
||||
assert (
|
||||
mock_openai.call_args.kwargs["base_url"]
|
||||
== "https://custom-llm-provider.com/v1"
|
||||
)
|
||||
assert mock_openai.call_args.kwargs["api_key"] == "test-key"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user