Install and run¶
You can install ApiPi from PyPI or from a git checkout. A laptop try
uses SQLite in the current directory and isolation none. Production
isolation is APIPI_RUN_MODE=microvm on workers (or on combined
apipi serve on one box). One process can keep SQLite. Several
processes share Postgres. How isolation and workers fit is in
Concepts.
Live turns need the Pi CLI (pi --mode rpc) on PATH and a model
host URL. The gateway pins Pi 0.85.1. apipi install can install that
Pi CLI, a Firecracker microVM, or both. On a TTY with no flags it asks
what to install (default is Pi). Without a TTY it installs Pi only, so
scripts and CI keep working. Put the Pi binary on PATH, or set
APIPI_PI_COMMAND. You can also install Pi yourself:
From PyPI¶
Python 3.13:
pip install geki-apipi
apipi install
export OPENAI_BASE_URL=http://your-model-host/v1
apipi migrate
apipi serve
The import package and CLI are apipi. uv add geki-apipi works in a
project. S3-compatible artifact storage is an extra:
pip install "geki-apipi[s3]".
When DATABASE_URL is unset, the process uses SQLite at
.apipi/apipi.db in the current working directory, next to
.apipi/sessions. Run apipi migrate before apipi serve. Isolation
defaults to none and logs a warning. Unset
APIPI_VAULT_MASTER_KEY uses a local default for MCP vault tokens and
logs a warning; set a 32-byte key in production. The process binds
0.0.0.0:8000. OPENAI_BASE_URL is the model host that Pi calls.
apipi check verifies requirements and then exits. It leaves HTTP
unbound. --skip-db and --skip-model skip the store and the model
host. --fast skips the throwaway sandbox probe for microvm.
--role api|worker|all matches how the host will run (default all
is combined API plus sandbox). apipi install is idempotent; --force
reinstalls Pi and MicroVM files that are already present.
apipi install --role api installs nothing extra. --role worker
installs MicroVM.
Pi and MicroVM¶
Flags skip the prompt: --pi, --microvm, and --image default|browser.
--dry-run prints the commands and exits. --image without --pi
installs only the microVM.
apipi install --pi
apipi install --microvm
apipi install --microvm --image browser
apipi install --pi --microvm
--microvm pulls a prebuilt image when APIPI_IMAGE_SOURCE is set.
--build keeps the local recipe build for an air-gapped host.
--dry-run prints which of those it would run.
--microvm checks /dev/kvm, ip, iptables, and tc (it names
the packages; it does not run apt). It downloads pinned Firecracker
1.17.0 and jailer into $XDG_DATA_HOME/apipi/firecracker (or
~/.local/share/apipi/firecracker). With APIPI_IMAGE_SOURCE set it
pulls verified images into the images dir. Without that setting, and
with --build, it builds from images/<id>/ into
$XDG_CACHE_HOME/apipi/microvm (or ~/.cache/apipi/microvm). The
loop mount for a local build still needs sudo, the same way
./images/build.sh does. ./scripts/microvm-rootfs is a wrapper for
that script. A pull prints the images dir. A build prints export
lines for the kernel and rootfs. It does not write .env or
apipi.toml, and it does not set APIPI_RUN_MODE.
The Firecracker tarball also contains .debug binaries. Install
copies the release firecracker and jailer only. It skips that
download when both firecracker --version and jailer --version
exit 0 and report the pinned version. A missing or crashing jailer
is replaced on the next apipi install --microvm without --force.
--force still replaces binaries that already pass those checks.
When those image paths are unset, apipi serve and
apipi microvm shell use the cache files if they exist. Env, .env,
and [sandbox].kernel / rootfs still override. Missing files fail
with apipi install and the path that was looked at. Firecracker and
jailer are found on PATH, then in that install prefix, then under
SUDO_USER when the process is root.
apipi microvm shell needs a TTY. If you are not root, it re-runs
itself with sudo -E, the absolute Python interpreter, and PATH /
HOME kept, so sudo secure_path does not need uv. It never runs
sudo uv. apipi serve does not re-exec. TAP and jailer still need
root or the capabilities in run modes.
Build and publish guest images¶
apipi images build <id> runs the recipe in images/<id>/ and writes
a zstd rootfs plus manifest.json. It needs the same root, loop
mount, and packages as ./images/build.sh. --arch only checks that
you asked for this host. Cross-build is not supported.
apipi images publish --to <uri> uploads those files. <uri> is
s3://bucket/prefix or file:///path. https:// is read-only and is
rejected. S3 uses APIPI_S3_ENDPOINT, APIPI_S3_REGION, and
APIPI_S3_ADDRESSING. Credentials come from the AWS environment or
the instance role, not from TOML. Install the client with
uv sync --extra s3. Publishing the same image version again fails
unless you pass --force. --dry-run prints the object names.
The optional Images workflow builds the official default and
browser images and attaches them to a GitHub release. After that
runs, the release asset URL is an https:// image source. See
production.
From a git checkout¶
git clone https://github.com/GEKI-AI/apipi.git
cd apipi
uv sync
uv run apipi install
export OPENAI_BASE_URL=http://your-model-host/v1
uv run apipi migrate
uv run apipi serve
Prefix every apipi command with uv run while you work from the
checkout. Contributors use this path for tests and docs:
./scripts/check, and ./scripts/check --docs when Markdown changed.
Production store¶
One apipi serve can keep SQLite. File SQLite uses WAL and foreign
keys. Several processes, or HA, use Postgres. A Compose file at the
repo root starts Postgres 17 (user apipi, password apipi, database
apipi) on port 5432:
docker compose up -d postgres
export DATABASE_URL=postgresql+asyncpg://apipi:apipi@localhost:5432/apipi
apipi migrate
apipi serve
postgres:// and postgresql:// URLs are rewritten to
postgresql+asyncpg://. You can put the URL in .env or apipi.toml.
Give each process its own SQLite file, or share Postgres instead.
apipi migrate applies Alembic revisions (0001_initial, then
0002_workers). Databases created before 0.1.0 have no upgrade path
through the old revision chain. Recreate the database, then migrate.
Docker API¶
The Compose file can run Postgres and a rootless API container.
The image runs apipi serve --api-only. It does not get /dev/kvm
or TAP. Workers stay on Linux hosts:
export OPENAI_BASE_URL=http://your-model-host/v1
export APIPI_WORKER_TOKEN=secret
docker compose up --build
That publishes Postgres on 5432 and the API on 8000 at
0.0.0.0. Set OPENAI_BASE_URL or serve exits. Put
APIPI_WORKER_TOKEN in the environment so workers can connect.
self_hosted runners still attach to /v1/environments/{id} on the
API; they are not the worker.
On a KVM host:
apipi install --role worker
APIPI_API_URL=http://api.example:8000 APIPI_WORKER_TOKEN=secret \
APIPI_RUN_MODE=microvm apipi worker
Unit files are in deploy/systemd/.
Three ways to run¶
Everything starts through the apipi CLI. uvicorn is not a
supported operator path.
Combined (one host)¶
Laptop or a single server. API and sandbox share one process.
apipi install
export OPENAI_BASE_URL=http://your-model-host/v1
apipi check
apipi migrate
apipi serve
Isolation defaults to none. For Firecracker on that same box:
apipi serve probes the run mode. If microvm cannot start, the
process exits.
Split (API + worker)¶
Rootless API, KVM on another host. Example Compose plus a worker:
# API host (or docker compose up --build)
export OPENAI_BASE_URL=http://your-model-host/v1
export APIPI_WORKER_TOKEN=secret
apipi check --role api
apipi migrate
apipi serve --api-only
# KVM host
apipi install --role worker
export OPENAI_BASE_URL=http://your-model-host/v1
export APIPI_WORKER_TOKEN=secret
export APIPI_API_URL=http://api.example:8000
export APIPI_RUN_MODE=microvm
apipi check --role worker
apipi worker
The API never opens /dev/kvm. The worker probes Firecracker before
it connects. Put APIPI_WORKER_TOKEN in the process environment, not
in the browser. Example apipi.toml keys: worker_token is allowed
but secrets belong in .env.
# .env on the API and on each worker
APIPI_WORKER_TOKEN=secret
OPENAI_BASE_URL=http://your-model-host/v1
DATABASE_URL=postgresql+asyncpg://apipi:apipi@db:5432/apipi
Set APIPI_VAULT_MASTER_KEY on the API to a 32-byte key (base64 or
hex) so MCP vault tokens are not encrypted with the local default.
Several workers¶
One API tier, many KVM hosts, shared Postgres. Start more
apipi worker processes with the same token and API URL. Each worker
advertises capacity (from APIPI_MAX_SESSIONS) and memory_mb (from
APIPI_WORKER_MEMORY_MB, default max_sessions × mem_mib). Placement
picks the worker with the most free RAM among those that still have a
session slot and enough remaining RAM. A turn with no lease returns
429 with code capacity. Drain a worker with a heartbeat
"drain": true before you stop it.
API replicas do not need sticky routing for Pi. See multiple nodes.
Model URL¶
OPENAI_BASE_URL is required. It is the model host Pi calls. Clients
use a different URL for this gateway. On apipi serve, the process
lists {OPENAI_BASE_URL}/models, checks that pi --version is 0.85.1,
and exits before it binds HTTP if those checks fail.
The model key is the request Authorization: Bearer value. Auth maps
that bearer to a tenant. The raw bearer stays out of Postgres. It is
passed into the live Pi process as OPENAI_API_KEY. Optional
OPENAI_API_KEY_OVERWRITE replaces that key for every session when you
want one operator key instead of the caller's bearer. A process
OPENAI_API_KEY is ignored.
The agent.model on the request must exist on that host. An unknown id
returns 400 with code model_not_found. Clients can list those ids
with GET /v1/models, which proxies to the model host unless
APIPI_FORWARD_MODELS is off. Pi is started with that id and a
gateway-owned models.json.
Live turns also need Pi on PATH. You can override the binary with
APIPI_PI_COMMAND.
Serve¶
Production operators set APIPI_RUN_MODE=microvm so each session
boots in a Firecracker guest. That starts when /dev/kvm, Firecracker,
jailer, guest images, ip, iptables, and tc are present, and after
a throwaway guest has booted and been torn down:
The process default is none (Pi as a child of the gateway). It logs
a warning that this isolation is meant for laptops and CI:
apipi serve is the combined path: API plus a local sandbox in one
process. apipi serve --api-only is the control plane only. It does
not probe KVM or start Firecracker, so it can run in rootless Docker.
apipi worker connects outbound to that API (APIPI_API_URL,
--url, or http://127.0.0.1:8000) with APIPI_WORKER_TOKEN. See
sandbox workers.
That binds 0.0.0.0:8000 by default. --host, --port, and
--config change the bind and the TOML file. Startup also logs usage
store depth, retention, whether usage and payload export are on, and
whether Prometheus metrics and OpenTelemetry traces are on. Logs are
JSON lines on stderr and flush after each line. A POST logs
request start immediately with the URL path. A turn logs turn start, then microVM
boot/jailer/vsock and pi prompt / first pi event while it runs.
The HTTP request line is written when the stream ends. Guest kernel
and Firecracker console lines are debug (APIPI_LOG_LEVEL=debug).
microvm reaches the model URL and HTTP MCP through a TAP device.
Guest traffic uses that TAP rather than host loopback to Postgres.
The TAP may use the public internet and is rate-limited. Private and
special-use IPv4 ranges are rejected. An optional destination
allowlist can lock the guest to named public hosts.
Run
apipi install --microvm so the kernel and rootfs exist; unset, the
process uses those cache files. Production units still set explicit
paths in the environment file.
GET /health returns {"status": "ok"} without a bearer.
One apipi serve is one process. The Pi pool lives in that process, so
run a single uvicorn worker. Combined serve with several processes
needs sticky routing. API-only plus workers does not, for live Pi.
See production and multiple nodes.
systemd¶
Run the API under systemd as apipi serve --api-only. Run guests as
apipi worker with APIPI_RUN_MODE=microvm. Keep secrets in an
environment file that the unit loads. Worker units need /dev/kvm and
permission to create TAP devices. Files are in deploy/systemd/ and
run modes.
Example units: deploy/systemd/apipi-api.service (serve --api-only,
no KVM) and deploy/systemd/apipi-worker.service (DeviceAllow for
/dev/kvm and TAP). Both worker units need KillMode=control-group
so a stop or crash restart does not leave host Pi processes. Combined
serve on one box can still use apipi serve with
APIPI_RUN_MODE=microvm if that host is the hypervisor.
Environment variables in /etc/apipi.env override keys in the TOML
file. Bind, run mode, worker token, and the auth callback are the
usual ones to set there.
Auth callback¶
Unset APIPI_AUTH uses the default hash: any non-empty bearer is a
tenant. For production, point auth at an in-process function
package.mod:func. A small example is examples/auth_callback.py. See
auth.