Federate your local LLM nodes — Macs, GPU boxes, small homelab rigs — into one OpenAI-compatible endpoint with hardware-aware routing across specialists.
You probably already run Ollama or vLLM on one box. Slancha-Mesh is the layer on top: it discovers every node on your LAN or tailnet, learns what each one is good at (code / reasoning / multilingual / small-and-fast), and routes each prompt to the right one. No central server required (one is optional), and no data leaves your hardware. Apache-2.0.
Status: the discovery, routing, and heartbeat substrate is stable and
well-tested (~1,000 unit tests plus live demos on GB10 hardware). The
specialist catalog ships one bring-up-validated card
(qwen3-coder-30b-a3b-fp8) alongside ten draft cards spanning Ollama and
vLLM; see docs/CATALOG_STATUS.md for per-card
validation status.
Slancha-Mesh has three pieces:
- Nodes run your models on your hardware (through Ollama, vLLM,
llama.cpp, or MLX) and expose both an OpenAI-compatible endpoint and a
small
/modelsself-description. - Discovery builds a routing table by pulling each node's
self-description — over your LAN (an explicit
--peerlist) or a Tailscale / Headscale tailnet. - The router presents one OpenAI
/v1endpoint. For each request it looks up the target specialist, picks the best reachable node — using live queue depth and measured p95 latency to break ties — and proxies the call, falling through to the next node on failure.
The block below installs the project, serves a model through your existing Ollama, and routes a real prompt through the mesh.
# 1. Install. uv is fastest; the plain pip path works identically.
# uv: curl -LsSf https://astral.sh/uv/install.sh | sh
git clone https://github.com/SlanchaAi/slancha-mesh.git
cd slancha-mesh
uv venv && source .venv/bin/activate && uv pip install -e ".[dev]"
# pip-only alternative (no uv):
# python -m venv .venv && source .venv/bin/activate && pip install -e ".[dev]"
# 2. Pull a model your hardware can serve, through your existing Ollama:
ollama pull qwen2.5-coder:7b-instruct-q4_K_M
# 3. Start the node — adopts your running Ollama daemon, serves node-info on :8088.
slancha-mesh up --specialist qwen2.5-coder-7b-q4-ollama
# 4. In another terminal: a drop-in OpenAI /v1 endpoint over your mesh.
slancha-mesh router --peer 127.0.0.1 --port 8080
# 5. In a third terminal: ask a question — same shape as api.openai.com /v1.
curl -s http://localhost:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen2.5-coder-7b-q4-ollama",
"messages": [{"role":"user","content":"reverse a string in python"}]
}' | jq -r '.choices[0].message.content'Expected output from step 5 — a short code answer:
You can reverse a string in Python with a slice:
s = "hello"
print(s[::-1]) # 'olleh'
That response came back from localhost:8080 — your local router
discovered the node and proxied the prompt to the model on your own
Ollama daemon. No cloud, no API key, one box.
Install the classifier extra and start the router with --auto-route;
then model: "auto" routes each prompt by classified domain and
difficulty (a built-in mmBERT-small + treelite classifier, ~ms per
prompt, fully local — the model weights ship in the wheel, so this
works air-gapped):
pip install "slancha-mesh[classifier]"
slancha-mesh router --peer 127.0.0.1 --port 8080 --auto-route
curl -s http://localhost:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model": "auto", "messages": [{"role":"user","content":"reverse a string in python"}]}'The X-Slancha-Specialist response header names the model it picked,
and the router log shows the classified signals per request. Coding
prompts land on your coder, hard general prompts on your strongest
generalist, easy ones on the smallest model that covers them.
To inspect the routing table instead of sending a prompt:
# Reads local node-info, skips Tailscale, shows the node reachable
# with the specialist bound to your Ollama daemon's URL.
slancha-mesh discover --peer 127.0.0.1The router can emit a neutral usage event per completed inference — token
counts + routing metadata only, never prompt or completion bodies — to a
receiver you run. It's off by default; set SLANCHA_USAGE_SINK_URL to turn
it on. Events are written to a local append-only JSONL spool (off the request
path, so telemetry never blocks or fails a completion) and a background task
drains them at-least-once with retry/backoff; the receiver dedups on
request_id.
SLANCHA_USAGE_SINK_URL=http://127.0.0.1:8977/v1/usage \
slancha-mesh router --peer 127.0.0.1 --port 8080| Env var | Default | Meaning |
|---|---|---|
SLANCHA_USAGE_SINK_URL |
(unset → off) | Receiver URL. Unset = no telemetry (no spool, no drain). |
SLANCHA_USAGE_SINK_TOKEN |
(none) | Optional bearer, sent only to that URL. |
SLANCHA_USAGE_SPOOL_PATH |
~/.slancha/usage-spool.jsonl |
Local spool file (mode 0600). |
SLANCHA_USAGE_DRAIN_INTERVAL_S |
5 |
Seconds between drain passes. |
The event carries token counts, not cost — the receiver prices them (mesh
has no pricing). The router has no per-user principal (auth is a shared node
token), so user_id is the caller-asserted OpenAI user field when present,
else "unattributed". If the receiver scopes this emitter's token to a fixed
set of actors, include "unattributed" in it (or don't scope it) — otherwise
that share of usage is refused and silently dropped by the receiver.
The planner recommends an engine per OS. The table below is what actually serves today versus what's recommended; pick the model size your VRAM fits.
| Hardware | Backend | Notes |
|---|---|---|
| Apple Silicon Mac (e.g. 16GB) | MLX or Ollama | Planner prefers MLX (native Metal via mlx_lm); set mlx_repo on the card. Ollama is the zero-config fallback. Good for 7B Q4. |
| Windows + NVIDIA (e.g. 8GB) | Ollama | Native CUDA. vLLM is Linux/WSL-only. Good for 7B Q4. |
| Linux + NVIDIA ≥24GB (3090/4090) | vLLM | Throughput; FP8 on Ada/Hopper+, else AWQ. Ollama also works. |
| Linux + NVIDIA <24GB | Ollama | GGUF fits. |
| GB10 / DGX Spark (aarch64 unified) | Ollama | No official vLLM sm_121 wheels yet. |
| CPU-only | llama.cpp or Ollama | Planner prefers llama.cpp (native llama-server); set gguf_path on the card. Ollama also works. |
Ollama is the universal zero-config backend; vLLM adds throughput on
Linux. MLX (Apple Silicon, mlx_repo) and llama.cpp (any box with a
GGUF, gguf_path) are native paths the planner recommends.
No Tailscale required. On a trusted LAN, exposing a node's node-info
endpoint on all interfaces needs an explicit acknowledgement:
SLANCHA_AUTH_REQUIRED=false (the network is your trust boundary) or a
shared SLANCHA_NODE_TOKEN.
On box A (say a Mac mini, 192.168.1.10):
OLLAMA_HOST=0.0.0.0:11434 ollama serve # one-time: bind Ollama on the LAN
ollama pull phi4-mini:3.8b-q4_K_M
SLANCHA_AUTH_REQUIRED=false \
slancha-mesh up --specialist phi-4-mini-q4-ollama --node-info-host 0.0.0.0On box B (say a 3090 box, 192.168.1.20):
ollama pull qwen2.5-coder:7b-instruct-q4_K_M
SLANCHA_AUTH_REQUIRED=false \
slancha-mesh up --specialist qwen2.5-coder-7b-q4-ollama --node-info-host 0.0.0.0From either box (or your laptop), federate:
slancha-mesh discover --peer 192.168.1.10 --peer 192.168.1.20You get a routing table that knows box A is good at small/easy prompts
and box B is good at code. The router takes a classifier verdict and
picks the right node from domain + difficulty_tiers + live queue
depth + measured p95 latency.
See docs/HOMELAB.md for the longer walkthrough
(2-GPU rigs, mixed Mac+Linux, fault-tolerant routing).
| Existing tool | What it does well | What Slancha-Mesh adds |
|---|---|---|
| Ollama / LM Studio | Easy single-box model serving; great UX. | Federating N such boxes into one routed endpoint with hardware-aware specialist allocation. Ollama is a first-class backend here. |
| exo / petals | Splits one model's layers across nodes for memory-bound inference. | The opposite topology: route different models to different nodes. Complementary — use exo to run one 70B split across 4 Macs; use Slancha-Mesh to size each box for a specialist and route which specialist answers. |
| vLLM / llama.cpp directly | Best-in-class single-engine throughput. | The mesh treats them as backends behind one /v1/chat/completions seam; the engine choice happens behind that seam. |
| LiteLLM / OpenRouter | Unified API across N hosted providers. | The same OpenAI-compatible surface, but every node is yours on your hardware — no third-party billing, no data egress. |
| llama-swap | OpenAI-compatible proxy that hot-swaps which local model process runs on one box — good for VRAM-constrained model-juggling. | Cross-node discovery and federation: the router picks which node answers, not just which model is loaded on the one box. Complementary if you already run llama-swap on a node. |
| SGLang | High-performance serving engine (RadixAttention prefix caching, structured output, strong tool-call throughput). | It's an engine, not an orchestrator. Backends are a pluggable seam here; SGLang is a natural fit for that seam and is on the roadmap, not yet wired. |
| Module | What it does |
|---|---|
mesh/cli.py |
The slancha-mesh CLI: up / discover / status / serve / doctor / plan. |
mesh/discovery.py |
Pull discovery: walk a tailnet or an explicit --peer list into routes with host-pinned node_urls. |
mesh/node_server.py |
build_node() — the daemon and /models self-description share one registry. |
mesh/registry.py |
Event-sourced, thread-safe, deterministic replay. |
mesh/backends.py |
VLLMBackend, OllamaBackend, LlamaCppBackend, MLXBackend, NullBackend. The BaseBackend protocol is the seam — one class per engine. |
mesh/serve.py |
ServeDaemon boots backends and runs the heartbeat loop. |
mesh/select.py |
select_mesh_route — classifier verdict + snapshot → ranked routes with cloud fallback. |
mesh/allocator.py |
model_fit_score plus three cluster-allocation strategies. |
mesh/probe.py |
Hardware/network probe with GB10 unified-memory detection. |
mesh/catalog/*.toml |
11 specialist cards (1 validated + 10 draft). |
mesh/tests/ |
~1,000 hermetic unit tests plus live-vLLM integration tests (gated by VLLM_LIVE_URL). |
| Backend | Status | How to use |
|---|---|---|
ollama |
Wired | Mac, AMD, Windows + NVIDIA, GB10, small NVIDIA. Adopts your running Ollama daemon at 127.0.0.1:11434 (or OLLAMA_HOST=0.0.0.0:11434 for LAN). OLLAMA_PORT honored. |
vllm |
Wired (kernel-gated on Blackwell) | Linux/WSL + CUDA. Native FP8 on Hopper/Ada; Marlin weight-only fallback on Blackwell consumer (sm_120/sm_121). |
llamacpp |
Wired | Any box with a GGUF — the CPU-only / no-CUDA-no-Metal path. Owns (or adopts) a llama-server subprocess. Set gguf_path on the card (local path or repo:file HF id); needs llama-server on PATH. |
mlx |
Wired | Apple Silicon native (Metal). Owns an mlx_lm.server subprocess. Set mlx_repo on the card (an mlx-community/... HF repo); needs mlx_lm installed. Refuses on non-Darwin/arm64 hosts. |
Once you outgrow a single LAN — boxes on different networks, no port
forwarding, encrypted transport — Slancha-Mesh's pull discovery walks a
Tailscale / Headscale tailnet for tag:specialist peers and pulls each
node's /models over WireGuard. The tailnet ACL is the credential;
nothing is exposed to the open internet.
slancha-mesh up --tailnet --auto --key tskey-...
slancha-mesh discover --tailnetSee ONBOARDING.md for the full tailnet bring-up
(tagging, ACL shape, MagicDNS resolution, the tag:specialist membrane).
# Unit tests (~3s)
uv run pytest mesh/tests/ -v
# Live vLLM integration tests (require a running vLLM)
VLLM_LIVE_URL=http://127.0.0.1:8001 \
uv run pytest mesh/tests/test_integration_vllm.py -v
# Probe the local machine
uv run python -m mesh.probe --pretty
# What would the mesh allocate to this box?
slancha-mesh plan
# Diagnose tagged-but-undiscoverable nodes, ACL gaps, etc.
slancha-mesh doctor
# Run the node boot-persistent (systemd / launchd / Windows task) — see NODE_SETUP.md
slancha-mesh service install # defaults to `up --auto`
# Bring up a Spark node end-to-end (probe → vLLM serve → smoke test).
# --trust-remote-code and HF-revision pinning are opt-in (supply-chain safe by default):
# MESH_TRUST_REMOTE_CODE=1 MESH_MODEL_REVISION=<sha> bash mesh/scripts/bring-up-spark.sh ...
bash mesh/scripts/bring-up-spark.sh qwen3-coder-30b-a3b-fp8 8001On GB10 (Blackwell sm_121), vLLM has no official FP8 GEMM kernel yet, so
the 30B-FP8 weights currently OOM. The validated bring-up served a
smaller cached model under the catalog's --served-model-name to
exercise the routing, discovery, and backend-lifecycle path — all of
which are model-agnostic.
Discovery and the optional central registry both default to :8088 but
mean opposite things:
- Pull / per-node
/models(the default; whatslancha-mesh upruns): each node serves its own self-description, and a consumer walks the tailnet or your--peerlist and pulls. No central server, no shared write token. - Push / central registry (optional, for ops dashboards or
slancha-api integration): one shared
MeshRegistrybehindPOST /heartbeat+GET /registry. Run it standalone with thedocker/image, or mountmesh.registry_appinto slancha-api (Wire to slancha-api).
There is exactly one default per role. Mixing them on the same path produces a node that's alive but invisible (in nobody's routing table) or one that shows up twice.
| Pull (discover) — the local default | Push (registry) — the service tier | |
|---|---|---|
| Use when | one box, a LAN, or a tailnet you can name | a multi-tenant gateway, ops dashboards, or slancha-api — anything aggregating nodes it doesn't control |
| Who decides membership | the consumer walks the network and finds live nodes | the node announces itself to a central registry and heartbeats |
| Central server | none | a MeshRegistry control plane |
| A dead node | silently drops out of discovery | stops heartbeating; the registry ages it out |
The only time both run at once is when a node lives on the local mesh and also pushes to a separate hosted gateway for a different audience — and even then, each consumer builds its routing table from exactly one plane, so the node is counted once.
Rule of thumb: local / LAN / tailnet → pull. service / cloud / ops → push.
On a tailnet the ACL is deny-by-default, so ports are not interchangeable. One rule the rest of the docs assume:
If a node reports itself as discoverable, its advertised model URL must be reachable from the documented gateway under the documented ACL.
On a Tailscale / Headscale mesh:
:8003(vLLM) /:8004(HF) — the model ports, and the only ones the gateway ACL opens (tag:gateway -> tag:specialist:8003,8004).--base-portdefaults to8003, so you land here automatically.:8088— node-info / discovery only (the/modelsself-description the gateway pulls). Never a model port.:8000(slancha-local) /:8001(vLLM dev) — off-ACL. A node serving here registers fine but is unroutable: the gateway can't dial it. Re-serve on:8003(slancha-mesh up --base-port 8003).slancha-mesh doctorwarns on exactly this.
LAN mode (--peer, no Tailscale) has no ACL membrane, so any reachable
port is fine; the invariant only binds where an ACL gates reachability.
- Bandwidth, not just VRAM, decides where the interactive hot path
goes. Measured (zero-install ctypes bench, MBU 0.82): an RTX PRO 6000
Blackwell sustains ~1467 GB/s against GB10's 273 GB/s datasheet figure.
Live on GB10, a small resident model decodes at 46 tok/s while a large
one on the same box drops to 8 tok/s — a 30 tok/s interactive floor
discriminates correctly between the two. Full derivation:
docs/SIZING_BANDWIDTH_BRIEF.md. - Unified-memory nodes get
RAM − 8GB OS reserveas their effective model-fit budget. GB10 reports[N/A]for VRAM via nvidia-smi; the probe detects this and falls back to RAM with a warning. - The tiered allocator diversifies before duplicating. A 2-Spark cluster gets one math and one code specialist; a 5-Spark cluster gets three tier-1 specialists plus two replicas of the highest-traffic domain.
- Routes are pre-ranked at snapshot time, not per request.
- Snapshot replay is pure from the event log, and the registry is
thread-safe under concurrent
POST /heartbeat. - The backend abstraction swaps engines without router changes.
ServeDaemondoesn't know vLLM or Ollama exists — onlyBaseBackenddoes. - Adopt, don't own, the local daemon.
VLLMBackendadopts a port-busyvllm serve;OllamaBackendadopts your running Ollama daemon. The mesh never SIGTERMs a process it didn't spawn. - Heartbeats report degraded, never crash the daemon. A backend death
becomes
health="degraded"withloaded_models=[]on the next heartbeat, and the router falls through to the next route.
Drop a TOML into mesh/catalog/ matching the SpecialistCard schema. To
actually serve, set required_backend to a wired engine and provide the
matching field (ollama_tag, model_id, mlx_repo, or gguf_path).
Working examples: mesh/catalog/qwen2.5-coder-7b-q4-ollama.toml (Ollama)
and mesh/catalog/qwen3-coder-30b-a3b-fp8.toml (vLLM).
- Append to the
Backendliteral inmesh/models.py. - Add detection to
mesh/probe.py:_detect_backends. - Implement the
BaseBackendprotocol inmesh/backends.py(mirrorOllamaBackendfor adopt-the-daemon, orVLLMBackendfor own-the-subprocess). - Add a branch in
mesh/serve.py:build_backend().
This is an optional central-registry (push) mode; the standalone mesh
above is pull-only and needs none of it. mesh/registry.py exposes the
FastAPI request/response shapes (HeartbeatPostRequest,
RegistryGetResponse). Mount
mesh.registry_app.create_mesh_app(registry=shared_registry) on
slancha-api at /mesh/v1.
mesh/select.py:select_mesh_route returns a MeshSelectionResult that
extends slancha-api's SelectionResult. Call it before falling through to
select_model_lmarena; on cluster_coverage_used=False, defer to the
existing cloud selector.
Apache-2.0. See LICENSE.