Skip to content

Repository files navigation

Local Studio

Local Studio is a local-first workstation for running, managing, and using self-hosted LLM backends. One machine can launch models, watch GPU/runtime state, chat with OpenAI-compatible endpoints, and run agent sessions against local or remote controllers. Version 2.0 unifies day-to-day operation around Status, Workbench, Configure, and Usage instead of separate model, integration, and server surfaces.

It is built from two modules that share one controller API:

  • controller/ — Bun/Hono backend. Owns model lifecycle (launch, evict, recipes, downloads, runtime process coordination), an OpenAI-compatible proxy (chat, models, tokenization, audio), system state (GPU metrics, logs, usage, settings, SSE), and controller integrations.
  • frontend/ — Next.js 16 + React 19 UI and the macOS Electron desktop shell. Hosts the Workbench (/agent), consolidated Configure surface, settings, usage, logs, and browser-facing API routes.

What is a controller?

A controller is the backend process the UI talks to — the Bun/Hono server in controller/. You can run one locally or point the frontend at a remote controller on a GPU host. The controller owns model lifecycle, the OpenAI-compatible proxy, system state, and SSE event streams.

Architecture

flowchart LR
    User["User"] --> Desktop["Electron desktop app"]
    User --> Web["Next.js web UI"]
    Desktop --> Frontend["Frontend server / API routes"]
    Web --> Frontend
    Frontend --> Controller["Controller API (Bun + Hono)"]

    Controller --> Runtime["Inference runtime process"]
    Runtime --> Backends["vLLM / SGLang / llama.cpp / MLX recipes"]
    Controller --> Data["Local data directory"]
    Controller --> Events["SSE status and runtime events"]
    Frontend --> Agent["Pi coding agent runtime"]
Loading
flowchart TB
    subgraph Frontend["frontend/"]
        AgentPage["/agent"]
        Configure["/configure"]
        Settings["/settings"]
        Usage["/usage"]
        ProxyRoutes["/api/* proxy and agent routes"]
        DesktopMain["desktop/ Electron shell"]
    end

    subgraph Controller["controller/"]
        HttpApp["src/http/app.ts"]
        Engines["src/modules/engines"]
        Models["src/modules/models"]
        Proxy["src/modules/proxy"]
        Studio["src/modules/studio"]
        System["src/modules/system"]
        Audio["src/modules/audio"]
        Stores["src/stores"]
    end

    ProxyRoutes --> HttpApp
    HttpApp --> Engines
    HttpApp --> Models
    HttpApp --> Proxy
    HttpApp --> Studio
    HttpApp --> System
    HttpApp --> Audio
    System --> Stores
Loading

Quick start

Prerequisites: Bun 1.x (controller), Node.js 22.19+ and npm (frontend), Python 3.10+ on PATH (uv strongly recommended; engine installs fall back to pip), Git. vLLM/SGLang serving on Linux needs NVIDIA driver + CUDA; Apple Silicon uses the MLX backend.

Run the preflight check first (toolchain, ports, directories, network):

npm run doctor

Start the controller (listens on 127.0.0.1:8080, data dir + SQLite created automatically, model weights in LOCAL_STUDIO_MODELS_DIR, default /models):

cd controller && bun install && bun src/main.ts

Start the frontend in a second terminal, then open http://localhost:3000/setup:

cd frontend && npm ci && npm run dev

npm ci runs a postinstall patch against @earendil-works/pi-ai. If that step prints a warning, agent streaming may misrender. The setup wizard walks through choosing a models directory, installing an engine, downloading a model, launching it, and benchmarking. Engine installs (vLLM/SGLang/MLX) land in <data dir>/runtime/venvs/<backend>-latest.

Agent runtime

The agent surface lives at /agent in the frontend. It uses @earendil-works/pi-coding-agent through the frontend runtime rather than shelling out to a separate agent process for normal turns. Agent skills and extensions are discovered through Pi and surfaced in the session UI. Pi remains the source of truth for authentication, settings, resources, tools, and native JSONL sessions. The runtime respects PI_CODING_AGENT_DIR, PI_CODING_AGENT_SESSION_DIR, and Pi's sessionDir setting in the same precedence order as the CLI. Existing Local Studio session storage remains a read-compatible legacy source, while new sessions use Pi's resolved directory. Workbench sends only the active controller to Pi and shows that controller's advertised models by default. The model picker has an explicit Other models switch for models from the user's Pi catalog and providers connected in Configure. Those opt-in models use Pi's native provider routing without adding saved inactive controllers to the session.

New Workbench chats start with Pi's read, grep, find, and ls tools. Full access enables every tool registered in that Pi session, including extension tools. Read only is a model-tool allowlist, not an operating-system sandbox, and loaded extensions may still have their own behavior. Pi runs with the full permissions of the host user. Tailscale limits who can reach the dashboard; it does not sandbox Pi.

Runtime backends

Recipes launch through the controller runtime layer. Wired backend families:

  • vllm — vLLM server recipes through configured/discovered/system/Docker/bundled targets.
  • sglang — SGLang launch-server recipes through configured or discovered Python targets.
  • llamacpp — llama.cpp llama-server recipes for GGUF models.
  • mlx — MLX mlx_lm.server recipes for Apple Silicon.

Runtime target discovery, models, integrations, and server controls are surfaced in Configure; selections persist in the controller data directory.

Production

Build the frontend, then serve it with the standalone server:

cd frontend && npm run build && npm run start

npm run start launches the standalone server (scripts/start-standalone.mjs). Never use plain next start — it breaks SSE streaming. The controller runs the same way in production as in development: bun src/main.ts.

The production frontend binds only to 127.0.0.1 and defaults to port 4783. PORT may be set to an integer from 1024 through 65535. Workspace paths are canonicalized and must be under WORKSPACE_ROOTS, a platform-path-delimited list that defaults to the current user's home directory. Add mounted locations explicitly, for example WORKSPACE_ROOTS="$HOME:/Volumes/Projects" on macOS.

For private mobile access, first configure the exact Serve hostname:

cd frontend
ALLOWED_TAILSCALE_HOSTS=studio.example.ts.net npm start
tailscale serve --bg http://127.0.0.1:4783
tailscale serve status

Serve supplies a private HTTPS tailnet URL. Both devices must be in the intended tailnet, and ACLs or grants should restrict the URL to its owner. Do not use Tailscale Funnel. tailscale serve --bg persists the proxy configuration across Tailscale restarts and reboots; it does not start Local Studio. Optionally set ALLOWED_TAILSCALE_USERS to a comma-separated login allowlist. The Tailscale-User-Login header is trusted only because the backend remains bound to loopback behind Serve.

Manual availability requires npm start to remain active. An OS-native user service can start the compiled app after login and restart it after a crash, but it is intentionally not installed automatically. The host must still be on, awake, online, and connected to Tailscale.

Remote / LAN deployment

The controller binds 127.0.0.1 by default. Binding a non-loopback host (e.g. LOCAL_STUDIO_HOST=0.0.0.0) requires LOCAL_STUDIO_API_KEY — startup throws without it. On a trusted LAN you may instead set LOCAL_STUDIO_ALLOW_UNAUTHENTICATED=true to opt out of authentication.

Point the frontend at a remote controller with BACKEND_URL or NEXT_PUBLIC_API_URL (default http://localhost:8080).

Remote deployment is handled by scripts/deploy-remote.sh. Configure .env.local first (see .env.example):

REMOTE_HOST=192.168.x.x
REMOTE_USER=username
REMOTE_PATH=/home/user/project
# Optional: REMOTE_SSH_KEY (defaults to ~/.ssh/id_ed25519)
./scripts/deploy-remote.sh controller   # sync + build + restart controller
./scripts/deploy-remote.sh frontend     # sync + build + restart frontend
./scripts/deploy-remote.sh status       # inspect remote processes

Local daemon helper: ./scripts/daemon.sh {start|stop|status}. The controller installer registers a persistent user service automatically (launchd on macOS and systemd --user on Linux), so installed controllers return after login without a manual daemon command.

Validation

npm run check
npm run test:integration

The configured pre-push hook (.githooks/pre-push) checks conventional commits and runs the frontend quality gate (npm --prefix frontend run check:quality) before pushing.

Releases

Every successful main CI run builds and boots an isolated unsigned macOS app, checks the embedded frontend, agent runtime, native desktop bridge, and PTY, and keeps the exact-SHA package as a GitHub Actions artifact. Conventional commits then trigger release.yml. Semantic Release chooses the next version (feat → minor, breaking → major, all other allowed commit types → patch).

The release job runs on an arm64 macOS host, rebuilds the exact CI-tested revision, signs it with Developer ID, notarizes and staples it, rechecks that the revision is still origin/main, and only then creates the GitHub release with the DMG, updater files, stable website alias, checksums, and source manifest. There is no npm publish and tags are never created by hand.

To reproduce the notarized release locally with the keychain profile:

APPLE_KEYCHAIN_PROFILE=vllm-studio-notarize npm run release:build-desktop -- \
  --version 2.2.2 \
  --commit "$(git rev-parse HEAD)"

Remove frontend/dist-desktop/ and release-staging/ after installation and upload; neither directory belongs in git.

Contributing

Contributions should be small, focused, and easy to review. Start from the latest main, one logical change per branch, no formatting-only rewrites, no secrets or build artifacts. Run npm run check (and npm run test:integration for behavior changes) before opening a PR; include a concise summary, the validation commands you ran, and screenshots for UI changes. See AGENTS.md for the full code standards an agent (or contributor) must follow.

License

See LICENSE.

Releases

Packages

Used by

Contributors

Languages