Skip to content

Architecture

giveen edited this page Sep 12, 2026 · 1 revision

Architecture

┌────────────────────────────────────────────────────────────────────┐
│  UI (React 19 + Vite + Tailwind 4 + Radix + Lucide)                │
│  ┌──────────┬──────────────────┬───────────┬────────────────────┐  │
│  │ Chat     │ Engine           │ Models    │ Settings           │  │
│  └──────────┴──────────────────┴───────────┴────────────────────┘  │
│         │ fetch /api/*  +  fetch /v1/chat/completions (SSE)        │
└─────────┼──────────────────────────────────────────────────────────┘
          │  loopback
┌─────────▼──────────────────────────────────────────────────────────┐
│  CONTROL PLANE — Rust `ninfier-control` crate (axum + tokio)       │
│  inside the Tauri 2 desktop app (WebKitGTK window); standalone     │
│  `ninfier-control` binary in dev mode (Vite HMR against :8787)     │
│  • engine lifecycle: spawn / poll /health / SIGTERM / adopt        │
│  • profile → CLI args (build_serve_args, 1:1 with ninfer-serve)    │
│  • models dir scan, hf download supervisor, nvidia-smi stats       │
│  • engine log tail, SSE-safe API proxy                             │
│  • static hosting of the built UI (single process, no Node)        │
└─────────┼──────────────────────────────────────────────────────────┘
          │ spawn + stdout/stderr pipe · HTTP :8080
┌─────────▼──────────────────────────────────────────────────────────┐
│  NInfer engine (ninfer-serve, C++/CUDA, RTX 5090 / sm_120a)        │
│  OpenAI Chat Completions + Responses + Anthropic Messages + /health│
│  one resident .ninfer artifact · 1–8 lanes · KV cache tiers        │
└────────────────────────────────────────────────────────────────────┘

Repo layout

desktop/         Rust control plane + Tauri 2 desktop shell
  control/       ninfier-control: engine supervision, model scan, hf downloads,
                 coder endpoints, VRAM accounting, nvidia-smi stats, SSE-safe
                 engine API proxy, static hosting
  app/           ninfier-studio: Tauri 2 window hosting the control plane
apps/web         React 19 + Vite + Tailwind 4 frontend
                 (Chat / Coder / Engine / Models / Settings)
start-stack.sh   dev convenience: boot the control plane, seed config from env vars,
                 start/adopt the engine
~/.config/ninfier-studio/   per-user runtime state (CONFIG_HOME); AppData/Roaming/ninfier-studio
                 on Windows — config.json, profile.json, chats.json, last-start.json,
                 engine-<port>.log. Override location with NINFIER_STUDIO_DATA.

The control plane (desktop/control) is the only process that touches the engine. In development it runs standalone (pnpm control:start); in the desktop app it runs inside the Tauri core, so the engine's parent is the app itself — a single process supervises the engine.

Why this shape

  • The engine is a specialized single-GPU C++/CUDA binary — the UI must never import or interpret model code. It talks only the engine's documented HTTP contract.
  • Keep a thin, crash-proof supervisor between UI and engine. A CUDA fault in the engine must not take down the control plane. The supervisor is the Rust ninfier-control crate running inside the Tauri 2 desktop core: the engine's parent process is the app itself, so a native window + a single Rust process supervise everything. The same binary serves browser dev mode (Vite HMR against :8787). The UI code is identical in both because it only speaks the loopback HTTP contract.
  • The proxy (/v1/*, /health → engine port) keeps the UI on a single origin (no CORS), injects the configured API key, and is SSE-safe (chunks piped as they arrive).

Key lifecycle behaviors

  • adopt-don't-kill — if the configured port already serves /health: ok, the control plane marks the engine external (scans /proc for the ninfer-serve pid) and never double-spawns. The UI shows external · not spawned by studio and can still SIGTERM it.
  • start — builds args from the profile, spawns with cwd = engine dir, appends stdout+stderr to engine-<port>.log, polls /health every 2 s (3-min deadline), records fail reasons (e.g. VRAM shortfall) from the log.
  • stop — SIGTERM the child (8 s grace → SIGKILL); for external engines SIGTERM the discovered pid. Crashes are recorded with a log pointer, not auto-restarted.
  • native desktop shell — desktop/app (Tauri 2, WebKitGTK) opens a window pointed at http://127.0.0.1:8787; the control plane binds that port inside the app's own tokio runtime before the window loads (setup hook with a readiness channel), then serves apps/web/dist (SPA fallback). One native process hosts UI + control plane + engine parent.
  • production hosting — pnpm build → the app serves apps/web/dist at :8787; dev uses Vite :5173 with proxies (browser mode) or pnpm control:start (headless Rust control plane, no window).
  • stops the engine before Rebuild — so a freshly built binary can link cleanly.

Control-plane API

API Purpose
GET /api/status engine state (incl. adopted external pids, max context window), GPU, VRAM accounting, models, downloads, config (secrets redacted)
POST /api/engine/start profile + artifact → ninfer-serve argv; spawn, health-poll, log capture
POST /api/engine/stop SIGTERM the child (8 s grace) or the adopted external pid
POST /api/engine/update pull (git pull --ff-only) or build (stops a running engine first, then cmake/Ninja)
GET /api/logs?n=400 tail of the spawned engine's log
GET /api/models scan modelsDir for *.ninfer + registered catalog
POST /api/models/download hf download <repo> <file> --local-dir (uses HF_TOKEN when set)
GET /api/gpu nvidia-smi memory/util/process list
GET/POST /api/config app settings (config dir / config.json)
GET/POST /api/profile-state engine profile, chosen artifact, saved named profiles (profile.json)
GET/POST /api/conversations chat conversations + per-conversation params (chats.json)
/api/coder/* coder harness: workspace, tree/dirs/read/write/edit/patch, grep/glob, exec + jobs, safe mode, memory, git diff/log
GET /health, /v1/* SSE-safe proxy to the engine port (injects API key)

All state-changing endpoints accept only loopback Host/Origin values (browser cross-origin calls are limited to the allow-listed Tauri/Vite origins) — see Security.

Data & state

  • config.json — app settings (control-plane-owned).
  • engine-<port>.log — spawned engine stdout/stderr (appended, tailed by the UI; also the source of the VRAM capacity line).
  • localStorage / per-user store — conversations, chat params, engine profile, saved profiles, selected artifact.
  • Engine-side state (KV tiers, checkpoints, response store) is engine-owned; the UI never writes model state.

See Screens for how each screen uses this API, and Configuration for the persisted files.

Clone this wiki locally