Coverage for src/local_deep_research/llm/providers/implementations/llamacpp.py: 100%
40 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
1"""llama.cpp LLM provider for Local Deep Research.
3Talks to llama.cpp's OpenAI-compatible HTTP server (`llama-server`) instead
4of loading models in-process via `llama-cpp-python`. Modeled after
5`LMStudioProvider`. For setups that need API key auth or non-default URLs
6beyond a single endpoint, use the `openai_endpoint` provider directly.
7"""
9from ....config.constants import DEFAULT_LLAMACPP_URL
10from ....settings.exceptions import SettingsValueError
11from ....utilities.url_utils import normalize_url
12from ..base import Exposure
13from ..openai_base import OpenAICompatibleProvider
16class LlamaCppProvider(OpenAICompatibleProvider):
17 """llama.cpp provider using its OpenAI-compatible HTTP endpoint.
19 Run `llama-server -m <model.gguf>` (port 8080 by default) and point
20 `llm.llamacpp.url` at the server's `/v1` endpoint.
21 """
23 provider_name = "llama.cpp"
24 # llama-server HAS an API key concept (for setups behind an auth proxy);
25 # api_key_optional makes the base resolver fall back to a placeholder
26 # when no key is set, instead of raising.
27 api_key_setting = "llm.llamacpp.api_key"
28 api_key_optional = True
29 url_setting = "llm.llamacpp.url" # type: ignore[assignment]
30 default_base_url = DEFAULT_LLAMACPP_URL
31 default_model = "" # User must specify the model loaded by llama-server
33 # Metadata for auto-discovery
34 provider_key = "LLAMACPP"
35 company_name = "llama.cpp"
36 is_cloud = False # Local provider
37 # Egress exposure (ADR-0007): local inference sink — data stays on the box.
38 egress_exposure = Exposure.CONTAINED
40 @classmethod
41 def create_llm(cls, model_name=None, temperature=0.7, **kwargs):
42 """Create a ChatOpenAI client pointed at llama-server."""
43 from ....config.thread_settings import get_setting_from_snapshot
45 settings_snapshot = kwargs.get("settings_snapshot")
47 url = get_setting_from_snapshot(
48 "llm.llamacpp.url",
49 cls.default_base_url,
50 settings_snapshot=settings_snapshot,
51 )
53 kwargs["base_url"] = normalize_url(url)
54 # Real key when configured (llama-server behind an auth proxy),
55 # otherwise the unified placeholder; a no-auth llama-server
56 # ignores it.
57 kwargs["api_key"] = cls.resolve_api_key_or_placeholder(
58 settings_snapshot
59 ) # gitleaks:allow
61 return super()._create_llm_instance(model_name, temperature, **kwargs)
63 @classmethod
64 def is_available(cls, settings_snapshot=None):
65 """Check whether llama-server is reachable.
67 Sends ``Authorization: Bearer`` when an API key is configured so
68 llama-server instances behind an auth proxy are correctly detected
69 as available. Empty key → no auth header → unauthenticated installs
70 still work. Mirrors the LMStudio pattern at lmstudio.py:_get_auth_headers.
71 """
72 try:
73 from ....config.thread_settings import get_setting_from_snapshot
74 from ....security import safe_get
76 url = get_setting_from_snapshot(
77 "llm.llamacpp.url",
78 cls.default_base_url,
79 settings_snapshot=settings_snapshot,
80 )
81 base_url = normalize_url(url)
82 response = safe_get(
83 f"{base_url}/models",
84 timeout=1,
85 headers=cls.build_bearer_header(
86 settings_snapshot=settings_snapshot
87 ),
88 allow_localhost=True,
89 allow_private_ips=True,
90 )
91 return response.status_code == 200
92 except SettingsValueError:
93 raise
94 except Exception:
95 return False
97 @classmethod
98 def requires_auth_for_models(cls):
99 """llama-server doesn't require authentication for listing models."""
100 return False