Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions .cursor/rules/model-registry.mdc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Backend-owned engine model catalogue (Ollama committed list + LM Studio and llama.cpp Hugging Face catalogs)
description: Backend-owned engine model catalogue (Ollama committed list + LM Studio, llama.cpp and Colibri Hugging Face catalogs)
alwaysApply: true
---
<!--
Expand All @@ -14,7 +14,7 @@ SPDX-License-Identifier: Apache-2.0
It lives in the backend because both front ends need it: the desktop app's
Add Model modal and the terminal interface's model browser. There is **no
shared GGUF registry** — each enabled engine has exactly one curated source,
and only llama.cpp's can be searched.
and only llama.cpp's and Colibri's can be searched.
- **Ollama**: a **locked, committed list**, not a live scrape.
`services/nvpair-engine-manager/catalog/ollama-models.json` (wire shape
`{ scrapedAt, source, count, models: [...] }`) is compiled in with `go:embed`,
Expand Down Expand Up @@ -45,8 +45,23 @@ SPDX-License-Identifier: Apache-2.0
exact `owner/repository:Q4_K_M` pull id; other quantizations are not listed.
The reply says `searchable: true`; other sources ignore a query and return
their whole list. **Never log the user's search text.**
- The LM Studio list, the llama.cpp list, and each llama.cpp query share one
cache: 6-hour TTL, a coalescing guard so concurrent callers share one request,
- **Colibri**: a **live fetch** of every open Hugging Face repository tagged
`colibri` (`filter=colibri&full=true`, top 200 by downloads). It is
**searchable** within the tag. Only repositories with a root `config.json`
are kept, only when the hub's parse of that `config.json` (`config=true`)
names a `model_type` the Colibri manifest's `model_types` lists, and the
ports' builds (tags `speedycolibri` and `peng`, or a `speedy-colibri` id)
and non-text-generation pipelines are dropped. Each
result is the `owner/repo` id, which `pull_model` (an `hf_snapshot`)
downloads whole into `~/colibri-models/<owner>/<repo>`. Ahead of the tagged
repositories come the models the Colibri manifest's `catalog` lists by
repository (Colibri v2.1.0's own `coli setup` list, with vendors' untagged
checkpoints such as DeepSeek V4 Flash and Kimi K3), each fetched for its live
downloads and date and dropped if it is now gated. The list lives in the
manifest, not in Go, and so do its download rules (pinned `revision`,
`exclude` patterns, `after_download` Python steps).
- The LM Studio list, the llama.cpp list, the Colibri list, and each search
query share one cache: 6-hour TTL, a coalescing guard so concurrent callers share one request,
and a failure backoff so a dead upstream is not re-dialled on every call.
- All sources normalize to `CatalogModel`, which the desktop maps to the shared
`EngineHubModel` (`desktop/src/shared/types/engine-api.ts`).
Expand All @@ -68,7 +83,7 @@ SPDX-License-Identifier: Apache-2.0
- The desktop relays through
`desktop/src/electron/service-bridge/model-catalog.ts`, which exposes
`getEngineHubModels(engineType, query?)` and `warmEngineHubs()` (warms the
LM Studio and llama.cpp catalogs — Ollama needs none). The `engine:search-hub`
LM Studio, llama.cpp and Colibri catalogs — Ollama needs none). The `engine:search-hub`
handler in `empty-handlers.ts` calls the former; the `overview:ready` handler
in `window.ipc.ts` calls the latter. Warm on renderer-ready, **not** on
service connect: a catalog fetch started before the window has painted
Expand All @@ -77,8 +92,9 @@ SPDX-License-Identifier: Apache-2.0
- Both front ends fetch an engine's full catalog once (cached per-engine) and
filter **locally** as the user types. The desktop renderer
(`src/ui/components/ModelHub/`) sorts client-side by Updated / Name / Size;
for llama.cpp, Enter/Search sends an explicit query and merges the response
with matching listed rows. The terminal interface's browser sends `/` as a
for a source whose `engineHub.searchable` capability is set (llama.cpp and
Colibri), Enter/Search sends an explicit query and merges the response with
matching listed rows. The terminal interface's browser sends `/` as a
query for a searchable source and shows its results. Pulling goes through the
engine manager (`engine:pull-model` → `pull_model`); the catalogue only
produces the pull-ready id.
2 changes: 1 addition & 1 deletion .cursor/rules/proxy-inference-routing.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ For model-bearing inference, each facade first filters a request-local discovery
snapshot to nodes whose per-engine inventory advertises the requested model.
Empty and non-matching inventories are ineligible; an empty owner set returns a
local `502` without contacting an engine. Ollama normalizes the implicit
`:latest` tag; LM Studio and llama.cpp IDs match exactly.
`:latest` tag; LM Studio, llama.cpp and Colibri IDs match exactly.

Within that eligible owner set, proxy target precedence is:

Expand Down
9 changes: 6 additions & 3 deletions .cursor/rules/system-architecture.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ The broker uses both for scheduling.

### Inference HTTP

Ollama, LM Studio, and llama.cpp proxies expose local client-compatible HTTP
Ollama, LM Studio, llama.cpp, and Colibri proxies expose local client-compatible HTTP
endpoints. Chat behaves as a local third-party client. Prompts, messages,
chunks, and response bodies must never be logged.

Expand Down Expand Up @@ -235,11 +235,14 @@ The model catalogue is owned by `nvpair-engine-manager` and served over
`engine:catalog`, so the desktop app and the terminal interface browse one
implementation. Electron relays it through
`src/electron/service-bridge/model-catalog.ts`. It provides curated Ollama,
LM Studio, and llama.cpp catalogs, filtered for the OS and CPU a model will
LM Studio, llama.cpp, and Colibri catalogs, filtered for the OS and CPU a model will
install on; model operations go through the engine manager. llama.cpp's is
populated from approved Hugging Face publishers and also answers a bounded
search across public publishers. Every llama.cpp result is an exact
`owner/repository:Q4_K_M` pull ID.
`owner/repository:Q4_K_M` pull ID. Colibri's is the models its manifest lists
by repository (Colibri's own `coli setup` list), then every open Hugging Face
repository tagged `colibri`, searchable within the tag, and every result is the
`owner/repository` id its snapshot download fetches whole.

Managed llama.cpp installation uses CUDA archives on Windows and Linux and
standard Metal-capable archives on macOS. Package choice is based on platform
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ one, and both report live GPU and memory use throughout.
| **Architectures** | x64 and arm64 on all three. Windows on ARM is experimental. |
| **Installers** | Windows `.exe`; Linux `.deb`; macOS `.dmg`. On other Linux distributions, [build from source](docs/building.mdx). |
| **Mixing nodes** | Windows, Linux, and macOS nodes can all be paired with each other |
| **Inference engines** | Ollama, LM Studio, and llama.cpp |
| **Inference engines** | Ollama, LM Studio, llama.cpp, and Colibri |

**PAIR running on a machine does not mean an engine will.** PAIR itself runs on
any supported Windows, Linux, or macOS machine. Each engine sets its own requirements
Expand Down Expand Up @@ -100,7 +100,7 @@ you want by its full filename instead.
**Settings → Service** and read the status there.

- **Get an engine running.** On the node's card, open **Engine settings** and
select **Install** next to Ollama, LM Studio, or llama.cpp. PAIR downloads and
select **Install** next to Ollama, LM Studio, llama.cpp, or Colibri. PAIR downloads and
sets the engine up for you, so nothing needs to be in place beforehand.

![The Install engines dialog with Ollama downloading, reporting progress as it installs.](docs/assets/onboarding/engine-lifecycle/01-engine-installing.png)
Expand All @@ -110,7 +110,11 @@ you want by its full filename instead.
choice. llama.cpp populates this list from popular GGUF repositories on
Hugging Face. Type to filter the list, or press Enter to search the wider
public catalog. Its exact IDs include the repository and quantization, such
as `ggml-org/gemma-3-1b-it-GGUF:Q4_K_M`.
as `ggml-org/gemma-3-1b-it-GGUF:Q4_K_M`. Colibri lists the models of
Colibri's own setup list, DeepSeek V4 Flash and Kimi K3 among them, then the
Hugging Face repositories tagged `colibri`, searchable the same way; its models are whole
repositories of tens to hundreds of gigabytes, and its launcher needs Python
3.10 or newer.

![A node card with its engine expanded, one model pulling and the Add model button beside the list.](docs/assets/onboarding/getting-started/07-add-model.png)

Expand Down Expand Up @@ -151,7 +155,7 @@ The reply is ordinary OpenAI-shaped JSON, abbreviated here:
}
```

If you changed a port, or you are using LM Studio or llama.cpp rather than
If you changed a port, or you are using LM Studio, llama.cpp, or Colibri rather than
Ollama, copy the URL from **Endpoints → API endpoints** instead of assuming the
one above. The copied URL includes `/v1`, ready for Hermes and other
OpenAI-compatible clients. Append `/chat/completions` for the request above.
Expand Down Expand Up @@ -199,7 +203,8 @@ Your data means settings, logs, cluster identity and certificates, and any engin
PAIR installed for you. An engine the uninstaller cannot remove — one still
holding a file open, say — is left in place rather than stopping the uninstall;
remove it with its own uninstaller. **Model weights are not touched** — they live in the
engine's own storage (`~/.ollama`, `~/.llamacpp`, `~/.lmstudio/models`), so
engine's own storage (`~/.ollama`, `~/.llamacpp`, `~/.lmstudio/models`,
`~/colibri-models`), so
removing PAIR does not delete the models you downloaded. Delete those through the
engine, or by removing its directory. An engine you installed yourself is also
left alone.
Expand Down Expand Up @@ -284,6 +289,7 @@ feedback and contributions will help shape priorities.
### Engines and integrations

- [x] llama.cpp support.
- [x] Colibri support (large mixture-of-experts models streamed from disk).
- [ ] vLLM support.
- [ ] EXO support.
- [ ] ComfyUI integration.
Expand Down
13 changes: 11 additions & 2 deletions desktop/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ not select or pin proxy routes.
An NVPAIR-launched engine binds to loopback and is never directly LAN-reachable.
Peers reach it only through the node's proxy over a cluster-mTLS ingress, so
discovery advertises the promoted proxy port for `ol`/`lm` rather than the
engine's private port; llama.cpp uses the additional `lc` key. Transport
engine's private port; llama.cpp uses the additional `lc` key and Colibri the
`co` key. Transport
security is backend-owned; Electron only
reflects the advertised proxy port and reads a remote engine's real port from
`engine:remote-get-installed` facts.
Expand All @@ -276,6 +277,13 @@ same method, so both front ends browse one implementation.
public Hugging Face matches across publishers, each query cached on its own.
Both keep only public, generative repositories with a primary `Q4_K_M` file
and emit exact `owner/repository:Q4_K_M` pull IDs;
- Colibri models come from the list in Colibri's engine manifest (Colibri's
own setup list, vendors' original checkpoints such as DeepSeek V4 Flash and
Kimi K3 among them), then every public Hugging Face repository tagged
`colibri` that has a root `config.json`, fetched and cached the same way and
searchable within the tag. Each pull ID is the `owner/repository` id, which
the engine manager's `hf_snapshot` action downloads whole into
`~/colibri-models`;
- the request takes an optional `platform` and `arch`, marks Apple-only (MLX)
rows, keeps them only for Apple Silicon, and echoes the target it filtered
for, so a client driving a peer is not offered models that peer cannot
Expand Down Expand Up @@ -408,7 +416,8 @@ deletes finish. Unpackaged (`electron-vite dev`) builds omit relaunch and the UI
tells the developer to run `npm start` again — quitting Electron tears down the Vite
renderer server, so auto-relaunch would come up with nothing to load.

The engines' model stores (`~/.ollama`, `~/.llamacpp`, `~/.lmstudio/models`) and
The engines' model stores (`~/.ollama`, `~/.llamacpp`, `~/.lmstudio/models`,
`~/colibri-models`) and
the application install tree are never removed. The reset does uninstall the
engines PAIR installed, through `engine:uninstall-managed` while the broker is
still running, so an engine a vendor installer placed in the user's home is
Expand Down
19 changes: 12 additions & 7 deletions desktop/docs/frontend-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,14 @@ They do not imply a WebSocket connection. Browser clients are not supported.
- `onSettingsDisconnected(callback)` reports that a node's settings authority
became unreachable, so its cached snapshot is stale.

`EngineType` currently includes `ollama`, `lm-studio`, and `llama-cpp`.
`searchHub(engineType, query?)` serves Ollama's locked local catalog and cached
live Hugging Face catalogs for LM Studio and llama.cpp. A non-empty llama.cpp
query performs an explicit bounded search across public Hugging Face GGUF
repositories; other calls return the populated engine catalog. llama.cpp pull
`EngineType` currently includes `ollama`, `lm-studio`, `llama-cpp`, and
`colibri`. `searchHub(engineType, query?)` serves Ollama's locked local catalog
and cached live Hugging Face catalogs for LM Studio, llama.cpp, and Colibri. A
non-empty llama.cpp query performs an explicit bounded search across public
Hugging Face GGUF repositories, and a non-empty Colibri query searches within
the `colibri` tag; other calls return the populated engine catalog. Colibri pull
IDs are `owner/repository` ids; load switches the served model by restarting
the engine, and there is no unload. llama.cpp pull
IDs are exact `owner/repository:quantization` values; load, unload, and delete
all use that same exact id.

Expand Down Expand Up @@ -205,8 +208,10 @@ Commands return no state. Renderer stores update from
- the node-local Inference Demo (`inferenceDemo.getState()` / `start()` / `stop()`
and the `demo:state` push). `start()` rejects if a demo is already running on
this node or if no Ollama, LM Studio, or llama.cpp proxy exposed a
text-generation model; callers show that as an ordinary error banner. Demo
state is not synchronized across nodes.
text-generation model; callers show that as an ordinary error banner. Colibri
is deliberately not driven by the demo, because a server that holds one model
per process cannot absorb its burst of concurrent agents. Demo state is not
synchronized across nodes.

`demo:state` is a main-to-renderer broadcast rather than a service push, so it is
typed in `IpcPushChannelMap` (`shared/types/ipc-channels.ts`) instead of the
Expand Down
7 changes: 4 additions & 3 deletions desktop/docs/services-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,12 @@ reachability verdict of its own — a failed `/v1/node-info` poll keeps the last
good metrics and never marks a node offline.

The model catalogue is backend-owned. `nvpair-engine-manager` serves the curated
Ollama, LM Studio, and llama.cpp lists over `engine:catalog`, filtered for the
Ollama, LM Studio, llama.cpp, and Colibri lists over `engine:catalog`, filtered for the
operating system and CPU a model will install on; Electron relays the call and
maps rows for the renderer, which then sends pull-ready model IDs back through
the engine manager. llama.cpp's source is also searchable: the relay passes the
model hub's query through, and the engine manager searches Hugging Face with it.
the engine manager. llama.cpp's and Colibri's sources are also searchable: the
relay passes the model hub's query through, and the engine manager searches
Hugging Face with it (GGUF repositories, or the `colibri` tag).
The Ollama reply is a single multi-megabyte frame, so every hop on its path
shares `jsonrpc.WorkerFrameBytes`.

Expand Down
18 changes: 10 additions & 8 deletions desktop/docs/services-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ history.
| Cluster pairing | Complete | PIN pairing, identity, membership, leave, and removal |
| Cluster transport security | Backend-owned | Node-to-node transport security, including the proxies' cluster-mTLS inference ingress, is entirely backend; Personal AI Router implements none |
| Settings | Partial | Cluster identity plus per-engine ports and engine arguments, local and remote; inert backend settings are not surfaced |
| Model catalog search | Backend-owned | `engine:catalog` serves the locked Ollama list and the cached LM Studio and llama.cpp catalogs, and searches Hugging Face for llama.cpp |
| Model catalog search | Backend-owned | `engine:catalog` serves the locked Ollama list and the cached LM Studio, llama.cpp and Colibri catalogs, and searches Hugging Face for llama.cpp and Colibri |

## Supervision

Expand Down Expand Up @@ -267,15 +267,17 @@ restart is entirely backend-owned: PAIR sends the same `deleteModel` command as
for any other engine and never issues `engine:restart` itself, so the bundled
`nvpair` terminal UI and a remote peer's deletion get the same behavior.

**The full engine restart is LM Studio only.** Ollama and llama.cpp reflect a
deletion in-process, so their manifests omit `restart_after` and their capability
entries omit `restartsOnModelDelete`: no engine bounce and no restart
confirmation. llama.cpp may stop the selected model while removing its cache
**The full engine restart is LM Studio only.** Ollama, llama.cpp and Colibri
reflect a deletion without one (Colibri lists its store from disk), so their
`delete_model` actions omit `restart_after` and their capability entries omit
`restartsOnModelDelete`: no engine bounce and no restart confirmation. Colibri's
`load_model` does restart: it serves one model per process, so switching models
is a restart onto the new one. llama.cpp may stop the selected model while removing its cache
entry, but the router and its other models stay up. Those manifest and
capability facts have to stay in step across Go and TypeScript, which nothing in
either type system enforces — so
`tests/modular/delete-model-restart.test.ts` reads the shipped manifests and
asserts the pair agrees, and `TestBundledManifestsRestartOnlyLMStudio` guards the
asserts the pair agrees, and `TestBundledManifestsRestartOnlyWhereNeeded` guards the
same thing from the Go side.

Several model-action timeouts have to be ordered correctly. For a
Expand Down Expand Up @@ -346,8 +348,8 @@ safety-net timeout (`pending-actions.store.ts`). Loaded state carries no
name-set, not structured details.

The model catalogue is owned by the backend: `engine:catalog` on
`nvpair-engine-manager` serves the curated Ollama, LM Studio, and llama.cpp
lists, and searches Hugging Face for llama.cpp, and both the desktop app and
`nvpair-engine-manager` serves the curated Ollama, LM Studio, llama.cpp, and
Colibri lists, and searches Hugging Face for llama.cpp and Colibri, and both the desktop app and
the terminal interface browse it. Electron only relays the call and maps rows
for the renderer.

Expand Down
1 change: 1 addition & 0 deletions desktop/src/electron/service-bridge/model-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,5 @@ export async function getEngineHubModels(
export function warmEngineHubs(): void {
void getEngineHubModels('lm-studio')
void getEngineHubModels('llama-cpp')
void getEngineHubModels('colibri')
}
6 changes: 4 additions & 2 deletions desktop/src/electron/service-bridge/modular-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ function emptyEngines(): Record<ProxyEngine, EnginePresence> {
return {
ollama: emptyPresence(),
'lm-studio': emptyPresence(),
'llama-cpp': emptyPresence()
'llama-cpp': emptyPresence(),
colibri: emptyPresence()
}
}

Expand Down Expand Up @@ -890,7 +891,8 @@ class ModularBridgeState {
private proxyPorts: Record<ProxyEngine, number> = {
ollama: 0,
'lm-studio': 0,
'llama-cpp': 0
'llama-cpp': 0,
colibri: 0
}
private selfId: string | null = null
/**
Expand Down
Loading