You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .agents/adding-backends.md
+111-2Lines changed: 111 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -34,7 +34,7 @@ The build matrix is data-only YAML at `.github/backend-matrix.yml` (not inside `
34
34
35
35
**Without an entry here no image is ever built or pushed, and the gallery entry in `backend/index.yaml` will point at a tag that does not exist.** The `dockerfile:` field must point at `./backend/Dockerfile.<lang>` matching the language bucket from step 1 (e.g. `Dockerfile.python`, `Dockerfile.golang`, `Dockerfile.rust`). The `tag-suffix` must match the `uri:` in the corresponding `backend/index.yaml` image entry exactly.
36
36
37
-
**`scripts/changed-backends.js` registration — REQUIRED for any new dockerfile suffix.** This is the single most common omission, because it has no effect on the PR that adds the backend (when no prior path filter could catch it anyway) — it only breaks the *next* PR that touches your backend's directory, which then gets zero CI jobs and looks broken for unrelated reasons. Edit `scripts/changed-backends.js:inferBackendPath` and add a branch BEFORE the more-generic suffixes:
37
+
**Path-filter registration — REQUIRED for any new dockerfile suffix.** This is the single most common omission, because it has no effect on the PR that adds the backend (when no prior path filter could catch it anyway) — it only breaks the *next* PR that touches your backend's directory, which then gets zero CI jobs and looks broken for unrelated reasons. Edit `scripts/lib/backend-filter.mjs:inferBackendPath` and add a branch BEFORE the more-generic suffixes:
38
38
39
39
```js
40
40
if (item.dockerfile.endsWith("<your-dockerfile-suffix>")) {
@@ -54,7 +54,9 @@ for (const e of m.include.filter(e => e.backend === '<your-backend>')) {
54
54
}"
55
55
```
56
56
57
-
A quick way to find the right insertion point: `grep -n 'item.dockerfile.endsWith' scripts/changed-backends.js`.
57
+
A quick way to find the right insertion point: `grep -n 'item.dockerfile.endsWith' scripts/lib/backend-filter.mjs`.
58
+
59
+
If your backend consumes a *shared* build input that lives outside its own directory (a new script under `scripts/build/`, a new file copied into every image), add a rule to `SHARED_BUILD_INPUTS` in the same file — the per-backend prefix match cannot see those, and a miss ships your change to no image at all. See `scripts/lib/backend-filter_test.mjs` for the pattern; `make test-ci-scripts` runs it.
58
60
59
61
**`bump_deps.yaml` registration — REQUIRED for any backend pinning an upstream commit.** If your backend's Makefile has a `*_VERSION?=<sha>` pin to a third-party repo, the daily auto-bump bot at `.github/workflows/bump_deps.yaml` won't notice it unless you register the backend in its matrix. The bot runs `.github/bump_deps.sh` which `grep`s for `^$VAR?=` in the Makefile you list — so the pin MUST live in the Makefile (not in a separate shell script). The bump for ds4 (#9761) had to walk this back because the original landed the pin in `prepare.sh`, which the bot can't see. Pattern (for `antirez/ds4`):
60
62
@@ -102,6 +104,24 @@ Multi-arch backends are NOT a single matrix entry with `platforms: 'linux/amd64,
102
104
103
105
Entries whose `dockerfile` is `./backend/Dockerfile.{llama-cpp,ik-llama-cpp,turboquant}` must also set a `builder-base-image` field pointing at a prebuilt base from `quay.io/go-skynet/ci-cache:base-grpc-*` (CI builds these via `.github/workflows/base-images.yml`). The mapping is by `(build-type, platforms)` — see existing entries for the pattern. CI uses these prebuilt bases to skip the gRPC compile (~25–35 min cold). Local `make backends/<name>` ignores `builder-base-image` and uses the from-source path inside the Dockerfile, so you don't need quay access for local builds.
104
106
107
+
### Cover every OS the project supports (Linux **and** Darwin)
108
+
109
+
`.github/backend-matrix.yml` has two matrices, and they are the source of truth for which OS a backend ships on:
110
+
111
+
- `include:`— the **Linux** matrix (x86_64 + arm64; CPU and CUDA / ROCm / SYCL / Vulkan).
112
+
- `includeDarwin:`— the **macOS / Apple Silicon** matrix (arm64; Metal where the engine supports it, otherwise a native arm64 CPU build).
113
+
114
+
**A new backend must target every OS it can build for — do not ship Linux-only by default.** A backend that appears only under `include:` is silently unavailable on macOS even when its code would run there. Most C/C++/GGML engines build on Darwin out of the box (ggml defaults `GGML_METAL=ON` on Apple, so a plain build is Metal-enabled), and many Python backends do too (CPU / MPS wheels). If a backend genuinely cannot support an OS (e.g. CUDA-only, no CPU variant), state that in the PR description instead of omitting it silently.
115
+
116
+
Wiring a backend into `includeDarwin:` is more than the matrix entry:
117
+
118
+
1. **`includeDarwin:` entry** — `tag-suffix: "-metal-darwin-arm64-<backend>"`, `build-type: "metal"`, `lang: "go"` for go+ggml backends; omit `build-type` for the bespoke C++ ones (llama-cpp / ds4 / privacy-filter). Match an existing entry of the same shape.
119
+
2. **`backend/index.yaml`** — add `metal:` to the backend's `capabilities` map (main and `-development`) and concrete `metal-<backend>` / `metal-<backend>-development` image entries pointing at the `-metal-darwin-arm64-<backend>` images.
120
+
3. **C/C++ backends only** — add an `inferBackendPathDarwin` case in `scripts/lib/backend-filter.mjs` returning `backend/cpp/<backend>/` (the generic fallthrough assumes `backend/<lang>/`, which is wrong for a C++ source tree driven with `lang: go`), and give `run.sh` a Darwin branch that exports `DYLD_LIBRARY_PATH` instead of `LD_LIBRARY_PATH`. If the build is bespoke (single `grpc-server` + dylib bundling), model it on `scripts/build/ds4-darwin.sh` and add a `backends/<backend>-darwin` make target plus a gated step in `.github/workflows/backend_build_darwin.yml`.
121
+
4. **C++ proto gotcha** — if the backend compiles the generated gRPC/protobuf in a separate CMake target (e.g. `hw_grpc_proto`), that target must link `protobuf::libprotobuf` + `gRPC::grpc++` so the Homebrew include dirs propagate; otherwise macOS fails with `google/protobuf/runtime_version.h not found` (Linux hides this because apt headers sit in `/usr/include`).
122
+
123
+
The CI path filter only builds a backend on a PR when a file under its directory changes, so a darwin-only YAML edit builds nothing — touch a file under `backend/<lang>/<backend>/` (a one-line comment is enough) in the same PR.
124
+
105
125
## 3. Add Backend Metadata to `backend/index.yaml`
| `backendBuildTagPreferenceRules` | build tags (`cuda`, `rocm`, `metal`) | installed build directory names, as a substring | alias resolution in `ListSystemBackends` |
245
+
| `engineNamePreferenceRules` | engine names (`vllm`, `llama-cpp`, `mlx`) | a gallery entry's `backend:`, as a substring | gallery variant ranking |
246
+
| `servingFeaturePreferenceTokens` | serving features (`dflash`, `mtp`) | a gallery entry's `tags:`, compared whole and case-insensitively, and nothing else | gallery variant ranking, one rank below the engine |
247
+
248
+
**Putting a token in the wrong table matches nothing and does not error**: every
249
+
candidate scores equal and the next sort key decides, so the preference silently
250
+
stops existing. The block comment above all three tables spells the contract out.
251
+
252
+
The serving feature table is the odd one: it is not keyed by capability, because
253
+
no hardware prefers a plain build over an equivalent faster build of the same
254
+
weights. It reads a declared tag and nothing else. The entry name was the
255
+
original signal and is gone: a naming convention is not a contract, and names
256
+
are author-supplied free text where a short marker like `mtp` turns up inside
257
+
unrelated words or on weights whose entry enables nothing.
258
+
`overrides.options` was rejected for the mirror-image reason: `spec_type:`is
259
+
llama.cpp's config vocabulary, whereas a cross-backend ranking decision must
260
+
work the same for `ds4`'s `mtp_path:` and `sglang`'s `speculative_algorithm:`.
261
+
262
+
**If your backend can serve the same weights faster** (speculative decoding,
263
+
multi-token prediction), say so in the docs for its gallery entries so curators
264
+
tag them: the tagging rule and the per-backend evidence table live in
265
+
[adding-gallery-models.md](adding-gallery-models.md). A backend never needs to
266
+
appear in the token table itself; it ranks builds, not engines.
267
+
268
+
Leaving your backend out is a valid choice when no ordering can be justified for
269
+
it. It then ranks below every known engine and selection falls back to size,
270
+
which is the behaviour that predates preference.
271
+
272
+
**Leaving a whole capability out is not.** A missing row gives that host an
273
+
empty preference list, so size alone decides among everything that survives the
274
+
filters, and the filter will not save you: `IsBackendCompatible`derives hardware
275
+
support from the engine NAME, so `vllm` and `sglang` carry no darwin, cuda, rocm
276
+
or sycl token and are never dropped on a host with no GPU. That is why `default`
277
+
(no usable accelerator, including a GPU under the 4 GiB VRAM floor) and
278
+
`darwin-x86`both have rows putting `llama-cpp` first. Every capability
279
+
`getSystemCapabilities()`can return needs a row unless every engine really is
280
+
equally at home there. When you add one, enumerate the engines you are demoting
281
+
rather than relying on them falling through unmatched: unmatched engines all tie
282
+
with each other, so size decides among them.
283
+
284
+
## Documenting the backend (README + docs)
285
+
286
+
A backend is not "added" until it is discoverable. Update the user-facing docs:
287
+
288
+
- **`docs/content/features/backends.md`** - add the backend to the right
289
+
category in the "LocalAI supports various types of backends" list (and add a
290
+
new category if it introduces a new modality, e.g. sound classification).
291
+
- If the backend introduces a **new API surface** (a new endpoint or a realtime
292
+
capability), document it under `docs/content/` where its area lives (audio,
293
+
vision, etc.) and follow the api-endpoints checklist in
**If the backend is a native C/C++/GGML engine created and maintained by the
297
+
LocalAI team** (a from-scratch port like `parakeet.cpp`, `ced.cpp`,
298
+
`vibevoice.cpp`, `rf-detr.cpp`, not a wrapper around a third-party runtime), it
299
+
ALSO belongs in the top-level **`README.md`** table under "native C/C++/GGML
300
+
engines ... developed and maintained by the LocalAI project itself". Add a row
301
+
linking the upstream engine repo with a one-line description. This is the
302
+
project's showcase of its own engines; a new in-house backend that is missing
303
+
from it is a documentation bug.
304
+
201
305
## 5. Verification Checklist
202
306
203
307
After adding a new backend, verify:
204
308
205
309
- [ ] Backend directory structure is complete with all necessary files
206
310
- [ ] Build configurations added to `.github/backend-matrix.yml` for all desired platforms (per-arch entries with `platform-tag` for multi-arch; `builder-base-image` for llama-cpp / ik-llama-cpp / turboquant)
311
+
- [ ] **OS coverage considered**: added to `includeDarwin:` (macOS/Apple Silicon) if the backend can build there — with the `backend/index.yaml` `metal:` capability + `metal-<backend>` image entries, a `run.sh` Darwin/DYLD branch and `inferBackendPathDarwin` case (in `scripts/lib/backend-filter.mjs`) for C++ backends — or the PR explains why an OS is unsupported. Do not ship Linux-only by default.
207
312
- [ ] Meta definition added to `backend/index.yaml` in the `## metas` section
208
313
- [ ] Image entries added to `backend/index.yaml` for all build variants (latest + development)
209
314
- [ ] Tag suffixes match between workflow file and index.yaml
210
315
- [ ] Makefile updated with all 6 required changes (`.NOTPARALLEL`, `prepare-test-extra`, `test-extra`, backend definition, docker-build target eval, `docker-build-backends`)
211
316
- [ ] No YAML syntax errors (check with linter)
212
317
- [ ] No Makefile syntax errors (check with linter)
213
318
- [ ] Follows the same pattern as similar backends (e.g., if it's a transcription backend, follow `faster-whisper` pattern)
319
+
- [ ] **`Load` validates its input and refuses models it can't serve.** When a model config has no explicit `backend:`, the model loader greedily probes *every* installed backend with the model's name and binds to the first `Load` that succeeds — an accept-anything `Load` will capture arbitrary LLMs (issue #9287). Backends that load a real artefact get this for free (the load fails); backends with no artefact must gate on the name: `opus` accepts only its own name (or none), `local-store` requires the `store.NamespacePrefix` namespace marker sent by `core/backend/stores.go`.
320
+
- [ ] **Gallery variant ranking considered**: if this backend should be preferred over another on some hardware, it is listed in `engineNamePreferenceRules` (NOT `backendBuildTagPreferenceRules`, NOT `servingFeaturePreferenceTokens`) in `pkg/system/capabilities.go`. A missing entry silently ranks it last and lets the next sort key decide.
321
+
- [ ] Documented: added to the category list in `docs/content/features/backends.md` (and any new endpoint/realtime capability documented under `docs/content/`)
322
+
- [ ] If it is an in-house native C/C++/GGML engine, added to the maintained-engines table in the top-level `README.md`
0 commit comments