Coverage for src/local_deep_research/llm/providers/_helpers.py: 100%
24 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-07-19 23:35 +0000
1"""Shared utilities for LLM provider construction.
3These helpers were previously inlined in ``config/llm_config.get_llm``'s
4procedural ``if/elif`` chain (which is now removed). They live here so
5provider-class ``create_llm`` methods are the single source of truth for
6LLM construction logic — no per-provider drift.
7"""
9DEFAULT_LOCAL_CONTEXT_WINDOW_SIZE = 8192
10DEFAULT_CLOUD_CONTEXT_WINDOW_SIZE = 128000
11LOCAL_PROVIDERS = ("ollama", "llamacpp", "lmstudio")
14def get_context_window_for_provider(provider_type, settings_snapshot=None):
15 """Resolve effective context window size for a provider.
17 Local providers (ollama, llamacpp, lmstudio) use a smaller default to
18 prevent memory issues. Cloud providers respect
19 ``llm.context_window_unrestricted`` and return ``None`` when
20 unrestricted (the provider auto-handles its own context window).
21 """
22 from ...config.thread_settings import get_setting_from_snapshot
24 if provider_type in LOCAL_PROVIDERS:
25 window_size = get_setting_from_snapshot(
26 "llm.local_context_window_size",
27 DEFAULT_LOCAL_CONTEXT_WINDOW_SIZE,
28 settings_snapshot=settings_snapshot,
29 )
30 return (
31 int(window_size)
32 if window_size is not None
33 else DEFAULT_LOCAL_CONTEXT_WINDOW_SIZE
34 )
35 use_unrestricted = get_setting_from_snapshot(
36 "llm.context_window_unrestricted",
37 True,
38 settings_snapshot=settings_snapshot,
39 )
40 if use_unrestricted:
41 return None
42 window_size = get_setting_from_snapshot(
43 "llm.context_window_size",
44 DEFAULT_CLOUD_CONTEXT_WINDOW_SIZE,
45 settings_snapshot=settings_snapshot,
46 )
47 return (
48 int(window_size)
49 if window_size is not None
50 else DEFAULT_CLOUD_CONTEXT_WINDOW_SIZE
51 )
54def compute_max_tokens(
55 settings_snapshot=None, context_window_size=None
56) -> int | None:
57 """Resolve effective max_tokens for ``ChatXxx(max_tokens=...)``.
59 Caps at 80% of ``context_window_size`` (when provided) to leave room
60 for the prompt. Returns ``None`` (caller should omit the kwarg) when:
62 - ``llm.supports_max_tokens`` is False, OR
63 - ``llm.max_tokens`` is unset / explicitly None in the snapshot.
65 Omitting the kwarg when the setting is absent matches the pre-refactor
66 live-class behavior (the provider SDK's own default applies); a
67 hardcoded fallback like the dead chain's 100000 exceeds the output
68 limit of most cloud models. Production users have ``llm.max_tokens``
69 populated from ``default_settings.json`` (currently 30000), so the
70 unset branch only fires for partial-snapshot programmatic callers.
72 Raises:
73 NoSettingsContextError: when ``llm.max_tokens`` is absent from the
74 provided snapshot (or no snapshot is given) and no thread
75 settings context is available. A snapshot being present does
76 NOT prevent the raise — callers must wrap construction in
77 ``except NoSettingsContextError`` and omit the kwarg.
78 """
79 from ...config.thread_settings import get_setting_from_snapshot
81 if not get_setting_from_snapshot(
82 "llm.supports_max_tokens",
83 True,
84 settings_snapshot=settings_snapshot,
85 ):
86 return None
87 raw = get_setting_from_snapshot(
88 "llm.max_tokens",
89 None,
90 settings_snapshot=settings_snapshot,
91 )
92 if raw is None:
93 return None
94 max_tokens = int(raw)
95 if context_window_size is not None:
96 max_tokens = min(max_tokens, int(context_window_size * 0.8))
97 return max_tokens