Skip to content

Serve llama.cpp through its own proxy worker - #61

Merged
JaredP94 merged 10 commits into
mainfrom
feat/llamacpp-proxy
Nov 7, 2026
Merged

JaredP94 merged 10 commits into
mainfrom
feat/llamacpp-proxy

Conversation

@JaredP94

@JaredP94 JaredP94 commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

Description

llama.cpp PR 2 of 4: serve llama.cpp through PAIR. PR 1 (#60) added it as a managed engine on macOS arm64 without routing; this adds its own proxy worker, discovery and scheduling, so requests reach it locally and from verified peers.

  • New worker llamacpp-proxy (services/llamacpp-proxy, default port 1237), copied from lmstudio-proxy without changing that worker: model routing and strict routing, streaming passthrough, stall retry and cooldown with the opt-in first-body deadline, bounded bodies, workload identity and telemetry, cluster ingress over pinned mTLS with verified peers only, terminal peer ingress, and the shared listen helper for loopback port conflicts. llama.cpp has its own service identity, exact model IDs, and both /v1/models and /models lists.
  • Anthropic Messages: /v1/messages is routed, because llama-server b12171 serves it natively (tools/server/server.cpp:190).
  • Broker: supervises the worker like the other proxies (manual restart on a hang, bounded writes, settings convergence, the per-engine port-operation serialiser). The inventory is now 13 workers and 15 executables across AGENTS.md, docs, desktop, TUI, build scripts, installers and CI.
  • Discovery and scheduling: llama.cpp nodes advertise their service; model attribution stays per engine, so the same model name on two engines doesn't merge.
  • Model selection: llama.cpp's launch allowlist gains --model/-m, accepted only for a regular .gguf file inside PAIR's llama.cpp model cache ({install_dir}-models), with both the cache and file symlinks resolved and URI-shaped values rejected. Starting without a model (router mode) still works.
  • Settings: the engine-settings panel shows and edits the llama.cpp proxy port like the other engines; PR 1's zero-proxy-port exception and "not served through PAIR yet" notes are removed.

Co-authored-by credit for the upstream design: NVIDIA/Personal-AI-Router@6d2d67f.

Scope

New services/llamacpp-proxy; nvpair-ui-broker, nvpair-engine-manager, nvpair-node-scanner, nvpair-manual-nodes, nvpair-job-scheduler, shared, TUI, desktop, docs, build and installer scripts. Deferred: model hub, downloads and delete (PR 3); remaining desktop/TUI/release integration (PR 4).

Versions: llamacpp-proxy 1.0.0 (new); job-scheduler 1.2.0, manual-nodes 1.4.0, node-scanner 1.4.0; other touched components keep their existing unreleased bumps; product/installer unchanged.

Validation

  • Desktop: 945 tests in 95 files; typecheck, lint (zero warnings), dead-code and service-contracts checks pass. Engine-manager suite passes.
  • --model validation covers inside/outside the cache, symlink escapes, non-GGUF files and URI-shaped values.
  • Cross-process: a test starts the new proxy through the broker with a fake OpenAI engine and routes a request (PR CI).

Risk

A 13th worker adds a process and a listener on 1237. llama.cpp's macOS build is not notarized (see PR 1). Windows and Linux remain unsupported for llama.cpp.

Checklist

  • Every commit is signed off (git commit -s), certifying the Developer Certificate of Origin.
  • Tests cover the change.
  • Relevant documentation is updated; generated service contracts regenerated.
  • Versions bumped in services/versions.json.

Live check

2026-11-07 re-run, macOS 27.2 arm64, locally packaged PR head f21d3ac838cd335cbfe780aef870154d782e6f88 (not a released-asset certification). Built in detached /tmp/pair-work/live61b with npm ci --no-audit --no-fund && npm run -s build:mac:arm64. ZIP SHA-256 matched locally and remotely: 6426ba7c26829aeb52a9c922568e0cf489c199d37fce47843612b5c968756579. Python 3.9.6 isolated-mode driver used the packaged broker with the desktop's worker-path flags, including --llamacpp-proxy-path.

The previous run at f77de1f found oMLX moved from 1236 to 1238 and its saved revision advanced 1 → 2. This re-run confirms that regression is fixed in f21d3ac: oMLX stays on 1236 at revision 1.

Step Result Evidence
a PASS All 13 workers reported running. lsof recorded Ollama 11434, LM Studio 1234, oMLX 1236 and llama.cpp 1237, each on 127.0.0.1, ::1 and IPv6 *.
b PASS Pre-quit saved settings matched engine:get-settings after startup: Ollama revision 8/server 11435/proxy 11434; LM Studio revision 1/server 1235/proxy 1234; oMLX revision 1/server 1235/proxy 1236. No proxy-port or revision change.
c PASS PAIR installed and started llama.cpp; running and healthy, /props HTTP 200 with role router; settings server 8082, proxy 1237, empty launch text. No model downloaded.
d PASS 1237 /v1/models and /models: HTTP 200, zero models. Made-up-model chat: HTTP 502 in 1 ms, no available node advertises the requested model. Ollama 11434 and LM Studio 1234: baseline HTTP 503, model inventory unavailable; oMLX 1236: HTTP 200, 7 models.
e PASS engine:preview-settings rejected each of --model ../outside.gguf, --model /etc/hosts, --model https://x/y.gguf and --model missing.gguf, each with errors.launchText: model must be an existing regular .gguf file inside the PAIR llama.cpp model cache.
f PASS PAIR stopped and uninstalled llama.cpp; installed/running false and engine directory absent. Broker shutdown exited 0; no PAIR/nvpair/llama-server processes remained before restore.

Restored the original app and app data with ditto from fresh, verified ~/pair-live61b/backup/ (retained; older backups untouched). diff -rq passed (app-data check excludes Singleton* and logs); independent bytewise file, symlink, structure and mode verification matched before relaunch. Published PAIR 0.9.0 relaunched with broker + 12 live worker children; lsof confirmed the restored oMLX proxy on 1236. ~/pair-live61b/incoming removed. Local evidence: /tmp/pair-review/live-llamacpp/evidence-61b.jsonl. No live-check failures.

Proxy startup regression follow-up

Commit f21d3ac8 preserves each proxy's own default/saved port while retaining other engines' reservations, including stopped engines, and all engine-server/node-service exclusions. Upgrading no longer moves a free oMLX listener from 1236 or advances its settings revision unnecessarily. A usable persisted port is tried before a reported fallback; the port-operation serializer and unlocked worker inventory calls are retained.

Validation: broker race test binary passed twice; broker subpackage race tests, darwin/Linux/Windows vet, gofmt and SPDX checks passed. New regressions fail on f77de1f and pass with the fix. The isolated four-proxy startup test passed on Linux and macOS. CI run 42393452547, attempt 2, has all six jobs passing; the final Linux artifact has 127 passing tests and zero skips. The first Linux attempt hit an unchanged peer-discovery timeout in TestModelsHTTPEnrichment; it passed on retry. Cross-process tests were not run locally.

No additional service version bump: the broker already has the unreleased 2.6.0 bump versus 2.4.1 in v0.8.0, per the implementation brief.

JaredP94 and others added 10 commits November 7, 2026 15:57
Copy the LM Studio worker without changing its behavior, retaining routing, streaming, retry, workload and peer-ingress coverage. Give llama.cpp its own service identity and port 1237, exact model IDs and both model-list paths mapped to /models. Retain native Messages support verified in b12171. Adapt model-list semantics from NVIDIA/Personal-AI-Router@6d2d67f.

Co-authored-by: Terve <ntervalon@nvidia.com>
Signed-off-by: Jared Ping <13906915+JaredP94@users.noreply.github.com>
Validate --model and -m through the closed b12171 launch policy, resolving both cache and file symlinks and requiring a contained regular GGUF. Preserve no-model router starts, and use the selected model path as the single-model readiness and health identity. Publish the server inventory through the existing read-only list_models action. Engine-manager remains 1.5.0, the additive bump already present relative to v0.9.0.

Signed-off-by: Jared Ping <13906915+JaredP94@users.noreply.github.com>
A cache-relative filename could be parsed as a file URI without containing ://. Reject explicit URI schemes in non-absolute selections even when a same-named cache file exists. TestLlamaModelPathValidation was observed failing for file:model.gguf before this guard and passing afterward.

Signed-off-by: Jared Ping <13906915+JaredP94@users.noreply.github.com>
Supervise llamacpp-proxy alongside the existing proxies, with manual hang restarts, bounded writes, settings convergence, telemetry and terminal peer ingress. Advertise lc, preserve per-engine model attribution, and schedule llama.cpp work independently. Extend explicit bare-engine and verified-peer projections without granting previews routing authority.

Remove the zero-proxy-port settings exception and expose the reported proxy port in desktop and TUI. Update the 13-worker and 15-executable inventories, installers, CI routing coverage and generated contracts. Keep existing release evidence separate from the new source behavior. Add broker, discovery, manual probe and cross-process coverage; the latter is CI-only.

Signed-off-by: Jared Ping <13906915+JaredP94@users.noreply.github.com>
Open selected GGUF files read-only without following a final symlink, recheck the cache boundary and device/inode, and inherit only the verified descriptor into llama-server. Keep previews and saved selections separate from the child descriptor path and align owned-process probes with that path. Reject split filenames and split metadata because b12171 derives sibling paths for shards.

The fake-engine inode regressions fail on bf50b64 both before and after spawn. Engine-manager race tests and darwin/linux/windows vet pass. Its existing unreleased 1.5.0 minor bump since v0.9.0 covers this fix.

Signed-off-by: Jared Ping <13906915+JaredP94@users.noreply.github.com>
Use one fallback exclusion builder for llama.cpp, Ollama, LM Studio and oMLX. Reserve every engine server port from the static configured-ports inventory, including stopped and custom engines, plus accepted, persisted and live proxy ports and node-service listeners. Preserve per-engine port-operation ownership and release settings locks across inventory calls, revalidating Ollama plans before committing fallback state.

The stopped-engine regression fails on bf50b64 for all four proxies, including oMLX configured on 1237. Broker and all subpackage race-test binaries pass, along with darwin/linux/windows vet and SPDX. The existing unreleased broker 2.6.0 minor bump since v0.9.0 covers this fix.

Signed-off-by: Jared Ping <13906915+JaredP94@users.noreply.github.com>
Count the llama.cpp proxy in the broker inventory and build/test descriptions: thirteen workers and fifteen service executables. The worker table already includes llamacpp-proxy; keep that canonical row rather than adding a duplicate.

Signed-off-by: Jared Ping <13906915+JaredP94@users.noreply.github.com>
Opening only the model's final component without following symlinks still
allowed a cache parent to redirect both pathname resolutions to the same
outside inode after validation.

Resolve the cache once, walk its canonical ancestors from the filesystem
root with no-follow openat directory descriptors, then walk the selected
cache-relative model from the pinned cache descriptor. Validate the opened
regular GGUF directly and retain ExtraFiles inheritance without a second
pathname resolution. Close every intermediate descriptor on all paths.

Add parent-swap regressions before and after canonical cache resolution,
nested-model child inode and descriptor-leak coverage, and traversal checks.
Keep settings and previews unchanged. The engine-manager's existing
unreleased minor version remains sufficient for this fix.

Signed-off-by: Jared Ping <13906915+JaredP94@users.noreply.github.com>
The macOS runner exposed an EBADF race in os.ReadDir on /dev/fd: a
transient descriptor can close before the directory reader stats its entry.
Count Readdirnames results instead, closing the directory before checking
errors. Descriptor counting needs names only, not per-descriptor metadata.

Keep the leak assertion strict. A deliberate retained model descriptor
made the test fail with before=6 and after=7; removing it restored the
passing nested-model launch and failed-launch coverage.

Signed-off-by: Jared Ping <13906915+JaredP94@users.noreply.github.com>
Reserve other engines' default, saved, accepted and live proxy ports without excluding the requesting engine's own ports. Keep all engine-server and node-service reservations, including stopped engines, and retain the port-operation serialiser and unlocked inventory calls.

Try a usable persisted proxy port before selecting a reported fallback so an upgrade does not move oMLX from 1236 or advance its settings revision unnecessarily. Cover all four proxies, the live oMLX regression, and isolated four-proxy startup in the cross-process suite.

No additional version bump: nvpair-ui-broker already has an unreleased minor bump (2.6.0 versus 2.4.1 in v0.8.0), per the implementation brief.

Signed-off-by: Jared Ping <13906915+JaredP94@users.noreply.github.com>
@JaredP94
JaredP94 merged commit a9076b0 into main Nov 7, 2026
11 of 12 checks passed
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.

1 participant