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.
Files changed (79) hide show
  1. localforge_inference_lab-0.1.0/.gitignore +24 -0
  2. localforge_inference_lab-0.1.0/PKG-INFO +67 -0
  3. localforge_inference_lab-0.1.0/README.md +47 -0
  4. localforge_inference_lab-0.1.0/pyproject.toml +71 -0
  5. localforge_inference_lab-0.1.0/src/localforge/__init__.py +3 -0
  6. localforge_inference_lab-0.1.0/src/localforge/adapters/__init__.py +19 -0
  7. localforge_inference_lab-0.1.0/src/localforge/adapters/catalog.py +307 -0
  8. localforge_inference_lab-0.1.0/src/localforge/adapters/contracts.py +295 -0
  9. localforge_inference_lab-0.1.0/src/localforge/adapters/exllamav3.py +35 -0
  10. localforge_inference_lab-0.1.0/src/localforge/adapters/fake.py +573 -0
  11. localforge_inference_lab-0.1.0/src/localforge/adapters/koboldcpp.py +44 -0
  12. localforge_inference_lab-0.1.0/src/localforge/adapters/library_pipeline.py +863 -0
  13. localforge_inference_lab-0.1.0/src/localforge/adapters/library_worker.py +236 -0
  14. localforge_inference_lab-0.1.0/src/localforge/adapters/lightllm.py +44 -0
  15. localforge_inference_lab-0.1.0/src/localforge/adapters/llama_cpp.py +1344 -0
  16. localforge_inference_lab-0.1.0/src/localforge/adapters/lmdeploy.py +44 -0
  17. localforge_inference_lab-0.1.0/src/localforge/adapters/localai.py +157 -0
  18. localforge_inference_lab-0.1.0/src/localforge/adapters/mistral_rs.py +59 -0
  19. localforge_inference_lab-0.1.0/src/localforge/adapters/mlc_llm.py +44 -0
  20. localforge_inference_lab-0.1.0/src/localforge/adapters/mlx_lm.py +45 -0
  21. localforge_inference_lab-0.1.0/src/localforge/adapters/nvidia_dynamo.py +142 -0
  22. localforge_inference_lab-0.1.0/src/localforge/adapters/ollama.py +964 -0
  23. localforge_inference_lab-0.1.0/src/localforge/adapters/onnxruntime_genai.py +45 -0
  24. localforge_inference_lab-0.1.0/src/localforge/adapters/openai_compatible.py +1573 -0
  25. localforge_inference_lab-0.1.0/src/localforge/adapters/openvino_genai.py +45 -0
  26. localforge_inference_lab-0.1.0/src/localforge/adapters/prometheus.py +268 -0
  27. localforge_inference_lab-0.1.0/src/localforge/adapters/protocol.py +62 -0
  28. localforge_inference_lab-0.1.0/src/localforge/adapters/runner.py +82 -0
  29. localforge_inference_lab-0.1.0/src/localforge/adapters/serving_engine.py +1155 -0
  30. localforge_inference_lab-0.1.0/src/localforge/adapters/sglang.py +46 -0
  31. localforge_inference_lab-0.1.0/src/localforge/adapters/tensorrt_llm.py +44 -0
  32. localforge_inference_lab-0.1.0/src/localforge/adapters/tgi.py +50 -0
  33. localforge_inference_lab-0.1.0/src/localforge/adapters/triton.py +40 -0
  34. localforge_inference_lab-0.1.0/src/localforge/adapters/vllm.py +43 -0
  35. localforge_inference_lab-0.1.0/src/localforge/api/__init__.py +1 -0
  36. localforge_inference_lab-0.1.0/src/localforge/api/dependencies.py +44 -0
  37. localforge_inference_lab-0.1.0/src/localforge/api/experiments.py +1281 -0
  38. localforge_inference_lab-0.1.0/src/localforge/api/main.py +438 -0
  39. localforge_inference_lab-0.1.0/src/localforge/api/playground.py +245 -0
  40. localforge_inference_lab-0.1.0/src/localforge/api/profiles.py +830 -0
  41. localforge_inference_lab-0.1.0/src/localforge/api/runs.py +641 -0
  42. localforge_inference_lab-0.1.0/src/localforge/api/schemas.py +334 -0
  43. localforge_inference_lab-0.1.0/src/localforge/api/settings.py +132 -0
  44. localforge_inference_lab-0.1.0/src/localforge/api/workloads.py +1115 -0
  45. localforge_inference_lab-0.1.0/src/localforge/cli.py +140 -0
  46. localforge_inference_lab-0.1.0/src/localforge/comparisons.py +1114 -0
  47. localforge_inference_lab-0.1.0/src/localforge/core/__init__.py +1 -0
  48. localforge_inference_lab-0.1.0/src/localforge/core/models.py +890 -0
  49. localforge_inference_lab-0.1.0/src/localforge/core/urls.py +48 -0
  50. localforge_inference_lab-0.1.0/src/localforge/dataset_import.py +320 -0
  51. localforge_inference_lab-0.1.0/src/localforge/dataset_storage.py +121 -0
  52. localforge_inference_lab-0.1.0/src/localforge/metrics.py +1129 -0
  53. localforge_inference_lab-0.1.0/src/localforge/runs.py +575 -0
  54. localforge_inference_lab-0.1.0/src/localforge/security/__init__.py +1 -0
  55. localforge_inference_lab-0.1.0/src/localforge/security/local_session.py +54 -0
  56. localforge_inference_lab-0.1.0/src/localforge/storage/__init__.py +1 -0
  57. localforge_inference_lab-0.1.0/src/localforge/storage/database.py +312 -0
  58. localforge_inference_lab-0.1.0/src/localforge/storage/paths.py +22 -0
  59. localforge_inference_lab-0.1.0/src/localforge/storage/repositories.py +423 -0
  60. localforge_inference_lab-0.1.0/src/localforge/system/__init__.py +1 -0
  61. localforge_inference_lab-0.1.0/src/localforge/system/inspection.py +703 -0
  62. localforge_inference_lab-0.1.0/src/localforge/web/assets/index-BNnJ3ITv.js +12 -0
  63. localforge_inference_lab-0.1.0/src/localforge/web/assets/index-PG0RTmw6.css +1 -0
  64. localforge_inference_lab-0.1.0/src/localforge/web/favicon.svg +4 -0
  65. localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-engine-detail.jpg +0 -0
  66. localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-engine-hero.jpg +0 -0
  67. localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-experiments-hero.jpg +0 -0
  68. localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-inference-hero.jpg +0 -0
  69. localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-learning-hero.jpg +0 -0
  70. localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-overview-hero.jpg +0 -0
  71. localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-runs-hero.jpg +0 -0
  72. localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-system-hero.jpg +0 -0
  73. localforge_inference_lab-0.1.0/src/localforge/web/images/localforge-workloads-hero.jpg +0 -0
  74. localforge_inference_lab-0.1.0/src/localforge/web/index.html +19 -0
  75. localforge_inference_lab-0.1.0/src/localforge/web_ui.py +42 -0
  76. localforge_inference_lab-0.1.0/src/localforge/worker/__init__.py +1 -0
  77. localforge_inference_lab-0.1.0/src/localforge/worker/__main__.py +58 -0
  78. localforge_inference_lab-0.1.0/src/localforge/worker/execution.py +1405 -0
  79. 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,3 @@
1
+ """LOCALFORGE Inference Lab backend."""
2
+
3
+ __version__ = "0.1.0"
@@ -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
+ )