best-engine-ai-helper 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. best_engine_ai_helper-0.3.0/LICENSE +28 -0
  2. best_engine_ai_helper-0.3.0/PKG-INFO +136 -0
  3. best_engine_ai_helper-0.3.0/README.md +121 -0
  4. best_engine_ai_helper-0.3.0/best_engine_ai_helper/__init__.py +82 -0
  5. best_engine_ai_helper-0.3.0/best_engine_ai_helper/catalog.py +164 -0
  6. best_engine_ai_helper-0.3.0/best_engine_ai_helper/cli.py +420 -0
  7. best_engine_ai_helper-0.3.0/best_engine_ai_helper/config.py +149 -0
  8. best_engine_ai_helper-0.3.0/best_engine_ai_helper/detect.py +372 -0
  9. best_engine_ai_helper-0.3.0/best_engine_ai_helper/hardware.py +145 -0
  10. best_engine_ai_helper-0.3.0/best_engine_ai_helper/llm.py +570 -0
  11. best_engine_ai_helper-0.3.0/best_engine_ai_helper/pull.py +187 -0
  12. best_engine_ai_helper-0.3.0/best_engine_ai_helper/ralph.py +456 -0
  13. best_engine_ai_helper-0.3.0/best_engine_ai_helper/recommend.py +268 -0
  14. best_engine_ai_helper-0.3.0/best_engine_ai_helper/score.py +330 -0
  15. best_engine_ai_helper-0.3.0/best_engine_ai_helper/sources/__init__.py +14 -0
  16. best_engine_ai_helper-0.3.0/best_engine_ai_helper/sources/apxml.py +397 -0
  17. best_engine_ai_helper-0.3.0/best_engine_ai_helper/validate_llm.py +118 -0
  18. best_engine_ai_helper-0.3.0/best_engine_ai_helper/validate_vlm.py +181 -0
  19. best_engine_ai_helper-0.3.0/best_engine_ai_helper.egg-info/PKG-INFO +136 -0
  20. best_engine_ai_helper-0.3.0/best_engine_ai_helper.egg-info/SOURCES.txt +32 -0
  21. best_engine_ai_helper-0.3.0/best_engine_ai_helper.egg-info/dependency_links.txt +1 -0
  22. best_engine_ai_helper-0.3.0/best_engine_ai_helper.egg-info/entry_points.txt +2 -0
  23. best_engine_ai_helper-0.3.0/best_engine_ai_helper.egg-info/requires.txt +4 -0
  24. best_engine_ai_helper-0.3.0/best_engine_ai_helper.egg-info/top_level.txt +1 -0
  25. best_engine_ai_helper-0.3.0/pyproject.toml +43 -0
  26. best_engine_ai_helper-0.3.0/setup.cfg +4 -0
  27. best_engine_ai_helper-0.3.0/tests/test_catalog.py +140 -0
  28. best_engine_ai_helper-0.3.0/tests/test_cli.py +167 -0
  29. best_engine_ai_helper-0.3.0/tests/test_config.py +114 -0
  30. best_engine_ai_helper-0.3.0/tests/test_detect.py +82 -0
  31. best_engine_ai_helper-0.3.0/tests/test_hardware.py +107 -0
  32. best_engine_ai_helper-0.3.0/tests/test_phase0b.py +350 -0
  33. best_engine_ai_helper-0.3.0/tests/test_recommend.py +78 -0
  34. best_engine_ai_helper-0.3.0/tests/test_score.py +174 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Warith Harchaoui
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: best-engine-ai-helper
3
+ Version: 0.3.0
4
+ Summary: Pick and pull the best local LLM/VLM for the current hardware.
5
+ Author-email: Warith Harchaoui <warith.harchaoui@gmail.com>
6
+ License: BSD-3-Clause
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: click>=8.1
11
+ Requires-Dist: psutil>=5.9
12
+ Requires-Dist: pyyaml>=6.0
13
+ Requires-Dist: requests>=2.31
14
+ Dynamic: license-file
15
+
16
+ # best-engine-ai-helper
17
+
18
+ Pick and pull the best local large language model (LLM) or vision-language model (VLM) for the hardware in the current machine.
19
+
20
+ The tool detects available memory (Apple Silicon unified pool, NVIDIA VRAM, or system RAM), consults a bundled model catalog, and selects the highest-scoring model that fits within a configurable safety headroom. After selection, it pulls the model via Ollama, runs two quality gates (the Ralph Loop for prose and the Ralph Eyeball Loop for vision), and writes an environment file that downstream projects source to find the chosen model.
21
+
22
+ Phase 0a (this release) covers detection, catalog, scoring, and the CLI skeleton. Phase 0b adds the pull, validate, and env commands.
23
+
24
+ ## Requirements
25
+
26
+ - Python 3.10 or later
27
+ - [Ollama](https://ollama.com) (Phase 0b only; not needed for `detect` and `recommend`)
28
+ - psutil, PyYAML, click, requests (installed automatically)
29
+
30
+ ## Install
31
+
32
+ ```sh
33
+ pip install best-engine-ai-helper
34
+ ```
35
+
36
+ Or from source:
37
+
38
+ ```sh
39
+ git clone https://github.com/warith-harchaoui/best-engine-ai-helper
40
+ cd best-engine-ai-helper
41
+ pip install -e .
42
+ ```
43
+
44
+ ## Quick start
45
+
46
+ ```sh
47
+ # See what hardware this machine has
48
+ best-engine-ai-helper detect
49
+
50
+ # See which models would be selected (no download)
51
+ best-engine-ai-helper recommend
52
+
53
+ # Recommend the best engine(s) for a task, as a report (Markdown + JSON)
54
+ best-engine-ai-helper report --task "product descriptions and image-quality checks" --out engine
55
+
56
+ # Browse the full catalog
57
+ best-engine-ai-helper catalog show
58
+
59
+ # Browse the hardware chip table
60
+ best-engine-ai-helper hardware show
61
+ ```
62
+
63
+ See [EXAMPLES.md](https://github.com/warith-harchaoui/best-engine-ai-helper/blob/main/EXAMPLES.md) for runnable recipes with expected output.
64
+
65
+ ## How selection works
66
+
67
+ Selection weighs three factors, each made explicit in the `report` output so the
68
+ recommendation is reproducible and can be argued with.
69
+
70
+ ### 1. Memory fit (will it run on the accelerator?)
71
+
72
+ Available memory comes from the highest-priority probe that succeeds: Apple
73
+ Silicon unified memory (`system_profiler`), NVIDIA VRAM (`nvidia-smi`), AMD VRAM
74
+ (`rocm-smi`), else half of system RAM as a conservative CPU-only estimate.
75
+
76
+ The usable budget is **not** the whole pool. On Apple Silicon, Metal caps GPU
77
+ allocations at about **66%** of unified memory at or below 36 GB and about
78
+ **75%** above it (`recommendedMaxWorkingSetSize`); past that, work spills to the
79
+ CPU and slows sharply. An extra safety `headroom` (default **0.85**) is applied
80
+ on top, reserving room for the OS, your own application, and KV-cache growth as
81
+ context fills. A catalog entry's `ram_gb` is already a peak-inference estimate
82
+ (weights plus a moderate KV cache; roughly weights ÷ 0.7, since weights are only
83
+ about 70% of runtime memory and can approach 2× at long context). A model fits
84
+ when that `ram_gb` is at most the budget.
85
+
86
+ ### 2. Task fit (is it good at the job?)
87
+
88
+ A task — even a vague phrase like *"retail descriptions and image-quality
89
+ checks"* — maps to a benchmark axis (`generalist`, `code`, `math`, `ocr`,
90
+ `vision`) and to the model kinds it needs (text ⇒ LLM, anything visual ⇒ VLM).
91
+ Among fitting models, the highest benchmark on that axis wins.
92
+
93
+ ### 3. Throughput (how fast will it generate?)
94
+
95
+ Token generation is **memory-bandwidth bound**: each token reads the active
96
+ model from memory once, so the ceiling is `bandwidth ÷ model-size`, derated to
97
+ about 65% for KV-cache reads and overhead. `report` estimates tokens/s per
98
+ candidate from the chip's memory bandwidth, and offers a lighter, faster
99
+ alternative when one is nearly as strong. Bigger is not automatically better: a
100
+ 72B model that technically fits may run at a few tokens/s, while an 8–14B model
101
+ leaves headroom and runs several times faster.
102
+
103
+ When nothing fits, the tool falls back to the smallest model and says so.
104
+
105
+ **Sources.** Apple Metal working-set cap (Apple developer docs / apple-specs);
106
+ inference-memory breakdown weights + KV (15–20%) + overhead (5–10%) (local-LLM
107
+ sizing guides); bandwidth-bound decode `tok/s ≈ bandwidth ÷ active-bytes × 0.5–0.8`
108
+ (llama.cpp / MLX community benchmarks). The exact ratios live as documented
109
+ constants in `score.py`.
110
+
111
+ ## Model catalog
112
+
113
+ The bundled seed catalog (`models.yaml`) covers 13 models from the Qwen 3, Qwen 2.5, and Gemma 3 families, from 3B to 72B parameters, across Q4_K_M and Q8_0 quantizations. The catalog tracks on-disk size, estimated peak RAM, and benchmark scores from the Open LLM Leaderboard v2 and the OpenVLM Leaderboard.
114
+
115
+ `catalog update` (Phase 0b) refreshes the cache from external sources: the Ollama registry API, the HuggingFace Hub API, the Open LLM Leaderboard v2 dataset, the OpenVLM Leaderboard dataset, and the [ApXML LLM directory](https://apxml.com/models?modelType=open_weight) (open-weight models with VRAM/compute needs and coding benchmarks, consulted regularly). The bundled seed is never modified by the refresh.
116
+
117
+ ## Hardware table
118
+
119
+ `hardware.yaml` lists known GPU and Apple Silicon chip configurations with their usable memory (physical pool minus OS and driver overhead). `hardware update` (Phase 0b) refreshes NVIDIA entries from TechPowerUp.
120
+
121
+ ## Downstream integration
122
+
123
+ After `best-engine-ai-helper pull` completes, it writes `~/.best-engine-ai-helper/env.sh`:
124
+
125
+ ```sh
126
+ export BEST_LLM_TEXT=qwen3-vl:72b
127
+ export BEST_LLM_VISION=qwen3-vl:72b
128
+ export BEST_LLM_BACKEND=ollama
129
+ export BEST_LLM_BASE_URL=http://localhost:11434
130
+ ```
131
+
132
+ Projects that consume the selected model source this file or read the companion `config.json`.
133
+
134
+ ## License
135
+
136
+ [BSD-3-Clause](https://github.com/warith-harchaoui/best-engine-ai-helper/blob/main/LICENSE). Copyright 2026 Warith Harchaoui.
@@ -0,0 +1,121 @@
1
+ # best-engine-ai-helper
2
+
3
+ Pick and pull the best local large language model (LLM) or vision-language model (VLM) for the hardware in the current machine.
4
+
5
+ The tool detects available memory (Apple Silicon unified pool, NVIDIA VRAM, or system RAM), consults a bundled model catalog, and selects the highest-scoring model that fits within a configurable safety headroom. After selection, it pulls the model via Ollama, runs two quality gates (the Ralph Loop for prose and the Ralph Eyeball Loop for vision), and writes an environment file that downstream projects source to find the chosen model.
6
+
7
+ Phase 0a (this release) covers detection, catalog, scoring, and the CLI skeleton. Phase 0b adds the pull, validate, and env commands.
8
+
9
+ ## Requirements
10
+
11
+ - Python 3.10 or later
12
+ - [Ollama](https://ollama.com) (Phase 0b only; not needed for `detect` and `recommend`)
13
+ - psutil, PyYAML, click, requests (installed automatically)
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pip install best-engine-ai-helper
19
+ ```
20
+
21
+ Or from source:
22
+
23
+ ```sh
24
+ git clone https://github.com/warith-harchaoui/best-engine-ai-helper
25
+ cd best-engine-ai-helper
26
+ pip install -e .
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ```sh
32
+ # See what hardware this machine has
33
+ best-engine-ai-helper detect
34
+
35
+ # See which models would be selected (no download)
36
+ best-engine-ai-helper recommend
37
+
38
+ # Recommend the best engine(s) for a task, as a report (Markdown + JSON)
39
+ best-engine-ai-helper report --task "product descriptions and image-quality checks" --out engine
40
+
41
+ # Browse the full catalog
42
+ best-engine-ai-helper catalog show
43
+
44
+ # Browse the hardware chip table
45
+ best-engine-ai-helper hardware show
46
+ ```
47
+
48
+ See [EXAMPLES.md](https://github.com/warith-harchaoui/best-engine-ai-helper/blob/main/EXAMPLES.md) for runnable recipes with expected output.
49
+
50
+ ## How selection works
51
+
52
+ Selection weighs three factors, each made explicit in the `report` output so the
53
+ recommendation is reproducible and can be argued with.
54
+
55
+ ### 1. Memory fit (will it run on the accelerator?)
56
+
57
+ Available memory comes from the highest-priority probe that succeeds: Apple
58
+ Silicon unified memory (`system_profiler`), NVIDIA VRAM (`nvidia-smi`), AMD VRAM
59
+ (`rocm-smi`), else half of system RAM as a conservative CPU-only estimate.
60
+
61
+ The usable budget is **not** the whole pool. On Apple Silicon, Metal caps GPU
62
+ allocations at about **66%** of unified memory at or below 36 GB and about
63
+ **75%** above it (`recommendedMaxWorkingSetSize`); past that, work spills to the
64
+ CPU and slows sharply. An extra safety `headroom` (default **0.85**) is applied
65
+ on top, reserving room for the OS, your own application, and KV-cache growth as
66
+ context fills. A catalog entry's `ram_gb` is already a peak-inference estimate
67
+ (weights plus a moderate KV cache; roughly weights ÷ 0.7, since weights are only
68
+ about 70% of runtime memory and can approach 2× at long context). A model fits
69
+ when that `ram_gb` is at most the budget.
70
+
71
+ ### 2. Task fit (is it good at the job?)
72
+
73
+ A task — even a vague phrase like *"retail descriptions and image-quality
74
+ checks"* — maps to a benchmark axis (`generalist`, `code`, `math`, `ocr`,
75
+ `vision`) and to the model kinds it needs (text ⇒ LLM, anything visual ⇒ VLM).
76
+ Among fitting models, the highest benchmark on that axis wins.
77
+
78
+ ### 3. Throughput (how fast will it generate?)
79
+
80
+ Token generation is **memory-bandwidth bound**: each token reads the active
81
+ model from memory once, so the ceiling is `bandwidth ÷ model-size`, derated to
82
+ about 65% for KV-cache reads and overhead. `report` estimates tokens/s per
83
+ candidate from the chip's memory bandwidth, and offers a lighter, faster
84
+ alternative when one is nearly as strong. Bigger is not automatically better: a
85
+ 72B model that technically fits may run at a few tokens/s, while an 8–14B model
86
+ leaves headroom and runs several times faster.
87
+
88
+ When nothing fits, the tool falls back to the smallest model and says so.
89
+
90
+ **Sources.** Apple Metal working-set cap (Apple developer docs / apple-specs);
91
+ inference-memory breakdown weights + KV (15–20%) + overhead (5–10%) (local-LLM
92
+ sizing guides); bandwidth-bound decode `tok/s ≈ bandwidth ÷ active-bytes × 0.5–0.8`
93
+ (llama.cpp / MLX community benchmarks). The exact ratios live as documented
94
+ constants in `score.py`.
95
+
96
+ ## Model catalog
97
+
98
+ The bundled seed catalog (`models.yaml`) covers 13 models from the Qwen 3, Qwen 2.5, and Gemma 3 families, from 3B to 72B parameters, across Q4_K_M and Q8_0 quantizations. The catalog tracks on-disk size, estimated peak RAM, and benchmark scores from the Open LLM Leaderboard v2 and the OpenVLM Leaderboard.
99
+
100
+ `catalog update` (Phase 0b) refreshes the cache from external sources: the Ollama registry API, the HuggingFace Hub API, the Open LLM Leaderboard v2 dataset, the OpenVLM Leaderboard dataset, and the [ApXML LLM directory](https://apxml.com/models?modelType=open_weight) (open-weight models with VRAM/compute needs and coding benchmarks, consulted regularly). The bundled seed is never modified by the refresh.
101
+
102
+ ## Hardware table
103
+
104
+ `hardware.yaml` lists known GPU and Apple Silicon chip configurations with their usable memory (physical pool minus OS and driver overhead). `hardware update` (Phase 0b) refreshes NVIDIA entries from TechPowerUp.
105
+
106
+ ## Downstream integration
107
+
108
+ After `best-engine-ai-helper pull` completes, it writes `~/.best-engine-ai-helper/env.sh`:
109
+
110
+ ```sh
111
+ export BEST_LLM_TEXT=qwen3-vl:72b
112
+ export BEST_LLM_VISION=qwen3-vl:72b
113
+ export BEST_LLM_BACKEND=ollama
114
+ export BEST_LLM_BASE_URL=http://localhost:11434
115
+ ```
116
+
117
+ Projects that consume the selected model source this file or read the companion `config.json`.
118
+
119
+ ## License
120
+
121
+ [BSD-3-Clause](https://github.com/warith-harchaoui/best-engine-ai-helper/blob/main/LICENSE). Copyright 2026 Warith Harchaoui.
@@ -0,0 +1,82 @@
1
+ """
2
+ best_engine_ai_helper — pick the best local LLM/VLM for the current hardware.
3
+
4
+ Public API (importable without invoking the CLI):
5
+
6
+ from best_engine_ai_helper.detect import platform_name, chip_vendor, available_memory
7
+ from best_engine_ai_helper.catalog import load_catalog, estimate_ram
8
+ from best_engine_ai_helper.hardware import load_hardware, lookup_chip
9
+ from best_engine_ai_helper.score import select, rank, effective_budget
10
+ from best_engine_ai_helper import text_model, vision_model # cheap tag resolvers
11
+
12
+ The CLI entry point is ``best-engine-ai-helper`` (see cli.py). Importing this
13
+ package does not trigger any CLI parsing or subprocess calls.
14
+
15
+ Downstream suite packages that just need "which model tag do I use?" should call
16
+ :func:`text_model` / :func:`vision_model` — they resolve an env override, then
17
+ the selection persisted by ``pull``, then a safe default, without ever probing
18
+ hardware, so they are cheap and deterministic (CI-safe).
19
+
20
+ Author
21
+ ------
22
+ Warith Harchaoui <warith.harchaoui@gmail.com>
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from .catalog import estimate_ram, load_catalog
28
+
29
+ # Cheap, deterministic model-tag resolution (env -> persisted config -> default);
30
+ # the entry point downstream packages call to answer "which model do I use?".
31
+ from .config import (
32
+ DEFAULT_TEXT_MODEL,
33
+ DEFAULT_VISION_MODEL,
34
+ load_config,
35
+ resolved_models,
36
+ text_model,
37
+ vision_model,
38
+ )
39
+ from .detect import (
40
+ available_memory,
41
+ chip_name,
42
+ chip_vendor,
43
+ compute_profile,
44
+ platform_name,
45
+ )
46
+ from .hardware import load_hardware, lookup_chip
47
+
48
+ # Export the engine-recommendation function under a distinct name so it does
49
+ # not shadow the ``recommend`` submodule on attribute access.
50
+ from .recommend import parse_task, to_markdown, write_report
51
+ from .recommend import recommend as recommend_engines
52
+ from .score import effective_budget, estimated_tokens_per_second, rank, select
53
+
54
+ __all__ = [
55
+ "platform_name",
56
+ "chip_vendor",
57
+ "chip_name",
58
+ "available_memory",
59
+ "compute_profile",
60
+ "load_catalog",
61
+ "estimate_ram",
62
+ "load_hardware",
63
+ "lookup_chip",
64
+ "select",
65
+ "rank",
66
+ "effective_budget",
67
+ "estimated_tokens_per_second",
68
+ "recommend_engines",
69
+ "parse_task",
70
+ "to_markdown",
71
+ "write_report",
72
+ "text_model",
73
+ "vision_model",
74
+ "resolved_models",
75
+ "load_config",
76
+ "DEFAULT_TEXT_MODEL",
77
+ "DEFAULT_VISION_MODEL",
78
+ ]
79
+
80
+ __version__ = "0.3.0"
81
+ __author__ = "Warith Harchaoui"
82
+ __email__ = "warith.harchaoui@gmail.com"
@@ -0,0 +1,164 @@
1
+ """
2
+ catalog — load and merge the bundled model catalog with the user's cache.
3
+
4
+ The catalog lives in two layers:
5
+
6
+ 1. The **bundled seed** (`models.yaml` in the package root). Hand-maintained,
7
+ always present, never deleted by auto-refresh.
8
+ 2. The **user cache** (`~/.best-engine-ai-helper/catalog_cache.yaml`). Written
9
+ by `catalog update`; entries keyed by `id` overwrite matching seed entries.
10
+ Absent on first run; load_catalog silently skips it in that case.
11
+
12
+ Keeping the seed immutable and the cache additive means offline machines always
13
+ have a usable catalog and updates never lose hand-curated data.
14
+
15
+ Author
16
+ ------
17
+ Warith Harchaoui <warith.harchaoui@gmail.com>
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ import yaml
26
+
27
+ # Root of the installed package; models.yaml sits next to pyproject.toml
28
+ _PACKAGE_ROOT = Path(__file__).resolve().parent.parent
29
+
30
+ # Default path for the bundled seed catalog
31
+ _SEED_PATH = _PACKAGE_ROOT / "models.yaml"
32
+
33
+ # User-writable runtime directory; created on first run
34
+ _USER_DIR = Path.home() / ".best-engine-ai-helper"
35
+ _CACHE_PATH = _USER_DIR / "catalog_cache.yaml"
36
+
37
+ # Quant-specific overhead factors for estimating peak inference RAM.
38
+ # Values are empirically derived: KV cache at 4K context adds ~10-20%
39
+ # beyond on-disk size depending on quantization precision.
40
+ _QUANT_OVERHEAD: dict[str, float] = {
41
+ "Q4_K_M": 1.12,
42
+ "Q8_0": 1.10,
43
+ "FP16": 1.05,
44
+ "Q2_K": 1.20,
45
+ }
46
+ _DEFAULT_OVERHEAD = 1.15 # fallback for unknown quant identifiers
47
+
48
+
49
+ def estimate_ram(disk_gb: float, quant: str) -> float:
50
+ """
51
+ Estimate peak inference RAM from on-disk model size.
52
+
53
+ The estimate covers the model weights plus KV cache at the default context
54
+ length (4K tokens). For models with very large context windows (256K+),
55
+ actual RAM may exceed this estimate significantly; treat it as a lower bound.
56
+
57
+ Parameters
58
+ ----------
59
+ disk_gb : float
60
+ On-disk footprint of the model in GB.
61
+ quant : str
62
+ Quantization identifier, e.g. 'Q4_K_M', 'Q8_0', 'FP16', 'Q2_K'.
63
+
64
+ Returns
65
+ -------
66
+ float
67
+ Estimated peak RAM in GB.
68
+
69
+ Examples
70
+ --------
71
+ >>> estimate_ram(6.1, 'Q4_K_M')
72
+ 6.832
73
+ >>> estimate_ram(10.0, 'FP16')
74
+ 10.5
75
+ """
76
+ overhead = _QUANT_OVERHEAD.get(quant, _DEFAULT_OVERHEAD)
77
+ return round(disk_gb * overhead, 3)
78
+
79
+
80
+ def _load_yaml_file(path: Path) -> list[dict[str, Any]]:
81
+ """
82
+ Load a YAML file and return its contents as a list of dicts.
83
+
84
+ Returns an empty list on FileNotFoundError so callers can treat the
85
+ absence of the cache file as a no-op rather than an error.
86
+
87
+ Parameters
88
+ ----------
89
+ path : Path
90
+ Absolute path to the YAML file.
91
+
92
+ Returns
93
+ -------
94
+ list[dict[str, Any]]
95
+ Parsed entries, or [] if the file is absent or empty.
96
+ """
97
+ try:
98
+ raw = yaml.safe_load(path.read_text(encoding="utf-8"))
99
+ except FileNotFoundError:
100
+ return []
101
+ # yaml.safe_load returns None for empty files
102
+ if raw is None:
103
+ return []
104
+ return list(raw)
105
+
106
+
107
+ def load_catalog(catalog_path: Path | None = None) -> list[dict[str, Any]]:
108
+ """
109
+ Load the bundled seed catalog merged with the user's local cache.
110
+
111
+ Cache entries whose `id` matches a seed entry overwrite the seed entry.
112
+ New cache entries (no matching seed id) are appended. The seed is never
113
+ modified on disk.
114
+
115
+ Parameters
116
+ ----------
117
+ catalog_path : Path or None
118
+ Path to the seed `models.yaml`. Defaults to the bundled file next to
119
+ `pyproject.toml`. Pass an explicit path in tests to use a fixture.
120
+
121
+ Returns
122
+ -------
123
+ list[dict[str, Any]]
124
+ Merged model entries. Each entry is guaranteed to have at minimum:
125
+ ``id``, ``kind``, ``ram_gb``, ``benchmarks``.
126
+
127
+ Raises
128
+ ------
129
+ FileNotFoundError
130
+ If ``catalog_path`` is given explicitly and does not exist.
131
+
132
+ Examples
133
+ --------
134
+ >>> entries = load_catalog()
135
+ >>> len(entries) > 0
136
+ True
137
+ >>> all('id' in e for e in entries)
138
+ True
139
+ """
140
+ seed_path = catalog_path if catalog_path is not None else _SEED_PATH
141
+
142
+ # Start with the bundled seed; this must exist
143
+ seed = _load_yaml_file(seed_path)
144
+ if not seed and catalog_path is not None:
145
+ raise FileNotFoundError(f"Catalog not found: {seed_path}")
146
+
147
+ # Build a lookup by id so cache entries can overwrite in O(1)
148
+ merged: dict[str, dict[str, Any]] = {e["id"]: e for e in seed}
149
+
150
+ # Overlay user cache entries; absent cache is silently ignored
151
+ for entry in _load_yaml_file(_CACHE_PATH):
152
+ merged[entry["id"]] = entry
153
+
154
+ # Preserve seed ordering; append cache-only entries at the end
155
+ result: list[dict[str, Any]] = []
156
+ seen: set[str] = set()
157
+ for e in seed:
158
+ result.append(merged[e["id"]])
159
+ seen.add(e["id"])
160
+ for eid, entry in merged.items():
161
+ if eid not in seen:
162
+ result.append(entry)
163
+
164
+ return result