Skip to content

Feature/custom unmanaged inference engines - #196

Open
stevbev wants to merge 8 commits into
NVIDIA:developfrom
stevbev:feature/custom-engine
Open

stevbev wants to merge 8 commits into
NVIDIA:developfrom
stevbev:feature/custom-engine

Conversation

@stevbev

@stevbev stevbev commented Nov 5, 2026 •

Copy link
Copy Markdown

Description

This branch adds Custom — a fourth engine kind that fronts user-configured
external OpenAI-compatible endpoints (vLLM, llama.cpp, mtplx, strata, hosted APIs)
— and makes the cluster-status surfaces report real engine state.

Before this, PAIR only knew about the three installable managed engines. The
menu-bar / tray cluster-status window always rendered "Engines (n/3)": it
fabricated a placeholder for every manifest engine type, so a node with
nothing installed read "0/3", a node running a custom endpoint read "0/3" as
well, and custom engines were invisible everywhere. The custom proxy plane,
its configuration storage, relay, probe, and UI layers are all new in this
branch; the tray line and the TUI node-detail screen now count and list what
a node actually has. It also carries two macOS packaging fixes needed for
multi-node use: the Local Network permission declaration and a monochrome
menu-bar icon.

Release intent

Changelog title

Custom inference engines; truthful engine counts in the cluster-status window

Changelog body

You can now add external OpenAI-compatible endpoints (vLLM, llama.cpp servers, hosted APIs) as Custom engines on any node: add, edit, and remove them from the desktop app, set the shared custom-proxy port, and see health and model chips per endpoint. The TUI lists Custom endpoints on the node detail screen. A Custom endpoint's API key stays on the node that owns it.

  • The menu-bar / tray cluster-status window counts the engines a node actually has — installed managed engines plus configured custom endpoints, online over total — instead of always showing "Engines (n/3)" and ignoring custom engines.
  • macOS 14+ builds now declare Local Network usage, so peer discovery works instead of silently failing; the menu-bar icon renders as a proper monochrome template image.

Bumps

  • services: minor
  • nvpair-cluster-manager: none
  • nvpair-engine-manager: minor
  • nvpair-errors: none
  • nvpair-job-scheduler: patch
  • nvpair-manual-nodes: minor
  • nvpair-node-info: none
  • nvpair-node-scanner: patch
  • nvpair-node-settings: none
  • nvpair-proxy: minor
  • nvpair-tui: minor
  • nvpair-ui-broker: minor
  • nvpair-workload-manager: none

Bump rationale: broker, proxy, engine-manager, manual-nodes, and tui each ship
new user-visible IPC/HTTP behavior (custom:list/add/remove/refresh,
custom/get-port/set-port, the custom facade and its routing, the custom model
union, the probe source, the TUI section). job-scheduler and node-scanner have
no source change beyond tests; they carry patch because their compiled engines
table / TXT parse set now includes the custom kind (scheduler:get-status
gains a custom entry; node-record TXT gains the cu= key).

Resolves

#182
#165
#77
#39
#24
#2

Scope

Included: the Custom engine kind in the shared engine registry and mDNS TXT
key set; the custom-proxy facade (terminal forwarding, per-model routing,
key injection on the owning node's last hop); the broker-owned endpoint
config store (0600 file), the peer relay, and the configurable facade port;
engine-manager's custom model union and carrier; manual-nodes as the probe
cache; the desktop service bridge, management UI, and the tray/TUI count and
listing fixes; generated service docs, specs, and user docs.

Excluded: model operations and install lifecycle for Custom (unmanaged by
design); any push feed for custom health (the tray polls at 30s while
visible); changes to managed-engine behavior beyond skipping unmanaged kinds
in launcher-shaped consumers; services/versions.json / CHANGELOG.md
(automation-owned).

Validation

Environment: 3 macOS arm64 machines; branch tip 6a41a9e+style commit, based
on develop (039f9fe). 1 Windows 11 amd64 machine running 1.0 release.

From desktop/:

  • npm run typecheck — passed.
  • npm run lint — passed, 0 errors / 0 warnings after applying.
  • npm run test:unit — 51 files / 384 tests passed.
  • npm run dead-code:check — passed.
  • npm run service-contracts:check — passed (doc fresh).
  • node scripts/spdx-headers.mjs — passed.

From services/, go test ./... in the affected modules:
shared/engines, shared/noderec, nvpair-ui-broker, nvpair-proxy,
nvpair-manual-nodes, nvpair-node-scanner, nvpair-tui,
nvpair-job-scheduler — all passed.
nvpair-engine-manager — one failure, TestUninstall_TerminatesRunningInstance,
which reproduces identically on the develop base commit on this machine; it
is a pre-existing environment-dependent executor test, not introduced here.
The new cross-process suites in services/tests (advertise, interop, peer
ingress over mTLS, remote edit) require a paired multi-node harness and are
left to CI.

Manual UAT on macOS with one custom endpoint and no managed engines: the
tray window showed "Engines (1/1)". During a manual LM Studio
install/start/stop/uninstall cycle the same node went 1/2 → 2/2 → 1/2 → 1/1,
confirming installing/stopped states count in the denominator and custom
endpoints persist correctly across it.

Risk

  • Wire compatibility: the node-record TXT gains the cu= service key and
    a new emit position. Old peers parse unknown keys through the
    forward-compat path; mixed-version behavior is pinned by the new
    advertise/interop suites rather than manual reasoning.
  • Security: endpoint API keys ride only an add request and never return
    in replies (views carry keySet) or in logs (reply entries are rebuilt from
    known fields; redact-log gained apiKey/api_key). Keys live only in the
    owner's 0600 store, and forwarding injects the key only on the owning
    node's last hop with the caller's Authorization stripped. Adding an endpoint
    that would forward to one of the node's own facade addresses is refused, so
    the facade cannot loop on itself.
  • Ports: the Custom facade defaults to 8000; users proxying something else
    there set the per-node custom port instead, which is reserved away from
    engines, sibling proxies, and the node's own endpoints.
  • Packaging: macOS builds now trigger the Local Network permission prompt
    (documented in getting-started and troubleshooting, including the
    unsigned-build workaround). A denied grant means no peers, not a crash.
  • Tray polling: the custom:list pulls pause while the surface is hidden,
    run one-at-a-time, and keep the previous snapshot on a failed read
    (unknown ≠ "no custom engines").

Checklist

  • I have read the Contributing Guidelines.
  • Every commit is signed off (git commit -s), certifying the Developer Certificate of Origin.
  • New or existing tests cover the change.
  • Relevant documentation is updated.
  • I checked the diff, changed filenames, and commit messages for credentials, private data, internal URLs, internal issue identifiers, and generated artifacts.
  • I recorded the validation commands and results above.
  • I declared version bumps in the release-intent block above. services/versions.json is written by automation — do not edit it by hand.

@stevbev stevbev changed the title Feature/custom engine Feature/custom unmanaged inference engines Nov 5, 2026
@stevbev
stevbev force-pushed the feature/custom-engine branch 2 times, most recently from deee79c to 54b2ae6 Compare November 7, 2026 18:49
macOS 14+ silently denies an app's mDNS and local-network traffic unless
the bundle declares it: peers never appear and the log fills with
'sendto: no route to host' with no prompt. Add NSLocalNetworkUsageDescription
and NSBonjourServices (_nvpair-node._tcp) to the macOS extendInfo, and
document the Local Network permission in the getting-started pairing steps
and the troubleshooting guide (including the unsigned-build workaround).

Signed-off-by: Steven Beverly <thestevbev@gmail.com>
Register a fourth engine kind, Custom, in the shared engine registry
(nvpair-shared/engines) and the mDNS TXT key set (nvpair-shared/noderec,
key 'cu'). Custom is declared Unmanaged with no engine port base: PAIR
never installs, starts, stops, or relocates what sits behind it, and
launcher-shaped consumers must skip unmanaged kinds.

nvpair-proxy grows the custom-proxy facade for it. The facade fronts
user-configured OpenAI-compatible endpoints instead of a local process:
the endpoint set (URL, optional API key, declared models) arrives as a
wholesale replacement over the broker's control channel, ingress routes a
model request to the first endpoint that declares it and refuses to guess
between endpoints when no declared owner answers, and forwarding is
strictly terminal. The endpoint's key is injected only on the owning
node's last hop; the caller's own Authorization never leaves the trusted
hop. Peer-addressed Custom entries stay internal routing bookkeeping and
never surface on the outward node wire. Updates the proxy README and
specification, and the architecture rule file that records the facade and
key policy.

Signed-off-by: Steven Beverly <thestevbev@gmail.com>
…oss nodes

The configuration plane behind the Custom facade, per node:

- nvpair-ui-broker owns the endpoint set in a 0600 configs file (the only
  sanctioned home for an endpoint API key besides in-memory request
  plumbing). Port is a mandatory explicit field; an endpoint that would
  forward to one of this node's own facade addresses is refused at add.
  The Custom facade's own port is configurable and reserved away from
  sibling proxies, engines, and the node's own endpoints; unmanaged
  engine kinds get a clear error on engine-port edits instead of a
  relocation attempt.
- nvpair-manual-nodes becomes the probe cache for that configuration: one
  /v/models probe per endpoint per cycle, manual-node failure semantics,
  no persistence across restarts.
- nvpair-engine-manager merges the broker-pushed custom model union into
  every ModelsResult under the custom engine; key presence is the
  published semantic, so clearing the set stops peer attribution. The
  node-scanner copies it wholesale, pinned by a regression test.
- Cross-node management: a peer-addressed custom/list/refresh/add/remove/
  get-port/set-port is relayed over the pinned mTLS control channel
  through engine-manager's new custom carrier to the owner node, where it
  runs through the same local plane. The API key rides only an add
  request and never returns in a reply (views carry keySet) or a log.
- Advertisement follows routability, not configuration: the cu= TXT key,
  the model inventory, and the ingress backend set are gated on facade
  readiness, so peers stop routing when the endpoints go unhealthy.

collectlogs learns the custom engine token so it is never swept as a
hostname or model, and the credential file stays out of bundles by the
log allow-list. READMEs, service specs, and the architecture doc describe
the new plane.

Signed-off-by: Steven Beverly <thestevbev@gmail.com>
Teaches the whole desktop stack the fourth engine kind:

- Types/contract: `custom` joins EngineTypes, EnabledEngineTypes, and the
  proxy-engine identity (custom-proxy source); new `custom:list/refresh/
  add/remove/get-port/set-port` invoke channels with a CustomEndpointInfo
  that carries keySet, never a key. A new MANIFEST_ENGINE_TYPES keeps the
  compact engine surfaces (tray Engines n/m, node toggle row) counting
  only installable engines.
- Service bridge: custom channels execute locally or relay to a paired
  peer through the broker with a dedicated 45 s relay budget; a
  self-addressed nodeId runs the local path with addressing stripped.
  Reply entries are rebuilt from known fields only, so an apiKey a
  malformed reply smuggles cannot cross into the renderer, and a
  peer-addressed read failure goes read-only rather than pretending the
  peer has no endpoints. redact-log gains apiKey/api_key as a second
  layer keeping keys out of every log sink.
- UI: a per-node Custom endpoints section (add/edit form with explicit
  port and key-clear edit, health and model chips per card, editable
  proxy port field), wired through pairApi; welcome and engine-
  capability records learn Custom with no install/model operations.
- Tests cover the endpoint model, the UI section, and the updated engine
- identity and demo-lifecycle expectations; the generated service API
  doc, contract exceptions, and parity notes are regenerated.

Signed-off-by: Steven Beverly <thestevbev@gmail.com>
A node that fronts Custom endpoints lists them, read-only, between the
hardware readout and the Engines/Models panes: name, address, `down`
when the last probe failed, `[key]` where a key is configured, plus the
shared custom-proxy port — movable with `c` on this machine only while
the facade is listening. Configuration itself stays with the owning
node's desktop app. Model operations against an unmanaged engine kind
answer `custom endpoints manage their own models` before either the
local or the relay path, and demo runs skip unmanaged engines so the
self-check never spends the operator's money on a third-party service.
The Jobs tab lists the Custom facade's endpoint line. README and the
terminal-interface guide updated.

Signed-off-by: Steven Beverly <thestevbev@gmail.com>
Four suites over the real binaries in services/tests: the advertise plane
(custom attribution received through the scanner copy and published by a
live engine-manager), the end-to-end config-to-routing chain with outage
recovery on probe misses, peer-origin routing over real cluster mTLS
between two paired nodes (key injection on the owner's last hop, caller
Authorization stripped, unpinned peers refused), and remote endpoint
editing across the carrier, asserting the API key lands only in the
owner's 0600 store and never on an output stream.

Signed-off-by: Steven Beverly <thestevbev@gmail.com>
The per-node "Engines (n/m)" line was built from the padded manifest
row list: it fabricated an `initializing` placeholder for every
manifest engine type, so the denominator was always 3 even for nodes
with nothing installed, and it never saw user-configured Custom
endpoints. A node running only one custom endpoint read
"Engines (0/3)" instead of "Engines (1/1)".

Count the engines a node actually has instead: manifest entries the
node reported with an installed process state, plus the node's
per-endpoint custom:list snapshot (configured = entry, online =
healthy verdict). Custom health has no push feed, so the tray pulls
that snapshot itself on node-set change, on window focus, and on a 30s
cadence while the surface could be visible; a failed peer read keeps
the previous snapshot, because unknown is not "no custom engines".

Also drop the dead single-engine label branch: the enabled engine list
has four kinds, so its condition could never hold.

Signed-off-by: Steven Beverly <thestevbev@gmail.com>
Clear the ten prettier warnings the custom-engine commits left in
redact-log, ws-channels, custom-endpoints-api, CustomEndpointSection,
and their tests. Formatting only; typecheck, unit tests, and a clean
`npm run lint` verified after the fix.

Signed-off-by: Steven Beverly <thestevbev@gmail.com>
@stevbev
stevbev force-pushed the feature/custom-engine branch from 54b2ae6 to 73175cb Compare November 10, 2026 02:29
@Noah-Tervalon-Nvidia

Copy link
Copy Markdown
Collaborator

I think this idea is something we do want to do in the future, we are thinking of how exactly we want to engineer it though. We'd love effectively a universal adapter and need to take some time to design that internally. When we have ideas about the exact execution we would love to work with you to implement it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants