plank โ€บ User guide โ€บ Remote and hosted engines

10. Remote and hosted engines

All of this is off by default. A plain plank runs the local Metal engine and nothing else.

Hosted providers

plank can drive a hosted model instead of the local one. The provider sits behind the same engine boundary as the Metal backend, so tools, sessions, /btw, compaction, and the rest of the agent loop behave identically โ€” native provider tool calls are translated back into plank's own tool protocol on the way through.

export OPENAI_API_KEY=sk-...
plank --provider openai --model gpt-4o

export ANTHROPIC_API_KEY=sk-ant-...
plank --provider anthropic --model claude-sonnet-4-5

One-shot works too: plank --provider anthropic --model <name> -p "โ€ฆ".

Flag What
--provider openai|anthropic provider family. openai speaks OpenAI-compatible Chat Completions; anthropic speaks the Messages API.
--model NAME the provider's model name, not a GGUF path. Required with --provider.
--api-key KEY the key, if you would rather not use the env var. Prefer the env var โ€” a key on the command line lands in your shell history.
--base-url URL override the endpoint. Defaults to https://api.openai.com/v1 and https://api.anthropic.com/v1.
--provider-cache on|off Anthropic prompt caching over the stable prefix (tools + system). On by default; ignored for OpenAI.

Key resolution โ€” --api-key wins, otherwise $OPENAI_API_KEY or $ANTHROPIC_API_KEY. With neither, startup fails with a clear message rather than a confusing API error.

OpenAI-compatible gateways โ€” --provider openai plus --base-url reaches anything speaking that shape: vLLM, Ollama, OpenRouter, Together, LM Studio.

plank --provider openai --model llama3.3 \
      --base-url http://localhost:11434/v1 --api-key ollama

What stays the same โ€” every tool, MCP tools, @ completion, sessions and /resume, /btw, compaction. The one difference is the system prompt: a provider gets plank's own prompt with native tool definitions, never the byte-parity DeepSeek prompt, which is meant only for the local model it was trained on.

Two hosted models on one key โ€” a subagent definition can point at a different model on the same endpoint with the same credential. Only the model line differs from the parent:

provider: openai
model: qwen3-coder-next                 # the only difference from the parent
base-url: https://api.regolo.ai/v1
api-key-env: REGOLO_API_KEY             # the same variable the parent uses

Since only the variable's name is in the file, the definition is safe to commit. To confirm the sidechain really reached the second model, check /usage: two model rows against one key rather than a single total. Asking the model to name itself is weaker evidence โ€” models are unreliable about their own identity, and billing is not.

Notes โ€” --provider cannot be combined with --remote or the local backend selectors; it is the engine for that run. /usage reports billed tokens including cache reads, writes, and hit rate. The key is never written to settings.json.

Serve and connect

Run the model on the machine that has the GPU, work from the machine you are sitting at.

# on the Metal box
plank serve

# on the laptop
plank --remote https://metal-box:PORT

The transport is synchronous, adds no async runtime, and streams tokens as they generate.

Flag What
--remote URL drive a remote plank serve host instead of a local engine
--remote-token TOK bearer token (or $PLANK_REMOTE_TOKEN)
--insecure allow plaintext http:// to a non-loopback host

Plain http:// is allowed to localhost and refused elsewhere unless you pass --insecure. Keep a real deployment behind an SSH tunnel or a TLS reverse proxy.

The server side has its own guard, and its own --insecure, which means something different. plank serve --listen ADDR on a non-loopback address with no --token refuses to start, because that hands the model to anyone who can reach the port; bind to 127.0.0.1, pass a token, or pass plank serve --insecure to serve unauthenticated anyway. So the client flag waives TLS, the server flag waives authentication, and each is checked only by the side it belongs to. The server also caps a request body at 16 MiB and drops a connection that sits silent for 30 s mid-request.

Shared engine

plank serve --shared-engine

loads the weights once and serves many concurrent sessions from a single cooperative GPU thread. The scheduler round-robins at token granularity, so sessions are time-sliced, not parallel โ€” there is one Metal queue. A freshly attached session restores the warm system-prompt prefix instead of cold-prefilling it.

Flag What
--max-sessions N admission cap (default 8)
--kv-budget-bytes B aggregate KV-bytes budget; reject an attach past it rather than OOM
--session-ctx-size N per-session context window (0 = model max; a client's own request overrides)
--idle-reclaim-secs S snapshot an idle session's KV to disk and restore it on the next request

/info reports live-session and KV accounting.

A host session belongs to a client, not to a turn. The client sends a stable X-Plank-Client-Id header, so many turns from one laptop reuse one warm session instead of attaching a new one each time; a session idle for 30 minutes is swept. An older client that does not send the header still works and is keyed per turn as before.

Remote control

plank --control            # loopback WebSocket on 127.0.0.1:31415

Another process, a browser, or a terminal client can attach to a running plank instance: it mirrors the output, sends prompts, commands, /btw questions and interrupts, and can take or hand back control. One controller at a time, many mirrors, with a reconnect grace window. A self-contained web client is served at /.

plank remote ws://127.0.0.1:31415/

is the terminal client: typed lines become prompts, slash lines become commands, /btw <q> becomes a side question, and Ctrl-C interrupts. The token defaults to $PLANK_REMOTE_TOKEN.

Flag What
--control[=ADDR] start the server (default 127.0.0.1:31415, loopback only)
--control-token TOKEN shared bearer token; otherwise $PLANK_REMOTE_TOKEN, otherwise one is generated and printed once to stderr
--control-allow let a remote client take control without a local grant (implied in headless server mode)
--control-origin ORIGIN allow a browser Origin on the WebSocket upgrade (repeatable or comma-separated)
--control-queue-max BYTES per-client outbound queue cap; a client that exceeds it is evicted (default 1048576)

Missing and loopback Origins are always allowed; other browser Origins are refused by default.

/rc (and /rc on) pre-authorizes control: typing the command is your consent, so a client opening the link can drive immediately. /rc ask starts the same bridge without that consent. An attaching client then mirrors output but cannot drive; its request surfaces locally as [remote session 3 wants control โ€” /grant or /grant 3 to allow] and waits. Bare /grant approves the oldest waiting request, /grant 3 approves that session. Approving one request declines any others, since only one client can drive at a time.

Some commands are refused over the wire rather than queued: /open, bare /kvcache and bare /resume need the local terminal, and /rc, /quit, /exit and /grant would cut off the client running them. The refusal comes back with its reason. The same commands with an argument (/kvcache gc, /resume 3) are not interactive and work normally.

--ui-remote

For driving the TUI from a test harness:

plank --ui-remote=7777

Opens a 127.0.0.1-only listener (bare --ui-remote picks an ephemeral port and prints it to stderr) accepting line-delimited JSON keypress, snapshot, and uitree commands. snapshot and uitree replies are held until the screen reflects any keys sent first, so a harness can assert without sleeping. One client at a time; a second queues.


Next: The arcade โ†’