Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/modeldock/adapters/runtimes/gpt4all.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
"""GPT4All runtime adapter (planned). Implements RuntimePort; not yet shipped."""
"""GPT4All local model-directory discovery.

The adapter deliberately stops at discovery for issue #30. It does not download
or load models until the remaining GPT4All runtime work is implemented.
"""

from __future__ import annotations

from pathlib import Path
from typing import Any, List

from modeldock.adapters.runtimes.base import BaseRuntime
Expand All @@ -11,15 +16,25 @@


class Gpt4AllRuntime(BaseRuntime):
Comment thread
Deepnar marked this conversation as resolved.
"""Planned runtime adapter for GPT4All."""
"""Discover models already present in a GPT4All models directory."""

backend: RuntimeBackend = RuntimeBackend.GPT4ALL

def __init__(self, models_dir: Path | None = None) -> None:
super().__init__()
self._models_dir = models_dir or Path.home() / ".cache" / "gpt4all"

def _check_available(self) -> bool:
return False
return self._models_dir.is_dir()

def list_installed(self) -> List[ModelRef]:
raise RuntimeUnavailableError("gpt4all", hint="Adapter planned, not shipped.")
if not self._models_dir.is_dir():
return []
refs: List[ModelRef] = []
for path in sorted(self._models_dir.iterdir(), key=lambda path: path.name):
if path.is_file() and path.suffix.lower() in {".gguf", ".bin"}:
refs.append(ModelRef.parse(path.stem))
return refs

def _do_pull(self, ref: ModelRef, progress: Any) -> PullResult:
raise RuntimeUnavailableError("gpt4all", hint="Adapter planned, not shipped.")
Expand Down
4 changes: 4 additions & 0 deletions src/modeldock/adapters/runtimes/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from __future__ import annotations

from pathlib import Path
from typing import Any, Callable, Dict, List, cast

from modeldock.common.logging import get_logger
Expand Down Expand Up @@ -66,6 +67,7 @@ def get(
backend: RuntimeBackend,
host: str | None = None,
gpu_layers: int | None = None,
models_dir: Path | None = None,
) -> RuntimePort:
"""Return a runtime instance for backend (entry points win).

Expand All @@ -88,6 +90,8 @@ def get(
clear()
if gpu_layers is not None and hasattr(runtime, "_gpu_layers"):
runtime._gpu_layers = gpu_layers
if models_dir is not None and hasattr(runtime, "_models_dir"):
runtime._models_dir = models_dir
return runtime

@staticmethod
Expand Down
9 changes: 9 additions & 0 deletions src/modeldock/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Settings(BaseModel):
ollama_host: Optional[str] = None
lmstudio_host: Optional[str] = None
llamacpp_gpu_layers: Optional[int] = None
gpt4all_models_dir: Optional[Path] = None
config_path: Optional[Path] = None

@field_validator("log_level")
Expand Down Expand Up @@ -103,6 +104,9 @@ def to_env_overrides(self) -> Dict[str, str]:
f"{_ENV_PREFIX}CATALOG_SOURCE": self.catalog_source,
f"{_ENV_PREFIX}AUTO_INSTALL": str(self.auto_install).lower(),
f"{_ENV_PREFIX}CACHE_DIR": str(self.cache_dir),
f"{_ENV_PREFIX}GPT4ALL_MODELS_DIR": ""
if self.gpt4all_models_dir is None
else str(self.gpt4all_models_dir),
}


Expand Down Expand Up @@ -181,6 +185,10 @@ def _apply_mapping(settings: Settings, data: Dict[str, Any], source: str = "conf
if "llamacpp_gpu_layers" in data:
raw = data["llamacpp_gpu_layers"]
_safe_set(settings, "llamacpp_gpu_layers", raw if raw not in (None, "") else None, source)
if "gpt4all_models_dir" in data:
raw = data["gpt4all_models_dir"]
resolved_path = Path(str(raw)) if raw not in (None, "") else None
_safe_set(settings, "gpt4all_models_dir", resolved_path, source)


def load_settings(
Comment thread
Deepnar marked this conversation as resolved.
Expand Down Expand Up @@ -228,6 +236,7 @@ def load_settings(
f"{_ENV_PREFIX}OLLAMA_HOST": "ollama_host",
f"{_ENV_PREFIX}LMSTUDIO_HOST": "lmstudio_host",
f"{_ENV_PREFIX}LLAMACPP_GPU_LAYERS": "llamacpp_gpu_layers",
f"{_ENV_PREFIX}GPT4ALL_MODELS_DIR": "gpt4all_models_dir",
}
env_data = {
field_name: os.environ[env_key]
Expand Down
14 changes: 13 additions & 1 deletion src/modeldock/core/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ def _resolve_backend_catalog(self, cfg: Settings) -> Optional[RegistryPort]:
RuntimeBackend.LLAMACPP: "llamacpp_gpu_layers",
}

#: Config field holding the local model directory for file-based runtimes.
_MODELS_DIR_SETTING_FOR = {
RuntimeBackend.GPT4ALL: "gpt4all_models_dir",
}

def _resolve_runtime(self, backend: RuntimeBackend, cfg: Settings) -> RuntimePort:
# Apply the configured host at construction time so it always takes
# effect (clients are built lazily and cached per instance). Backends
Expand All @@ -173,8 +178,15 @@ def _resolve_runtime(self, backend: RuntimeBackend, cfg: Settings) -> RuntimePor
host = getattr(cfg, host_field, None) if host_field else None
gpu_layers_field = self._GPU_LAYERS_SETTING_FOR.get(backend)
gpu_layers = getattr(cfg, gpu_layers_field, None) if gpu_layers_field else None
models_dir_field = self._MODELS_DIR_SETTING_FOR.get(backend)
models_dir = getattr(cfg, models_dir_field, None) if models_dir_field else None
try:
runtime = self._runtime_registry.get(backend, host=host, gpu_layers=gpu_layers)
runtime = self._runtime_registry.get(
backend,
host=host,
gpu_layers=gpu_layers,
models_dir=models_dir,
)
except KeyError as exc:
raise RuntimeUnavailableError(backend.value) from exc
if not runtime.is_available():
Expand Down
12 changes: 12 additions & 0 deletions tests/unit/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ def test_settings_llamacpp_gpu_layers_defaults_to_none() -> None:
assert Settings().llamacpp_gpu_layers is None


def test_settings_gpt4all_models_dir_defaults_to_none() -> None:
assert Settings().gpt4all_models_dir is None


def test_settings_llamacpp_gpu_layers_accepts_non_negative_int() -> None:
assert Settings(llamacpp_gpu_layers=35).llamacpp_gpu_layers == 35

Expand Down Expand Up @@ -89,6 +93,14 @@ def test_load_settings_llamacpp_gpu_layers_invalid_env_falls_back(
assert s.llamacpp_gpu_layers is None


def test_load_settings_gpt4all_models_dir_env(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
models_dir = tmp_path / "gpt4all"
monkeypatch.setenv("MODELDOCK_GPT4ALL_MODELS_DIR", str(models_dir))
assert load_settings().gpt4all_models_dir == models_dir


def test_default_cache_dir_override(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("MODELDOCK_CACHE_DIR", str(tmp_path))
assert default_cache_dir() == tmp_path
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/test_gpt4all_runtime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Unit tests for GPT4All local model discovery (issue #30)."""

from __future__ import annotations

from pathlib import Path

from modeldock.adapters.runtimes.gpt4all import Gpt4AllRuntime
from modeldock.adapters.runtimes.registry import RuntimeRegistry
from modeldock.domain.model import RuntimeBackend


def test_list_installed_registers_supported_model_files(tmp_path: Path) -> None:
(tmp_path / "Phi-3-mini.Q4_0.gguf").touch()
(tmp_path / "legacy-model.bin").touch()
(tmp_path / "notes.txt").touch()
(tmp_path / "nested").mkdir()
(tmp_path / "nested" / "ignored.gguf").touch()

runtime = Gpt4AllRuntime(models_dir=tmp_path)

assert runtime.is_available() is True
assert [ref.name for ref in runtime.list_installed()] == [
"Phi-3-mini.Q4_0",
"legacy-model",
]


def test_missing_models_directory_is_unavailable_and_empty(tmp_path: Path) -> None:
runtime = Gpt4AllRuntime(models_dir=tmp_path / "missing")

assert runtime.is_available() is False
assert runtime.list_installed() == []


def test_registry_forwards_gpt4all_models_dir(tmp_path: Path) -> None:
runtime = RuntimeRegistry().get(RuntimeBackend.GPT4ALL, models_dir=tmp_path)

assert isinstance(runtime, Gpt4AllRuntime)
assert runtime._models_dir == tmp_path
Loading