Files
learningcircuit--local-deep…/tests/config/test_get_llm_dispatch_integration.py
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

337 lines
13 KiB
Python

"""Integration tests for get_llm() dispatch through the provider registry.
PR #3984 removed the procedural if/elif provider chain in get_llm(); the
registry path (auto-discovery -> provider class create_llm) is now the only
construction path. These tests pin the full dispatch chain end to end:
get_llm(provider=..., settings_snapshot=...)
-> _llm_registry (populated by discover_providers() at import)
-> <Provider>.create_llm
-> langchain client constructor (patched at the provider module)
Only the langchain client class itself is patched — settings resolution,
registry dispatch, URL handling, key resolution, and the max_tokens cap all
execute real production code.
"""
import pytest
from unittest.mock import patch
from langchain_core.language_models.fake_chat_models import FakeListChatModel
from local_deep_research.config.llm_config import get_llm
from local_deep_research.llm import is_llm_registered
from local_deep_research.llm.providers import get_discovered_provider_options
from local_deep_research.llm.providers.base import normalize_provider
OPENAI_CHAT = (
"local_deep_research.llm.providers.implementations.openai.ChatOpenAI"
)
ANTHROPIC_CHAT = (
"local_deep_research.llm.providers.implementations.anthropic.ChatAnthropic"
)
OLLAMA_CHAT = (
"local_deep_research.llm.providers.implementations.ollama.ChatOllama"
)
# LM Studio and llama.cpp construct their client in the shared parent
# (OpenAICompatibleProvider._create_llm_instance), so the patch point is
# openai_base, not the implementation module.
OPENAI_BASE_CHAT = "local_deep_research.llm.providers.openai_base.ChatOpenAI"
@pytest.fixture(autouse=True, scope="module")
def _ensure_providers_registered():
"""Re-run auto-discovery in case an earlier test cleared the registry."""
from local_deep_research.llm.providers import discover_providers
discover_providers(force_refresh=True)
def _snapshot(**overrides):
"""Minimal permissive settings snapshot for the live dispatch path."""
snap = {
"llm.supports_max_tokens": True,
"llm.max_tokens": 4096,
"rate_limiting.llm_enabled": False,
"search.tool": "searxng",
}
snap.update(overrides)
return snap
def _fake_llm():
return FakeListChatModel(responses=["ok"])
class TestRegistryPopulation:
"""Every built-in provider must be reachable through the registry,
since get_llm() no longer has any per-provider construction code."""
def test_all_discovered_providers_are_registered(self):
discovered = [
normalize_provider(option["value"])
for option in get_discovered_provider_options()
]
unregistered = [p for p in discovered if not is_llm_registered(p)]
assert unregistered == [], (
f"Providers missing from the LLM registry: {unregistered}. "
"get_llm() has no fallback construction path for these anymore."
)
class TestCloudProviderDispatch:
def test_openai_dispatches_through_provider_class(self):
snapshot = _snapshot(**{"llm.openai.api_key": "sk-test-key"})
fake = _fake_llm()
with patch(OPENAI_CHAT, return_value=fake) as mock_chat:
result = get_llm(
provider="openai",
model_name="gpt-4o-mini",
temperature=0.5,
settings_snapshot=snapshot,
)
kwargs = mock_chat.call_args.kwargs
assert kwargs["model"] == "gpt-4o-mini"
assert kwargs["api_key"] == "sk-test-key"
assert kwargs["temperature"] == 0.5
assert result.base_llm is fake
def test_anthropic_dispatches_through_provider_class(self):
snapshot = _snapshot(**{"llm.anthropic.api_key": "sk-ant-test"})
fake = _fake_llm()
with patch(ANTHROPIC_CHAT, return_value=fake) as mock_chat:
result = get_llm(
provider="anthropic",
model_name="claude-sonnet-4-5",
settings_snapshot=snapshot,
)
kwargs = mock_chat.call_args.kwargs
assert kwargs["model"] == "claude-sonnet-4-5"
assert kwargs["anthropic_api_key"] == "sk-ant-test"
assert result.base_llm is fake
def test_anthropic_missing_key_raises_value_error(self):
"""Required-key semantics survive on the live path: no placeholder."""
with pytest.raises(ValueError, match="API key not configured"):
get_llm(
provider="anthropic",
model_name="claude-sonnet-4-5",
settings_snapshot=_snapshot(),
)
class TestLocalProviderDispatch:
def test_lmstudio_appends_v1_suffix(self):
"""#4532: a URL without /v1 gets the suffix on the live path."""
snapshot = _snapshot(**{"llm.lmstudio.url": "http://localhost:1234"})
with patch(OPENAI_BASE_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="lmstudio",
model_name="qwen2.5-7b",
settings_snapshot=snapshot,
)
kwargs = mock_chat.call_args.kwargs
assert kwargs["base_url"] == "http://localhost:1234/v1"
assert kwargs["api_key"] == "not-required"
def test_lmstudio_does_not_double_v1_suffix(self):
snapshot = _snapshot(**{"llm.lmstudio.url": "http://localhost:1234/v1"})
with patch(OPENAI_BASE_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="lmstudio",
model_name="qwen2.5-7b",
settings_snapshot=snapshot,
)
assert (
mock_chat.call_args.kwargs["base_url"] == "http://localhost:1234/v1"
)
def test_lmstudio_real_api_key_passed_through(self):
snapshot = _snapshot(
**{
"llm.lmstudio.url": "http://localhost:1234/v1",
"llm.lmstudio.api_key": "lms-real-key",
}
)
with patch(OPENAI_BASE_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="lmstudio",
model_name="qwen2.5-7b",
settings_snapshot=snapshot,
)
assert mock_chat.call_args.kwargs["api_key"] == "lms-real-key"
def test_llamacpp_uses_placeholder_key_and_verbatim_url(self):
"""llama.cpp does NOT force /v1 — the user-provided URL is used as-is."""
snapshot = _snapshot(**{"llm.llamacpp.url": "http://localhost:8080"})
with patch(OPENAI_BASE_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="llamacpp",
model_name="llama-3.1-8b",
settings_snapshot=snapshot,
)
kwargs = mock_chat.call_args.kwargs
assert kwargs["base_url"] == "http://localhost:8080"
assert kwargs["api_key"] == "not-required"
def test_ollama_enable_thinking_false_reaches_chatollama(self):
"""#3984's headline bug fix: llm.ollama.enable_thinking was silently
ignored on the live path before this PR."""
snapshot = _snapshot(
**{
"llm.ollama.url": "http://localhost:11434",
"llm.ollama.enable_thinking": False,
}
)
fake = _fake_llm()
with patch(OLLAMA_CHAT, return_value=fake) as mock_chat:
result = get_llm(
provider="ollama",
model_name="deepseek-r1:14b",
settings_snapshot=snapshot,
)
kwargs = mock_chat.call_args.kwargs
assert kwargs["reasoning"] is False
assert kwargs["model"] == "deepseek-r1:14b"
assert result.base_llm is fake
def test_ollama_enable_thinking_default_true(self):
snapshot = _snapshot(**{"llm.ollama.url": "http://localhost:11434"})
with patch(OLLAMA_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="ollama",
model_name="deepseek-r1:14b",
settings_snapshot=snapshot,
)
assert mock_chat.call_args.kwargs["reasoning"] is True
class TestMaxTokensCap:
"""The 80%-of-context-window cap previously lived only in dead code;
these pin it on the live path end to end through get_llm()."""
def test_cloud_max_tokens_capped_at_80_percent_of_window(self):
snapshot = _snapshot(
**{
"llm.openai.api_key": "sk-test-key",
"llm.context_window_unrestricted": False,
"llm.context_window_size": 10000,
"llm.max_tokens": 200000,
}
)
with patch(OPENAI_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="openai",
model_name="gpt-4o-mini",
settings_snapshot=snapshot,
)
assert mock_chat.call_args.kwargs["max_tokens"] == 8000
def test_cloud_unrestricted_window_passes_max_tokens_uncapped(self):
snapshot = _snapshot(
**{
"llm.openai.api_key": "sk-test-key",
"llm.context_window_unrestricted": True,
"llm.max_tokens": 200000,
}
)
with patch(OPENAI_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="openai",
model_name="gpt-4o-mini",
settings_snapshot=snapshot,
)
assert mock_chat.call_args.kwargs["max_tokens"] == 200000
def test_ollama_num_ctx_from_shared_helper_no_max_tokens(self):
"""Ollama's window resolves through the shared helper (same source
as context_limit bookkeeping); no max_tokens kwarg is passed since
ChatOllama silently ignores it."""
snapshot = _snapshot(
**{
"llm.ollama.url": "http://localhost:11434",
"llm.local_context_window_size": 4096,
"llm.max_tokens": 200000,
}
)
with patch(OLLAMA_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="ollama",
model_name="llama3.1:8b",
settings_snapshot=snapshot,
)
kwargs = mock_chat.call_args.kwargs
assert kwargs["num_ctx"] == 4096
assert "max_tokens" not in kwargs
def test_lmstudio_capped_by_local_window_not_cloud_window(self):
"""LM Studio resolves its window via provider_key through the
LOCAL_PROVIDERS branch — the cloud llm.context_window_size must
play no role."""
snapshot = _snapshot(
**{
"llm.lmstudio.url": "http://localhost:1234/v1",
"llm.context_window_unrestricted": False,
"llm.context_window_size": 128000,
"llm.local_context_window_size": 4096,
"llm.max_tokens": 200000,
}
)
with patch(OPENAI_BASE_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="lmstudio",
model_name="qwen2.5-7b",
settings_snapshot=snapshot,
)
assert mock_chat.call_args.kwargs["max_tokens"] == int(4096 * 0.8)
def test_unset_max_tokens_omits_kwarg(self):
"""Partial snapshots without llm.max_tokens must not inject a
hardcoded default — the provider SDK's own default applies."""
snapshot = _snapshot(**{"llm.openai.api_key": "sk-test-key"})
del snapshot["llm.max_tokens"]
with patch(OPENAI_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="openai",
model_name="gpt-4o-mini",
settings_snapshot=snapshot,
)
assert "max_tokens" not in mock_chat.call_args.kwargs
def test_supports_max_tokens_false_omits_kwarg(self):
snapshot = _snapshot(
**{
"llm.openai.api_key": "sk-test-key",
"llm.supports_max_tokens": False,
}
)
with patch(OPENAI_CHAT, return_value=_fake_llm()) as mock_chat:
get_llm(
provider="openai",
model_name="gpt-4o-mini",
settings_snapshot=snapshot,
)
assert "max_tokens" not in mock_chat.call_args.kwargs
class TestContextLimitBookkeeping:
def test_context_limit_set_in_research_context(self):
"""Overflow detection relies on context_limit being populated even
though the registered path returns before get_llm's own bookkeeping."""
snapshot = _snapshot(
**{
"llm.openai.api_key": "sk-test-key",
"llm.context_window_unrestricted": False,
"llm.context_window_size": 10000,
}
)
research_context = {}
with patch(OPENAI_CHAT, return_value=_fake_llm()):
get_llm(
provider="openai",
model_name="gpt-4o-mini",
settings_snapshot=snapshot,
research_context=research_context,
)
assert research_context["context_limit"] == 10000