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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ change belongs in a Go service instead.
- Electron starts **only** `nvpair-ui-broker` from `desktop/cli-bin/`.
- The broker supervises the other Go workers: discovery, proxies, engines,
cluster, settings, manual nodes, workloads, errors, and the scheduler.
- The broker spawns all 11 workers at startup. Only the scanner is required; the
- The broker spawns all 12 workers at startup. Only the scanner is required; the
rest are optional and non-fatal.
- `nvpair-tui` is bundled but never supervised. It owns its own broker.
- Interprocess communication is newline-delimited JSON-RPC 2.0 over stdio
Expand Down Expand Up @@ -72,7 +72,7 @@ Never edit `desktop/docs/services-api.md` by hand. It is generated by

## Services (`services/`)

Thirteen Go binaries. Each component is its own module, with its tests beside its
Fourteen Go binaries. Each component is its own module, with its tests beside its
source and a `README.md` describing its JSON-RPC surface. Shared packages live in
`shared/`, and `tests/` holds cross-process tests that drive real binaries.
Prefer the Go source when a README disagrees with it.
Expand Down
2 changes: 1 addition & 1 deletion 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 and LM Studio |
| **Inference engines** | Ollama, LM Studio, and llama.cpp (adopt-only) |

**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
18 changes: 12 additions & 6 deletions desktop/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The canonical runtime inventory is
| `nvpair-ui-broker` | Electron | Worker supervision and control-plane relay |
| `ollama-proxy` | Broker | Ollama-compatible proxy and cluster routing |
| `lmstudio-proxy` | Broker, optional | LM Studio OpenAI-compatible proxy |
| `llamacpp-proxy` | Broker, optional | llama.cpp OpenAI-compatible proxy |
| `nvpair-node-scanner` | Broker | LAN discovery and announcement |
| `nvpair-node-info` | Broker | Node metadata and telemetry endpoint |
| `nvpair-workload-manager` | Broker, optional | Workload replication |
Expand Down Expand Up @@ -110,7 +111,7 @@ subscribes to broker relays after `app:ready`, and converts backend responses
into stable UI contracts.

Electron reports the service connected after broker `app:ready`. The
broker-owned Ollama and LM Studio proxies remain asynchronous capabilities; a
broker-owned Ollama, LM Studio, and llama.cpp proxies remain asynchronous capabilities; a
late or failed proxy does not misreport the broker startup as failed. If
`app:ready` does not arrive within the startup deadline, Overview opens Settings

Expand Down Expand Up @@ -195,16 +196,18 @@ Engine lifecycle and model operations flow through the broker's `engine:*`
relay to `nvpair-engine-manager`. The renderer identifies engines with the
closed `EngineType` union and narrows external strings with `isEngineType()`.

The Ollama and LM Studio proxies are cluster-aware. For model-bearing inference,
each proxy first keeps only nodes whose per-engine discovery inventory advertises
the requested model. Empty and non-matching inventories are excluded; an empty
owner set returns a local `502`. Routing precedence within the eligible set is:
The Ollama, LM Studio, and llama.cpp proxies are cluster-aware. For
model-bearing inference, each proxy first keeps only nodes whose per-engine
discovery inventory advertises the requested model. llama.cpp uses the **loaded**
set, not the on-disk catalog. Empty and non-matching inventories are excluded; an
empty owner set returns a local `502`. Routing precedence within the eligible set
is:

1. a user-selected manual node;
2. the priority list emitted by `nvpair-job-scheduler`;
3. the proxy's deterministic default ordering.

The scheduler combines total pending (queued and running) workload across both
The scheduler combines total pending (queued and running) workload across all
engines with a smoothed 0–3 pressure derived from the busiest GPU. Missing,
invalid, or older-than-10-second telemetry has neutral pressure. It emits the
order, pending count, and pressure, reranking on meaningful workload, discovery,
Expand Down Expand Up @@ -266,6 +269,9 @@ cannot yet be reported are centralized in
`src/shared/constants/modular-runtime.ts`.

- Ollama-compatible clients use the proxy port reported by the broker.
- llama.cpp clients use the OpenAI-compatible proxy at `http://127.0.0.1:8084/v1`
by default. PAIR adopts an already-running `llama-server` (default probe
`8082`) and does not install or load GGUFs.
- Cluster pairing currently uses port `14321`.
- Node telemetry is read from `/v1/node-info` at each discovered node's
advertised port.
Expand Down
8 changes: 4 additions & 4 deletions desktop/docs/macos-privileged-helper.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ The set in `native/PrivilegedHelper/main.swift` must stay in sync with the
(`src/shared/constants/modular-binaries.ts`) and the manual uninstaller.
`npm run service-contracts:check` fails when the Swift list differs from that canonical set:

`ollama-proxy`, `lmstudio-proxy`, `nvpair-node-info`, `nvpair-node-scanner`,
`nvpair-workload-manager`, `nvpair-errors`, `nvpair-cluster-manager`,
`nvpair-engine-manager`.
`ollama-proxy`, `lmstudio-proxy`, `llamacpp-proxy`, `nvpair-node-info`,
`nvpair-node-scanner`, `nvpair-workload-manager`, `nvpair-errors`,
`nvpair-cluster-manager`, `nvpair-engine-manager`.

> These eight mirror the per-program/per-port `netsh` rules in
> These nine mirror the per-program/per-port `netsh` rules in
> `scripts/build/installer.nsh` on Windows. macOS's Application Firewall is
> per-application and inbound-only, so one `--add`/`--unblockapp` per binary
> collapses Windows's per-port rules.
Expand Down
4 changes: 2 additions & 2 deletions desktop/docs/service-contract-exceptions.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$comment": "Intentionally unintegrated JSON-RPC methods for desktop ↔ services contract checks. Each ignoredMethods entry needs a current technical reason. Edit by hand; run npm run service-contracts:write to refresh docs/services-api.md after services/ changes.",
"ignoredMethods": {
"schedule:priority": "Broker-internal. nvpair-job-scheduler emits a per-engine priority snapshot ({engine, nodes, ranks?} — ordered node ids plus each node's pending count) to nvpair-ui-broker, which forwards it to ollama-proxy and lmstudio-proxy via node/set-priority; each proxy then layers its own optimistic burst reservations on that baseline. PAIR consumes the routing result rather than this notification, and must not mirror the pending/reservation accounting.",
"schedule:priority": "Broker-internal. nvpair-job-scheduler emits a per-engine priority snapshot ({engine, nodes, ranks?} — ordered node ids plus each node's pending count) to nvpair-ui-broker, which forwards it to ollama-proxy, lmstudio-proxy, and llamacpp-proxy via node/set-priority; each proxy then layers its own optimistic burst reservations on that baseline. PAIR consumes the routing result rather than this notification, and must not mirror the pending/reservation accounting.",
"engine:restore-enabled": "Broker-internal startup restoration. nvpair-ui-broker emits engine:restore-enabled directly to its supervised engine-manager after the managed Ollama port gate and on manager respawn; it is not a renderer/UI notification.",
"proxy:ready": "Consumed, not missing: the broker relays it and normalizeBrokerProxy (modular-supervisor.ts) strips the `proxy:` prefix, so the bridge handles the de-prefixed `ready` (sets proxyPort). The literal `proxy:ready` is intentionally absent from our TS — extractor limitation, not a gap.",
"node/selection-changed": "Automatic routing has no selected-node UI, so PAIR deliberately does not consume proxy selection changes.",
Expand All @@ -11,7 +11,7 @@
"workload:started": "Proxy-to-broker lifecycle event translated by the broker into workloads:upsert, which PAIR consumes.",
"workload:completed": "Proxy-to-broker terminal lifecycle event translated by the broker into workloads:upsert.",
"workload:errored": "Proxy-to-broker terminal lifecycle event translated by the broker into workloads:upsert.",
"node/activity": "Proxy-to-broker liveness evidence, consumed entirely inside the backend. ollama-proxy and lmstudio-proxy raise it (coalesced to one report per node per 2s by nvpair-shared/nodeactivity) whenever a peer's engine returns inference response bytes; nvpair-ui-broker relays it to nvpair-node-scanner as discovery:node-activity, where it keeps a node that is busy serving inference from being evicted for failing a liveness probe it had no spare CPU to answer. PAIR consumes the result — the node staying in the discovery snapshot — not this per-request notification, which carries no state a UI could render. Not currently surfaced by the extractor either (it is emitted through a shared noderec constant, like nodeinfo:observed-addresses); this entry records the intent regardless.",
"node/activity": "Proxy-to-broker liveness evidence, consumed entirely inside the backend. ollama-proxy, lmstudio-proxy, and llamacpp-proxy raise it (coalesced to one report per node per 2s by nvpair-shared/nodeactivity) whenever a peer's engine returns inference response bytes; nvpair-ui-broker relays it to nvpair-node-scanner as discovery:node-activity, where it keeps a node that is busy serving inference from being evicted for failing a liveness probe it had no spare CPU to answer. PAIR consumes the result — the node staying in the discovery snapshot — not this per-request notification, which carries no state a UI could render. Not currently surfaced by the extractor either (it is emitted through a shared noderec constant, like nodeinfo:observed-addresses); this entry records the intent regardless.",
"errors:report": "Producer-to-broker event demultiplexed into broker-owned nvpair-errors. PAIR consumes the resulting errors:update snapshot.",
"errors:clear": "Producer-to-broker event demultiplexed into broker-owned nvpair-errors. PAIR consumes the resulting errors:update snapshot.",
"cluster:trust-changed": "Broker-internal. nvpair-cluster-manager announces it from its trust store after a pin write lands, and nvpair-ui-broker answers it by calling discovery:reload-trust on nvpair-node-scanner so the scanner re-derives each peer's trusted annotation. PAIR consumes the result — the refreshed AvailableNode.trusted carried on the discovery snapshot — not this notification. It carries no payload by design: recipients re-read the cluster dir rather than trusting a diff on the wire.",
Expand Down
28 changes: 28 additions & 0 deletions desktop/docs/services-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
- none ✅

### Requests the backend handles but the bridge never calls (unused capability)
- ⚠️ llamacpp-proxy → node/selected
- ⚠️ llamacpp-proxy → node/set-local-backend
- ⚠️ lmstudio-proxy → node/selected
- ⚠️ lmstudio-proxy → node/set-local-backend
- ⚠️ nvpair-engine-manager → engine:describe
Expand Down Expand Up @@ -45,6 +47,31 @@
### Backend binaries not listed in `modular-binaries.ts`
- none ✅

## llamacpp-proxy

| Method | Direction | In bridge? |
|---|---|---|
| `error` | notification (we consume) | ✅ yes |
| `errors:clear` | notification (we consume) | ✅ yes |
| `errors:report` | notification (we consume) | ✅ yes |
| `node/discovered` | notification (we consume) | ✅ yes |
| `node/removed` | notification (we consume) | ✅ yes |
| `node/selection-changed` | notification (we consume) | ➖ ignored |
| `node/updated` | notification (we consume) | ✅ yes |
| `proxy/request` | notification (we consume) | ✅ yes |
| `proxy/request-started` | notification (we consume) | ➖ ignored |
| `ready` | notification (we consume) | ✅ yes |
| `node/add-manual` | request (we call) | ✅ yes |
| `node/remove-manual` | request (we call) | ✅ yes |
| `node/select` | request (we call) | ✅ yes |
| `node/selected` | request (we call) | ⚠️ not called |
| `node/set-local-backend` | request (we call) | ⚠️ not called |
| `node/set-priority` | request (we call) | ✅ yes |
| `nodes/list` | request (we call) | ✅ yes |

**Dynamic / unresolved notify sites (verify by hand — `npm run service-contracts` prints the line numbers):**
- `method (var) (proxy.go)`

## lmstudio-proxy

| Method | Direction | In bridge? |
Expand Down Expand Up @@ -263,6 +290,7 @@
- `proxy:* (broker.go)`
- `method (var) (clustermanager.go)`
- `method (var) (errors.go)`
- `llamacpp-proxy:* (llamacppproxy.go)`
- `lmstudio-proxy:* (lmstudioproxy.go)`
- `method (var) (proxy.go)`
- `method (var) (rpcworker.go, 2 sites)`
Expand Down
20 changes: 12 additions & 8 deletions desktop/docs/services-backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ broker supervises every worker and relays its control plane.
| `nvpair-ui-broker` | Worker supervision and relay |
| `ollama-proxy` | Ollama-compatible routing proxy with cluster-mTLS ingress |
| `lmstudio-proxy` | LM Studio routing proxy with cluster-mTLS ingress |
| `llamacpp-proxy` | llama.cpp routing proxy with cluster-mTLS ingress |
| `nvpair-node-scanner` | Discovery and node announcement |
| `nvpair-node-info` | Node metadata and telemetry |
| `nvpair-manual-nodes` | User-managed node entries |
Expand All @@ -45,7 +46,7 @@ flowchart TB
Broker["nvpair-ui-broker"]
Scanner["nvpair-node-scanner"]
NodeInfo["nvpair-node-info"]
Proxies["ollama-proxy / lmstudio-proxy"]
Proxies["ollama-proxy / lmstudio-proxy / llamacpp-proxy"]
Engines["nvpair-engine-manager"]
Cluster["nvpair-cluster-manager"]
Settings["nvpair-node-settings"]
Expand Down Expand Up @@ -90,8 +91,8 @@ engine, workload, cluster, and error relays. The bridge then emits renderer push
events from backend notifications.

Connector readiness follows the broker contract: `app:ready` establishes the
service connection, while Ollama and LM Studio proxy readiness remains an
asynchronous capability signal. Personal AI Router waits up to the canonical
service connection, while Ollama, LM Studio, and llama.cpp proxy readiness
remains an asynchronous capability signal. Personal AI Router waits up to the canonical
startup deadline in `src/shared/constants/modular-runtime.ts` for
`app:ready`; an outright failure or stalled broker startup is surfaced in
Settings > Service with retry and log access. If a stalled broker reports ready
Expand All @@ -117,7 +118,7 @@ reserved for inference clients.
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `app:ready` | Complete broker startup and refresh snapshots | `state:request-refresh` |
| `discovery:nodes-changed` | Replace discovery snapshot and diff nodes | `discovery:nodes-changed`, `nodes:upsert`, `nodes:remove` |
| `proxy:ready` / `lmstudio-proxy:ready` | Record engine proxy port | `engines:state-changed` |
| `proxy:ready` / `lmstudio-proxy:ready` / `llamacpp-proxy:ready` | Record engine proxy port | `engines:state-changed` |
| proxy `node/*` | Update per-engine node presence; the advertised port is the peer's promoted proxy port (not the engine's private loopback port) | node and engine pushes |
| `engine:ready` / `engine:state-changed` | Update engine facts and models | `engines:state-changed` |
| `engine:install-progress` / `engine:remote-progress` | Update operation progress | engine progress pushes |
Expand All @@ -128,7 +129,7 @@ reserved for inference clients.
| `nodes:changed` | Replace membership snapshot | `nodes:changed` |
| `workloads:upsert` / `workloads:remove` | Update workload catalog | workload pushes |

`nvpair-job-scheduler` combines queued and running work across both engines with
`nvpair-job-scheduler` combines queued and running work across all engines with
a smoothed 0–3 pressure from the busiest GPU. Invalid, missing, or
older-than-10-second telemetry receives neutral pressure. It emits
`schedule:priority` with order, pending count, and pressure; the broker applies
Expand All @@ -150,8 +151,11 @@ waiting for authoritative state. Pending state clears on matching engine state,
progress, or error pushes.

Local engine operations include install, start, stop, uninstall, update, port
changes, and model actions. Remote cluster operations use the engine manager's
remote control surface where supported.
changes, and model actions. llama.cpp is adopt-only: PAIR probes an
already-running `llama-server` (default `8082`) and does not install, spawn, or
load GGUFs. The app endpoint is `http://127.0.0.1:8084/v1`; routing requires the
model **loaded** on the serving node. Remote cluster operations use the engine
manager's remote control surface where supported.

`engine:stop` (and its cluster `ec` equivalent) reclaims an orphan a prior run
left on the engine's own managed port, terminating it only when that PID is
Expand Down Expand Up @@ -263,7 +267,7 @@ resolved back to the hostname the entry was keyed by.

An NVPAIR-launched engine binds to loopback only and is never directly
LAN-reachable. Each node fronts its engine with its `ollama-proxy` /
`lmstudio-proxy`, whose LAN ingress is gated by cluster mTLS: only a pinned
`lmstudio-proxy` / `llamacpp-proxy`, whose LAN ingress is gated by cluster mTLS: only a pinned
cluster member can send it work. Discovery advertises the promoted **proxy**
port (never the engine port), and the broker hands the private loopback engine to
the local proxy via `node/set-local-backend`. Every cluster-scoped worker derives
Expand Down
Loading