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
350 lines
13 KiB
Python
350 lines
13 KiB
Python
"""Deep coverage tests for llm_config.py targeting uncovered branches.
|
||
|
||
Focuses on:
|
||
- get_llm() with registered custom LLMs (callable factory and BaseChatModel instance)
|
||
- get_llm() with invalid provider
|
||
- ProcessingLLMWrapper.close()
|
||
- _get_context_window_for_provider() with None window_size from settings
|
||
"""
|
||
|
||
from unittest.mock import MagicMock, patch
|
||
|
||
import pytest
|
||
from langchain_core.language_models import BaseChatModel
|
||
|
||
MODULE = "local_deep_research.config.llm_config"
|
||
# Context-window resolution delegates to the _helpers twin, which reads
|
||
# settings via thread_settings.get_setting_from_snapshot. Patch this module
|
||
# (not MODULE) for context-window tests.
|
||
THREAD_SETTINGS = "local_deep_research.config.thread_settings"
|
||
|
||
|
||
def _make_settings_snapshot(provider="openai", model="gpt-4", **extra):
|
||
snap = {
|
||
"llm.provider": provider,
|
||
"llm.model": model,
|
||
"llm.temperature": 0.7,
|
||
"llm.max_tokens": 4096,
|
||
"llm.supports_max_tokens": True,
|
||
"llm.context_window_unrestricted": True,
|
||
"rate_limiting.llm_enabled": False,
|
||
}
|
||
snap.update(extra)
|
||
return snap
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# get_llm with registered custom LLM – callable factory
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestGetLlmRegisteredFactory:
|
||
def test_callable_factory_invoked(self):
|
||
mock_instance = MagicMock(spec=BaseChatModel)
|
||
mock_factory = MagicMock(return_value=mock_instance)
|
||
|
||
# Make isinstance(mock_factory, BaseChatModel) return False
|
||
# mock_factory is already a MagicMock (not BaseChatModel)
|
||
|
||
with (
|
||
patch(f"{MODULE}.is_llm_registered", return_value=True),
|
||
patch(f"{MODULE}.get_llm_from_registry", return_value=mock_factory),
|
||
patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
|
||
patch(f"{MODULE}.wrap_llm_without_think_tags") as mock_wrap,
|
||
):
|
||
mock_setting.side_effect = (
|
||
lambda key, default=None, settings_snapshot=None: {
|
||
"llm.model": "gpt-4",
|
||
"llm.temperature": 0.7,
|
||
"llm.provider": "my_custom",
|
||
"rate_limiting.llm_enabled": False,
|
||
}.get(key, default)
|
||
)
|
||
|
||
from local_deep_research.config.llm_config import get_llm
|
||
|
||
get_llm(
|
||
provider="my_custom",
|
||
settings_snapshot={"search.tool": "searxng"},
|
||
)
|
||
|
||
mock_factory.assert_called_once()
|
||
mock_wrap.assert_called_once()
|
||
|
||
def test_factory_returning_non_basechatmodel_raises(self):
|
||
mock_factory = MagicMock(return_value="not_a_model")
|
||
|
||
with (
|
||
patch(f"{MODULE}.is_llm_registered", return_value=True),
|
||
patch(f"{MODULE}.get_llm_from_registry", return_value=mock_factory),
|
||
patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
|
||
):
|
||
mock_setting.side_effect = (
|
||
lambda key, default=None, settings_snapshot=None: {
|
||
"llm.model": "gpt-4",
|
||
"llm.temperature": 0.7,
|
||
"llm.provider": "my_custom",
|
||
}.get(key, default)
|
||
)
|
||
|
||
from local_deep_research.config.llm_config import get_llm
|
||
|
||
with pytest.raises(ValueError, match="must return a BaseChatModel"):
|
||
get_llm(
|
||
provider="my_custom",
|
||
settings_snapshot={"search.tool": "searxng"},
|
||
)
|
||
|
||
def test_factory_with_bad_signature_raises_type_error(self):
|
||
def bad_factory():
|
||
return MagicMock(spec=BaseChatModel)
|
||
|
||
with (
|
||
patch(f"{MODULE}.is_llm_registered", return_value=True),
|
||
patch(f"{MODULE}.get_llm_from_registry", return_value=bad_factory),
|
||
patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
|
||
):
|
||
mock_setting.side_effect = (
|
||
lambda key, default=None, settings_snapshot=None: {
|
||
"llm.model": "gpt-4",
|
||
"llm.temperature": 0.7,
|
||
"llm.provider": "bad_factory",
|
||
}.get(key, default)
|
||
)
|
||
|
||
from local_deep_research.config.llm_config import get_llm
|
||
|
||
with pytest.raises(TypeError, match="invalid signature"):
|
||
get_llm(
|
||
provider="bad_factory",
|
||
settings_snapshot={"search.tool": "searxng"},
|
||
)
|
||
|
||
def test_registered_basechatmodel_instance_used_directly(self):
|
||
mock_instance = MagicMock(spec=BaseChatModel)
|
||
|
||
with (
|
||
patch(f"{MODULE}.is_llm_registered", return_value=True),
|
||
patch(
|
||
f"{MODULE}.get_llm_from_registry", return_value=mock_instance
|
||
),
|
||
patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
|
||
patch(f"{MODULE}.wrap_llm_without_think_tags") as mock_wrap,
|
||
):
|
||
mock_setting.side_effect = (
|
||
lambda key, default=None, settings_snapshot=None: {
|
||
"llm.model": "gpt-4",
|
||
"llm.temperature": 0.7,
|
||
"llm.provider": "my_instance",
|
||
}.get(key, default)
|
||
)
|
||
|
||
from local_deep_research.config.llm_config import get_llm
|
||
|
||
get_llm(
|
||
provider="my_instance",
|
||
settings_snapshot={"search.tool": "searxng"},
|
||
)
|
||
|
||
# The factory path is skipped for BaseChatModel instances
|
||
mock_wrap.assert_called_once()
|
||
|
||
def test_registered_invalid_type_raises_value_error(self):
|
||
"""Registered object that is not BaseChatModel and not callable raises ValueError."""
|
||
|
||
class WeirdThing:
|
||
pass
|
||
|
||
with (
|
||
patch(f"{MODULE}.is_llm_registered", return_value=True),
|
||
patch(f"{MODULE}.get_llm_from_registry", return_value=WeirdThing()),
|
||
patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
|
||
):
|
||
mock_setting.side_effect = (
|
||
lambda key, default=None, settings_snapshot=None: {
|
||
"llm.model": "gpt-4",
|
||
"llm.temperature": 0.7,
|
||
"llm.provider": "weird",
|
||
}.get(key, default)
|
||
)
|
||
|
||
from local_deep_research.config.llm_config import get_llm
|
||
|
||
with pytest.raises(
|
||
ValueError, match="must be either a BaseChatModel"
|
||
):
|
||
get_llm(
|
||
provider="weird",
|
||
settings_snapshot={"search.tool": "searxng"},
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# get_llm with invalid provider
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestGetLlmInvalidProvider:
|
||
def test_invalid_provider_raises_value_error(self):
|
||
with (
|
||
patch(f"{MODULE}.is_llm_registered", return_value=False),
|
||
patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
|
||
):
|
||
mock_setting.side_effect = (
|
||
lambda key, default=None, settings_snapshot=None: {
|
||
"llm.model": "gpt-4",
|
||
"llm.temperature": 0.7,
|
||
"llm.provider": "bogus_provider",
|
||
"llm.supports_max_tokens": False,
|
||
"llm.context_window_unrestricted": True,
|
||
"rate_limiting.llm_enabled": False,
|
||
}.get(key, default)
|
||
)
|
||
|
||
from local_deep_research.config.llm_config import get_llm
|
||
|
||
with pytest.raises(ValueError, match="Invalid provider"):
|
||
get_llm(
|
||
provider="bogus_provider",
|
||
settings_snapshot={"search.tool": "searxng"},
|
||
)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# _get_context_window_for_provider edge cases
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestContextWindowEdgeCases:
|
||
def test_none_window_size_for_local_provider_defaults_to_8192(self):
|
||
"""If local_context_window_size returns None, defaults to 8192."""
|
||
with patch(
|
||
f"{THREAD_SETTINGS}.get_setting_from_snapshot",
|
||
return_value=None,
|
||
):
|
||
from local_deep_research.config.llm_config import (
|
||
_get_context_window_for_provider,
|
||
)
|
||
|
||
result = _get_context_window_for_provider("ollama")
|
||
assert result == 8192
|
||
|
||
def test_none_window_size_for_cloud_restricted_defaults_to_128000(self):
|
||
"""Restricted cloud: if context_window_size returns None, defaults to 128000."""
|
||
call_num = [0]
|
||
|
||
def fake_setting(key, default=None, settings_snapshot=None):
|
||
call_num[0] += 1
|
||
if key == "llm.context_window_unrestricted":
|
||
return False
|
||
if key == "llm.context_window_size":
|
||
return None
|
||
return default
|
||
|
||
with patch(
|
||
f"{THREAD_SETTINGS}.get_setting_from_snapshot",
|
||
side_effect=fake_setting,
|
||
):
|
||
from local_deep_research.config.llm_config import (
|
||
_get_context_window_for_provider,
|
||
)
|
||
|
||
result = _get_context_window_for_provider("openai")
|
||
assert result == 128000
|
||
|
||
def test_llamacpp_is_local_provider(self):
|
||
"""llamacpp is in the local provider list."""
|
||
with patch(
|
||
f"{THREAD_SETTINGS}.get_setting_from_snapshot",
|
||
return_value=8192,
|
||
):
|
||
from local_deep_research.config.llm_config import (
|
||
_get_context_window_for_provider,
|
||
)
|
||
|
||
result = _get_context_window_for_provider("llamacpp")
|
||
assert result == 8192
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# ProcessingLLMWrapper.close()
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestProcessingLLMWrapperClose:
|
||
def _make_wrapper(self, mock_llm):
|
||
with patch(f"{MODULE}.get_setting_from_snapshot", return_value=False):
|
||
from local_deep_research.config.llm_config import (
|
||
wrap_llm_without_think_tags,
|
||
)
|
||
|
||
return wrap_llm_without_think_tags(mock_llm)
|
||
|
||
def test_close_called_without_error(self):
|
||
mock_llm = MagicMock()
|
||
wrapper = self._make_wrapper(mock_llm)
|
||
with patch(
|
||
"local_deep_research.utilities.llm_utils._close_base_llm"
|
||
) as mock_close:
|
||
wrapper.close()
|
||
mock_close.assert_called_once_with(mock_llm)
|
||
|
||
def test_close_swallows_exceptions(self):
|
||
mock_llm = MagicMock()
|
||
wrapper = self._make_wrapper(mock_llm)
|
||
with patch(
|
||
"local_deep_research.utilities.llm_utils._close_base_llm",
|
||
side_effect=RuntimeError("close failed"),
|
||
):
|
||
# Should not raise
|
||
wrapper.close()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# get_llm model/provider name cleaning
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class TestGetLlmNameCleaning:
|
||
def test_model_name_stripped_and_unquoted(self):
|
||
"""Model name with surrounding quotes/whitespace is cleaned."""
|
||
captured = {}
|
||
|
||
def fake_setting(key, default=None, settings_snapshot=None):
|
||
return {
|
||
"llm.model": ' "gpt-4" ',
|
||
"llm.temperature": 0.7,
|
||
"llm.provider": "openai",
|
||
"llm.openai.api_key": "sk-test",
|
||
"llm.supports_max_tokens": False,
|
||
"llm.context_window_unrestricted": True,
|
||
"rate_limiting.llm_enabled": False,
|
||
}.get(key, default)
|
||
|
||
with (
|
||
patch(f"{MODULE}.is_llm_registered", return_value=False),
|
||
patch(
|
||
f"{MODULE}.get_setting_from_snapshot", side_effect=fake_setting
|
||
),
|
||
patch(
|
||
"langchain_openai.ChatOpenAI.__init__", return_value=None
|
||
) as mock_init,
|
||
patch(
|
||
f"{MODULE}.wrap_llm_without_think_tags",
|
||
return_value=MagicMock(),
|
||
),
|
||
):
|
||
try:
|
||
from local_deep_research.config.llm_config import get_llm
|
||
|
||
get_llm(settings_snapshot={"search.tool": "searxng"})
|
||
if mock_init.call_args:
|
||
captured["model"] = mock_init.call_args.kwargs.get(
|
||
"model",
|
||
mock_init.call_args.args[0]
|
||
if mock_init.call_args.args
|
||
else None,
|
||
)
|
||
except Exception:
|
||
pass # ChatOpenAI init may fail, we just care about name cleaning
|