localforge-inference-lab 0.1.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.
- localforge_inference_lab-0.1.0/.gitignore +24 -0
- localforge_inference_lab-0.1.0/PKG-INFO +67 -0
- localforge_inference_lab-0.1.0/README.md +47 -0
- localforge_inference_lab-0.1.0/pyproject.toml +71 -0
- localforge_inference_lab-0.1.0/src/localforge/__init__.py +3 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/__init__.py +19 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/catalog.py +307 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/contracts.py +295 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/exllamav3.py +35 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/fake.py +573 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/koboldcpp.py +44 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/library_pipeline.py +863 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/library_worker.py +236 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/lightllm.py +44 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/llama_cpp.py +1344 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/lmdeploy.py +44 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/localai.py +157 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/mistral_rs.py +59 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/mlc_llm.py +44 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/mlx_lm.py +45 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/nvidia_dynamo.py +142 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/ollama.py +964 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/onnxruntime_genai.py +45 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/openai_compatible.py +1573 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/openvino_genai.py +45 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/prometheus.py +268 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/protocol.py +62 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/runner.py +82 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/serving_engine.py +1155 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/sglang.py +46 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/tensorrt_llm.py +44 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/tgi.py +50 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/triton.py +40 -0
- localforge_inference_lab-0.1.0/src/localforge/adapters/vllm.py +43 -0
- localforge_inference_lab-0.1.0/src/localforge/api/__init__.py +1 -0
- localforge_inference_lab-0.1.0/src/localforge/api/dependencies.py +44 -0
- localforge_inference_lab-0.1.0/src/localforge/api/experiments.py +1281 -0
- localforge_inference_lab-0.1.0/src/localforge/api/main.py +438 -0
- localforge_inference_lab-0.1.0/src/localforge/api/playground.py +245 -0
- localforge_inference_lab-0.1.0/src/localforge/api/profiles.py +830 -0
- localforge_inference_lab-0.1.0/src/localforge/api/runs.py +641 -0
- localforge_inference_lab-0.1.0/src/localforge/api/schemas.py +334 -0
- localforge_inference_lab-0.1.0/src/localforge/api/settings.py +132 -0
- localforge_inference_lab-0.1.0/src/localforge/api/workloads.py +1115 -0
- localforge_inference_lab-0.1.0/src/localforge/cli.py +140 -0
- localforge_inference_lab-0.1.0/src/localforge/comparisons.py +1114 -0
- localforge_inference_lab-0.1.0/src/localforge/core/__init__.py +1 -0
- localforge_inference_lab-0.1.0/src/localforge/core/models.py +890 -0
- localforge_inference_lab-0.1.0/src/localforge/core/urls.py +48 -0
- localforge_inference_lab-0.1.0/src/localforge/dataset_import.py +320 -0
- localforge_inference_lab-0.1.0/src/localforge/dataset_storage.py +121 -0
- localforge_inference_lab-0.1.0/src/localforge/metrics.py +1129 -0
- localforge_inference_lab-0.1.0/src/localforge/runs.py +575 -0
- localforge_inference_lab-0.1.0/src/localforge/security/__init__.py +1 -0
- localforge_inference_lab-0.1.0/src/localforge/security/local_session.py +54 -0
- localforge_inference_lab-0.1.0/src/localforge/storage/__init__.py +1 -0
- localforge_inference_lab-0.1.0/src/localforge/storage/database.py +312 -0
- localforge_inference_lab-0.1.0/src/localforge/storage/paths.py +22 -0
- localforge_inference_lab-0.1.0/src/localforge/storage/repositories.py +423 -0
- localforge_inference_lab-0.1.0/src/localforge/system/__init__.py +1 -0
- localforge_inference_lab-0.1.0/src/localforge/system/inspection.py +703 -0
- localforge_inference_lab-0.1.0/src/localforge/web/assets/index-BNnJ3ITv.js +12 -0
- localforge_inference_lab-0.1.0/src/localforge/web/assets/index-PG0RTmw6.css +1 -0
- localforge_inference_lab-0.1.0/src/localforge/web/favicon.svg +4 -0
- localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-engine-detail.jpg +0 -0
- localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-engine-hero.jpg +0 -0
- localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-experiments-hero.jpg +0 -0
- localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-inference-hero.jpg +0 -0
- localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-learning-hero.jpg +0 -0
- localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-overview-hero.jpg +0 -0
- localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-runs-hero.jpg +0 -0
- localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-system-hero.jpg +0 -0
- localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-workloads-hero.jpg +0 -0
- localforge_inference_lab-0.1.0/src/localforge/web/index.html +19 -0
- localforge_inference_lab-0.1.0/src/localforge/web_ui.py +42 -0
- localforge_inference_lab-0.1.0/src/localforge/worker/__init__.py +1 -0
- localforge_inference_lab-0.1.0/src/localforge/worker/__main__.py +58 -0
- localforge_inference_lab-0.1.0/src/localforge/worker/execution.py +1405 -0
- localforge_inference_lab-0.1.0/src/localforge/workspace_settings.py +131 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.mypy_cache/
|
|
6
|
+
.ruff_cache/
|
|
7
|
+
|
|
8
|
+
node_modules/
|
|
9
|
+
dist/
|
|
10
|
+
# Compiled UI copied in by scripts/build_release.py for packaging.
|
|
11
|
+
backend/src/localforge/web/
|
|
12
|
+
coverage/
|
|
13
|
+
playwright-report/
|
|
14
|
+
test-results/
|
|
15
|
+
|
|
16
|
+
.localforge/
|
|
17
|
+
.playwright-data/
|
|
18
|
+
*.sqlite3
|
|
19
|
+
*.sqlite3-shm
|
|
20
|
+
*.sqlite3-wal
|
|
21
|
+
|
|
22
|
+
.env
|
|
23
|
+
.env.*
|
|
24
|
+
!.env.example
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: localforge-inference-lab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first LLM inference benchmarking workbench
|
|
5
|
+
Project-URL: Repository, https://github.com/HaisamAbbas/Local-Forge
|
|
6
|
+
Keywords: benchmark,inference,llama.cpp,llm,ollama,vllm
|
|
7
|
+
Classifier: Environment :: Web Environment
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
11
|
+
Classifier: Topic :: System :: Benchmark
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: fastapi>=0.115
|
|
14
|
+
Requires-Dist: httpx2>=1.0
|
|
15
|
+
Requires-Dist: keyring>=25.6
|
|
16
|
+
Requires-Dist: psutil>=6.1.0
|
|
17
|
+
Requires-Dist: python-multipart>=0.0.32
|
|
18
|
+
Requires-Dist: uvicorn>=0.34
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# LocalForge Inference Lab
|
|
22
|
+
|
|
23
|
+
LocalForge measures and compares LLM inference engines on your own machine. Connect an engine
|
|
24
|
+
you already run (Ollama, llama.cpp, vLLM, SGLang, an OpenAI-compatible server, and more), build a
|
|
25
|
+
repeatable workload, run it, and read the results as charts: time to first token, latency
|
|
26
|
+
percentiles, tokens per second, and host CPU and memory during the run. Compare runs side by
|
|
27
|
+
side to see which model or setting is faster on your hardware.
|
|
28
|
+
|
|
29
|
+
Everything stays local. The app listens only on `127.0.0.1`, keeps its data in a local SQLite
|
|
30
|
+
database, and never downloads models.
|
|
31
|
+
|
|
32
|
+
## Install and run
|
|
33
|
+
|
|
34
|
+
You need [uv](https://docs.astral.sh/uv/) and an inference engine with a model, for example
|
|
35
|
+
[Ollama](https://ollama.com) with `ollama pull llama3.2:1b`.
|
|
36
|
+
|
|
37
|
+
```shell
|
|
38
|
+
uvx localforge-inference-lab
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
This starts LocalForge and opens it in your browser. Press Ctrl+C in the terminal to stop it.
|
|
42
|
+
To keep a `localforge` command installed instead:
|
|
43
|
+
|
|
44
|
+
```shell
|
|
45
|
+
uv tool install localforge-inference-lab
|
|
46
|
+
localforge
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Options
|
|
50
|
+
|
|
51
|
+
| Option | Purpose |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `--port 8123` | Listen on a specific port. By default 8000, or a free port if it is taken. |
|
|
54
|
+
| `--data-dir PATH` | Keep the database and datasets in another folder, for example a bigger drive. |
|
|
55
|
+
| `--no-browser` | Start without opening a browser tab. |
|
|
56
|
+
| `--version` | Print the installed version. |
|
|
57
|
+
|
|
58
|
+
## First benchmark
|
|
59
|
+
|
|
60
|
+
1. **Engines**: add your engine (for Ollama keep the default `http://127.0.0.1:11434`), probe it,
|
|
61
|
+
and pick a model.
|
|
62
|
+
2. **Workloads**: choose a preset, save the plan, and launch it.
|
|
63
|
+
3. **Runs**: follow the run live and read its charts and measurements.
|
|
64
|
+
4. **Experiments**: select several runs and compare them.
|
|
65
|
+
|
|
66
|
+
Source, documentation, and the development setup are at
|
|
67
|
+
<https://github.com/HaisamAbbas/Local-Forge>.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# LocalForge Inference Lab
|
|
2
|
+
|
|
3
|
+
LocalForge measures and compares LLM inference engines on your own machine. Connect an engine
|
|
4
|
+
you already run (Ollama, llama.cpp, vLLM, SGLang, an OpenAI-compatible server, and more), build a
|
|
5
|
+
repeatable workload, run it, and read the results as charts: time to first token, latency
|
|
6
|
+
percentiles, tokens per second, and host CPU and memory during the run. Compare runs side by
|
|
7
|
+
side to see which model or setting is faster on your hardware.
|
|
8
|
+
|
|
9
|
+
Everything stays local. The app listens only on `127.0.0.1`, keeps its data in a local SQLite
|
|
10
|
+
database, and never downloads models.
|
|
11
|
+
|
|
12
|
+
## Install and run
|
|
13
|
+
|
|
14
|
+
You need [uv](https://docs.astral.sh/uv/) and an inference engine with a model, for example
|
|
15
|
+
[Ollama](https://ollama.com) with `ollama pull llama3.2:1b`.
|
|
16
|
+
|
|
17
|
+
```shell
|
|
18
|
+
uvx localforge-inference-lab
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This starts LocalForge and opens it in your browser. Press Ctrl+C in the terminal to stop it.
|
|
22
|
+
To keep a `localforge` command installed instead:
|
|
23
|
+
|
|
24
|
+
```shell
|
|
25
|
+
uv tool install localforge-inference-lab
|
|
26
|
+
localforge
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Options
|
|
30
|
+
|
|
31
|
+
| Option | Purpose |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `--port 8123` | Listen on a specific port. By default 8000, or a free port if it is taken. |
|
|
34
|
+
| `--data-dir PATH` | Keep the database and datasets in another folder, for example a bigger drive. |
|
|
35
|
+
| `--no-browser` | Start without opening a browser tab. |
|
|
36
|
+
| `--version` | Print the installed version. |
|
|
37
|
+
|
|
38
|
+
## First benchmark
|
|
39
|
+
|
|
40
|
+
1. **Engines**: add your engine (for Ollama keep the default `http://127.0.0.1:11434`), probe it,
|
|
41
|
+
and pick a model.
|
|
42
|
+
2. **Workloads**: choose a preset, save the plan, and launch it.
|
|
43
|
+
3. **Runs**: follow the run live and read its charts and measurements.
|
|
44
|
+
4. **Experiments**: select several runs and compare them.
|
|
45
|
+
|
|
46
|
+
Source, documentation, and the development setup are at
|
|
47
|
+
<https://github.com/HaisamAbbas/Local-Forge>.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "localforge-inference-lab"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Local-first LLM inference benchmarking workbench"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
keywords = ["llm", "inference", "benchmark", "ollama", "llama.cpp", "vllm"]
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Environment :: Web Environment",
|
|
10
|
+
"Intended Audience :: Developers",
|
|
11
|
+
"Programming Language :: Python :: 3.12",
|
|
12
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
13
|
+
"Topic :: System :: Benchmark",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"fastapi>=0.115",
|
|
17
|
+
"httpx2>=1.0",
|
|
18
|
+
"keyring>=25.6",
|
|
19
|
+
"psutil>=6.1.0",
|
|
20
|
+
"python-multipart>=0.0.32",
|
|
21
|
+
"uvicorn>=0.34",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[dependency-groups]
|
|
25
|
+
dev = [
|
|
26
|
+
"mypy>=1.14",
|
|
27
|
+
"pytest>=8.3",
|
|
28
|
+
"ruff>=0.9",
|
|
29
|
+
"types-psutil>=7.2.2.20260906",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Repository = "https://github.com/HaisamAbbas/Local-Forge"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
localforge = "localforge.cli:main"
|
|
37
|
+
# Same command under the package name, so `uvx localforge-inference-lab` works directly.
|
|
38
|
+
localforge-inference-lab = "localforge.cli:main"
|
|
39
|
+
localforge-worker = "localforge.worker.__main__:main"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling>=1.26"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build]
|
|
46
|
+
# The compiled browser UI is gitignored but must ship in release packages.
|
|
47
|
+
artifacts = ["src/localforge/web/**"]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.wheel]
|
|
50
|
+
packages = ["src/localforge"]
|
|
51
|
+
|
|
52
|
+
[tool.hatch.build.targets.sdist]
|
|
53
|
+
include = ["src/localforge", "README.md", "pyproject.toml"]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
testpaths = ["tests"]
|
|
57
|
+
pythonpath = ["src"]
|
|
58
|
+
addopts = "-ra"
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
target-version = "py312"
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
python_version = "3.12"
|
|
69
|
+
mypy_path = "src"
|
|
70
|
+
strict = true
|
|
71
|
+
packages = ["localforge"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Capability-aware inference engine adapters and their shared catalog."""
|
|
2
|
+
|
|
3
|
+
from localforge.adapters.catalog import AdapterCatalog, default_adapter_catalog
|
|
4
|
+
from localforge.adapters.fake import FakeEngineAdapter, FakeEngineScenario
|
|
5
|
+
from localforge.adapters.llama_cpp import LlamaCppEngineAdapter
|
|
6
|
+
from localforge.adapters.openai_compatible import OpenAICompatibleEngineAdapter
|
|
7
|
+
from localforge.adapters.sglang import SglangEngineAdapter
|
|
8
|
+
from localforge.adapters.vllm import VllmEngineAdapter
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AdapterCatalog",
|
|
12
|
+
"FakeEngineAdapter",
|
|
13
|
+
"FakeEngineScenario",
|
|
14
|
+
"LlamaCppEngineAdapter",
|
|
15
|
+
"OpenAICompatibleEngineAdapter",
|
|
16
|
+
"SglangEngineAdapter",
|
|
17
|
+
"VllmEngineAdapter",
|
|
18
|
+
"default_adapter_catalog",
|
|
19
|
+
]
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""Adapter registry and the shared invocation path used by all connectors."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
|
|
6
|
+
from localforge.adapters.contracts import (
|
|
7
|
+
AbortResult,
|
|
8
|
+
AdapterConfiguration,
|
|
9
|
+
AdapterManifest,
|
|
10
|
+
AdapterModel,
|
|
11
|
+
AdapterProcessHandle,
|
|
12
|
+
AdapterStreamEvent,
|
|
13
|
+
ConfigValidationResult,
|
|
14
|
+
GenerationRequest,
|
|
15
|
+
MetricsResult,
|
|
16
|
+
PreparedRequest,
|
|
17
|
+
ProbeResult,
|
|
18
|
+
RuntimeFingerprint,
|
|
19
|
+
validate_capability_claims,
|
|
20
|
+
)
|
|
21
|
+
from localforge.adapters.protocol import EngineAdapter
|
|
22
|
+
from localforge.adapters.runner import AdapterCallRunner
|
|
23
|
+
from localforge.core.models import EngineIntegrationMode
|
|
24
|
+
|
|
25
|
+
ADAPTER_PROTOCOL_VERSION = 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AdapterNotFoundError(LookupError):
|
|
29
|
+
"""Raised when a requested adapter is not present in the local catalog."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AdapterVersionError(RuntimeError):
|
|
33
|
+
"""Raised when a connector speaks an unsupported adapter protocol version."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AdapterCapabilityClaimError(ValueError):
|
|
37
|
+
"""Raised when an adapter reports an undeclared capability or identity."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AdapterCatalog:
|
|
41
|
+
"""Registry and guarded operations shared by fake and real adapters."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, runner: AdapterCallRunner | None = None) -> None:
|
|
44
|
+
self._adapters: dict[str, EngineAdapter] = {}
|
|
45
|
+
self._manifests: dict[str, AdapterManifest] = {}
|
|
46
|
+
self._runner = runner or AdapterCallRunner()
|
|
47
|
+
|
|
48
|
+
def register(self, adapter: EngineAdapter) -> AdapterManifest:
|
|
49
|
+
manifest = adapter.describe()
|
|
50
|
+
if manifest.protocol_version != ADAPTER_PROTOCOL_VERSION:
|
|
51
|
+
raise AdapterVersionError(
|
|
52
|
+
f"Adapter '{manifest.adapter_id}' uses protocol version "
|
|
53
|
+
f"{manifest.protocol_version}; this LOCALFORGE build supports "
|
|
54
|
+
f"version {ADAPTER_PROTOCOL_VERSION}. Install a compatible adapter."
|
|
55
|
+
)
|
|
56
|
+
if manifest.implementation_status != "implemented":
|
|
57
|
+
raise ValueError("only implemented adapter manifests can be registered as adapters")
|
|
58
|
+
if manifest.adapter_id in self._adapters:
|
|
59
|
+
raise ValueError(f"adapter '{manifest.adapter_id}' is already registered")
|
|
60
|
+
self._adapters[manifest.adapter_id] = adapter
|
|
61
|
+
self._manifests[manifest.adapter_id] = manifest
|
|
62
|
+
return manifest
|
|
63
|
+
|
|
64
|
+
def register_catalog_entry(self, manifest: AdapterManifest) -> AdapterManifest:
|
|
65
|
+
"""Expose a planned connector without making it appear callable or installed."""
|
|
66
|
+
|
|
67
|
+
if manifest.protocol_version != ADAPTER_PROTOCOL_VERSION:
|
|
68
|
+
raise AdapterVersionError(
|
|
69
|
+
f"Catalog entry '{manifest.adapter_id}' uses protocol version "
|
|
70
|
+
f"{manifest.protocol_version}; this LOCALFORGE build supports "
|
|
71
|
+
f"version {ADAPTER_PROTOCOL_VERSION}."
|
|
72
|
+
)
|
|
73
|
+
if manifest.implementation_status == "implemented":
|
|
74
|
+
raise ValueError("catalog-only entries must be planned or unsupported")
|
|
75
|
+
if manifest.adapter_id in self._manifests:
|
|
76
|
+
raise ValueError(f"catalog entry '{manifest.adapter_id}' is already registered")
|
|
77
|
+
self._manifests[manifest.adapter_id] = manifest
|
|
78
|
+
return manifest
|
|
79
|
+
|
|
80
|
+
def list_manifests(self) -> list[AdapterManifest]:
|
|
81
|
+
"""Return registered adapters in stable id order for the catalog UI/API."""
|
|
82
|
+
|
|
83
|
+
return [self._manifests[key] for key in sorted(self._manifests)]
|
|
84
|
+
|
|
85
|
+
def get(self, adapter_id: str) -> EngineAdapter:
|
|
86
|
+
adapter = self._adapters.get(adapter_id)
|
|
87
|
+
if adapter is None:
|
|
88
|
+
raise AdapterNotFoundError(
|
|
89
|
+
f"Adapter '{adapter_id}' is not installed or registered in LOCALFORGE."
|
|
90
|
+
)
|
|
91
|
+
return adapter
|
|
92
|
+
|
|
93
|
+
def manifest_for(self, adapter_id: str) -> AdapterManifest:
|
|
94
|
+
self.get(adapter_id)
|
|
95
|
+
return self._manifests[adapter_id]
|
|
96
|
+
|
|
97
|
+
def _validate_mode(self, adapter_id: str, config: AdapterConfiguration) -> None:
|
|
98
|
+
manifest = self.manifest_for(adapter_id)
|
|
99
|
+
if config.integration_mode not in manifest.integration_modes:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Adapter '{adapter_id}' does not support integration mode "
|
|
102
|
+
f"'{config.integration_mode.value}'."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
async def validate_config(
|
|
106
|
+
self, adapter_id: str, config: AdapterConfiguration
|
|
107
|
+
) -> ConfigValidationResult:
|
|
108
|
+
self._validate_mode(adapter_id, config)
|
|
109
|
+
return await self._runner.invoke(self.get(adapter_id).validate_config, config)
|
|
110
|
+
|
|
111
|
+
async def probe(self, adapter_id: str, config: AdapterConfiguration) -> ProbeResult:
|
|
112
|
+
self._validate_mode(adapter_id, config)
|
|
113
|
+
manifest = self.manifest_for(adapter_id)
|
|
114
|
+
result = await self._runner.invoke(self.get(adapter_id).probe, config)
|
|
115
|
+
if (
|
|
116
|
+
result.adapter_id != manifest.adapter_id
|
|
117
|
+
or result.adapter_version != manifest.adapter_version
|
|
118
|
+
):
|
|
119
|
+
raise AdapterCapabilityClaimError("probe result identity does not match its manifest")
|
|
120
|
+
try:
|
|
121
|
+
validate_capability_claims(manifest, result.capabilities)
|
|
122
|
+
except ValueError as error:
|
|
123
|
+
raise AdapterCapabilityClaimError(str(error)) from error
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
async def start(self, adapter_id: str, config: AdapterConfiguration) -> AdapterProcessHandle:
|
|
127
|
+
self._validate_mode(adapter_id, config)
|
|
128
|
+
manifest = self.manifest_for(adapter_id)
|
|
129
|
+
if config.integration_mode is not EngineIntegrationMode.MANAGED_PROCESS:
|
|
130
|
+
raise ValueError("adapter start requires a managed_process integration mode")
|
|
131
|
+
if "managed_process" not in manifest.declared_capabilities:
|
|
132
|
+
raise ValueError(f"Adapter '{adapter_id}' does not declare managed process support.")
|
|
133
|
+
return await self._runner.invoke(self.get(adapter_id).start, config)
|
|
134
|
+
|
|
135
|
+
async def stop(self, adapter_id: str, process: AdapterProcessHandle) -> None:
|
|
136
|
+
adapter = self.get(adapter_id)
|
|
137
|
+
if process.adapter_id != adapter_id:
|
|
138
|
+
raise ValueError("process handle belongs to a different adapter")
|
|
139
|
+
await self._runner.invoke(adapter.stop, process)
|
|
140
|
+
|
|
141
|
+
async def list_models(
|
|
142
|
+
self, adapter_id: str, config: AdapterConfiguration
|
|
143
|
+
) -> list[AdapterModel]:
|
|
144
|
+
self._validate_mode(adapter_id, config)
|
|
145
|
+
return await self._runner.invoke(self.get(adapter_id).list_models, config)
|
|
146
|
+
|
|
147
|
+
async def prepare_request(
|
|
148
|
+
self,
|
|
149
|
+
adapter_id: str,
|
|
150
|
+
config: AdapterConfiguration,
|
|
151
|
+
request: GenerationRequest,
|
|
152
|
+
) -> PreparedRequest:
|
|
153
|
+
self._validate_mode(adapter_id, config)
|
|
154
|
+
prepared = await self._runner.invoke(self.get(adapter_id).prepare_request, config, request)
|
|
155
|
+
if prepared.request_id != request.request_id:
|
|
156
|
+
raise AdapterCapabilityClaimError(
|
|
157
|
+
f"Adapter '{adapter_id}' did not preserve the generation request identity."
|
|
158
|
+
)
|
|
159
|
+
return prepared
|
|
160
|
+
|
|
161
|
+
def stream_request(
|
|
162
|
+
self, adapter_id: str, request: PreparedRequest
|
|
163
|
+
) -> AsyncIterator[AdapterStreamEvent]:
|
|
164
|
+
manifest = self.manifest_for(adapter_id)
|
|
165
|
+
stream = self._runner.stream(self.get(adapter_id).stream_request, request)
|
|
166
|
+
|
|
167
|
+
async def validate_events() -> AsyncIterator[AdapterStreamEvent]:
|
|
168
|
+
async for event in stream:
|
|
169
|
+
if event.request_id != request.request_id:
|
|
170
|
+
raise AdapterCapabilityClaimError(
|
|
171
|
+
f"Adapter '{adapter_id}' returned an event for a different request."
|
|
172
|
+
)
|
|
173
|
+
if event.native_metrics:
|
|
174
|
+
metric_capabilities = {"native_metrics", "server_reported_durations"}
|
|
175
|
+
if not manifest.declared_capabilities.intersection(metric_capabilities):
|
|
176
|
+
raise AdapterCapabilityClaimError(
|
|
177
|
+
f"Adapter '{adapter_id}' returned native metrics it did not declare."
|
|
178
|
+
)
|
|
179
|
+
undeclared_metrics = {
|
|
180
|
+
metric.metric_name
|
|
181
|
+
for metric in event.native_metrics
|
|
182
|
+
if not _native_metric_is_declared(
|
|
183
|
+
metric.metric_name, manifest.native_metrics
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
if undeclared_metrics:
|
|
187
|
+
names = ", ".join(sorted(undeclared_metrics))
|
|
188
|
+
raise AdapterCapabilityClaimError(
|
|
189
|
+
f"Adapter '{adapter_id}' returned undeclared native metrics: {names}."
|
|
190
|
+
)
|
|
191
|
+
if any(
|
|
192
|
+
metric.source.value != "engine_native" for metric in event.native_metrics
|
|
193
|
+
):
|
|
194
|
+
raise AdapterCapabilityClaimError(
|
|
195
|
+
f"Adapter '{adapter_id}' returned request metrics with an invalid "
|
|
196
|
+
"source."
|
|
197
|
+
)
|
|
198
|
+
yield event
|
|
199
|
+
|
|
200
|
+
return validate_events()
|
|
201
|
+
|
|
202
|
+
async def read_metrics(self, adapter_id: str, config: AdapterConfiguration) -> MetricsResult:
|
|
203
|
+
self._validate_mode(adapter_id, config)
|
|
204
|
+
manifest = self.manifest_for(adapter_id)
|
|
205
|
+
result = await self._runner.invoke(self.get(adapter_id).read_metrics, config)
|
|
206
|
+
if result.available and "native_metrics" not in manifest.declared_capabilities:
|
|
207
|
+
raise AdapterCapabilityClaimError(
|
|
208
|
+
f"Adapter '{adapter_id}' returned native metrics it did not declare."
|
|
209
|
+
)
|
|
210
|
+
undeclared_metrics = {
|
|
211
|
+
metric.metric_name
|
|
212
|
+
for metric in result.metrics
|
|
213
|
+
if not _native_metric_is_declared(metric.metric_name, manifest.native_metrics)
|
|
214
|
+
}
|
|
215
|
+
if undeclared_metrics:
|
|
216
|
+
names = ", ".join(sorted(undeclared_metrics))
|
|
217
|
+
raise AdapterCapabilityClaimError(
|
|
218
|
+
f"Adapter '{adapter_id}' returned undeclared native metrics: {names}."
|
|
219
|
+
)
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
async def collect_fingerprint(
|
|
223
|
+
self, adapter_id: str, config: AdapterConfiguration
|
|
224
|
+
) -> RuntimeFingerprint:
|
|
225
|
+
self._validate_mode(adapter_id, config)
|
|
226
|
+
return await self._runner.invoke(self.get(adapter_id).collect_fingerprint, config)
|
|
227
|
+
|
|
228
|
+
async def abort_request(self, adapter_id: str, request_id: UUID) -> AbortResult:
|
|
229
|
+
manifest = self.manifest_for(adapter_id)
|
|
230
|
+
result = await self._runner.invoke(self.get(adapter_id).abort_request, request_id)
|
|
231
|
+
if result.supported and "cancellation" not in manifest.declared_capabilities:
|
|
232
|
+
raise AdapterCapabilityClaimError(
|
|
233
|
+
f"Adapter '{adapter_id}' accepted cancellation it did not declare."
|
|
234
|
+
)
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
async def close(self) -> None:
|
|
238
|
+
"""Close every adapter, then cancel any outstanding adapter call tasks."""
|
|
239
|
+
|
|
240
|
+
errors: list[Exception] = []
|
|
241
|
+
for adapter_id in sorted(self._adapters):
|
|
242
|
+
try:
|
|
243
|
+
await self._runner.invoke(self._adapters[adapter_id].close)
|
|
244
|
+
except Exception as error:
|
|
245
|
+
errors.append(error)
|
|
246
|
+
await self._runner.close()
|
|
247
|
+
if errors:
|
|
248
|
+
raise ExceptionGroup("One or more adapters failed to close cleanly", errors)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def default_adapter_catalog() -> AdapterCatalog:
|
|
252
|
+
"""Build the local catalog with fixture and local/attached server adapters."""
|
|
253
|
+
|
|
254
|
+
from localforge.adapters.exllamav3 import exllamav3_catalog_manifest
|
|
255
|
+
from localforge.adapters.fake import FakeEngineAdapter
|
|
256
|
+
from localforge.adapters.koboldcpp import KoboldCppEngineAdapter
|
|
257
|
+
from localforge.adapters.lightllm import LightLlmEngineAdapter
|
|
258
|
+
from localforge.adapters.llama_cpp import LlamaCppEngineAdapter
|
|
259
|
+
from localforge.adapters.lmdeploy import LmDeployEngineAdapter
|
|
260
|
+
from localforge.adapters.localai import LocalAIEngineAdapter
|
|
261
|
+
from localforge.adapters.mistral_rs import MistralRsEngineAdapter
|
|
262
|
+
from localforge.adapters.mlc_llm import MlcLlmEngineAdapter
|
|
263
|
+
from localforge.adapters.mlx_lm import MlxLmEngineAdapter
|
|
264
|
+
from localforge.adapters.nvidia_dynamo import NvidiaDynamoEngineAdapter
|
|
265
|
+
from localforge.adapters.ollama import OllamaEngineAdapter
|
|
266
|
+
from localforge.adapters.onnxruntime_genai import OnnxRuntimeGenAiEngineAdapter
|
|
267
|
+
from localforge.adapters.openai_compatible import OpenAICompatibleEngineAdapter
|
|
268
|
+
from localforge.adapters.openvino_genai import OpenVinoGenAiEngineAdapter
|
|
269
|
+
from localforge.adapters.sglang import SglangEngineAdapter
|
|
270
|
+
from localforge.adapters.tensorrt_llm import TensorRtLlmEngineAdapter
|
|
271
|
+
from localforge.adapters.tgi import TgiEngineAdapter
|
|
272
|
+
from localforge.adapters.triton import triton_tensorrt_llm_catalog_manifest
|
|
273
|
+
from localforge.adapters.vllm import VllmEngineAdapter
|
|
274
|
+
|
|
275
|
+
catalog = AdapterCatalog()
|
|
276
|
+
catalog.register(FakeEngineAdapter())
|
|
277
|
+
catalog.register(OpenAICompatibleEngineAdapter())
|
|
278
|
+
catalog.register(LlamaCppEngineAdapter())
|
|
279
|
+
catalog.register(OllamaEngineAdapter())
|
|
280
|
+
catalog.register(VllmEngineAdapter())
|
|
281
|
+
catalog.register(SglangEngineAdapter())
|
|
282
|
+
catalog.register(TensorRtLlmEngineAdapter())
|
|
283
|
+
catalog.register(LmDeployEngineAdapter())
|
|
284
|
+
catalog.register(LightLlmEngineAdapter())
|
|
285
|
+
catalog.register(TgiEngineAdapter())
|
|
286
|
+
catalog.register(LocalAIEngineAdapter())
|
|
287
|
+
catalog.register(KoboldCppEngineAdapter())
|
|
288
|
+
catalog.register(MlxLmEngineAdapter())
|
|
289
|
+
catalog.register(MlcLlmEngineAdapter())
|
|
290
|
+
catalog.register(MistralRsEngineAdapter())
|
|
291
|
+
catalog.register(OpenVinoGenAiEngineAdapter())
|
|
292
|
+
catalog.register(OnnxRuntimeGenAiEngineAdapter())
|
|
293
|
+
catalog.register_catalog_entry(triton_tensorrt_llm_catalog_manifest())
|
|
294
|
+
catalog.register_catalog_entry(exllamav3_catalog_manifest())
|
|
295
|
+
catalog.register(NvidiaDynamoEngineAdapter())
|
|
296
|
+
return catalog
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _native_metric_is_declared(name: str, declarations: list[str]) -> bool:
|
|
300
|
+
if name in declarations:
|
|
301
|
+
return True
|
|
302
|
+
return any(
|
|
303
|
+
declaration.endswith("*")
|
|
304
|
+
and name.startswith(declaration[:-1])
|
|
305
|
+
and len(name) > len(declaration[:-1])
|
|
306
|
+
for declaration in declarations
|
|
307
|
+
)
|