Install channels
Every command on this page installs release 0.11.2 and works today (pip inside a virtual environment, below). The binaries, the npm tarballs and the image archive live in
https://airmet.ai/dl/v0.11.2/, and the install page shows the same commands for your platform.
Every channel ends in the same place: one static airmet binary in your infra
and one base-URL change in your client. A release is one binary per
platform: the pip wheel, the npm package and the bare download carry it byte
for byte, the Docker image (built for linux/amd64 only) carries the Linux
x86_64 one, and airmet version tells you which you have. The three setups (two pinned providers on one marketplace key,
two provider keys, one provider raced against itself) work identically on all
of them; the two-leg hedger.yaml shapes are on the landing's Start-here
block. Each of those files names policy: twinleg at alpha: 0.3,
which is what the banner below reports — and a file that names no policy gets
the same default, so it reports the same.
pip
Run pip inside a virtual environment (python3 -m venv .venv, then activate
it): on a stock Mac and on a Python that refuses system-wide installs, the
command as typed fails outside one.
$ pip install --index-url https://airmet.ai/simple/ airmet
$ airmet up --config hedger.yaml # a Start-here file: twinleg at alpha 0.3
airmet up · Go data plane · 2 leg(s) · twinleg α=0.3 · /v1 on 127.0.0.1:8080 · run dir airmet-results/up-…
pipx installs it in an isolated environment instead, in two commands:
$ pipx upgrade-shared
$ pipx install --index-url https://airmet.ai/simple/ airmet
From pipx 1.6 on, pipx's first install also fetches pipx's own copy of pip,
from the index that install names. Our index carries only airmet, so on a pipx
that has never run, the install alone fails with No matching distribution found for pip. pipx upgrade-shared fetches that copy from your default index
first. An older pipx has no upgrade-shared and needs only the second command.
The --index-url in both install commands points pip at our index alone,
so a same-named package on another index cannot win the resolution; the wheel
has no dependencies to fetch from anywhere else.
The wheel is the binary plus a short shim that execs it: no Python engine, no
runtime dependencies, Python ≥ 3.9 only to run the shim. The sidecar is a local
proxy, so your app language is irrelevant — Node, Go, and Python clients all
just change base_url.
airmet up takes one plane flag. --config hedger.yaml is the two-leg file
for legs with their own keys, models or per-leg provider
pinning: two providers with one leg each, or a pinned
pair on one marketplace key (the OpenAI-compatible
quickstart shows the file). --providers URL
(alias --replicas) is one provider, raced against itself — the sidecar adds a
twin leg on the same endpoint and model, with its own connections and session
id — or a comma-separated list of engines you run yourself, with --model-name
for the model id every leg receives and --api-key-env naming the key's
environment variable (the key itself never appears in argv):
$ airmet up --providers https://api.a.example/v1 \
--model-name <model> --api-key-env MY_KEY
Environment defaults exist for every flag you would put in a fleet config
(AIRMET_REPLICAS, AIRMET_MODEL_NAME, AIRMET_HOST, AIRMET_PORT,
AIRMET_POLICY, AIRMET_CONFIG, AIRMET_API_KEY_ENV).
The sidecar listens on 127.0.0.1, this machine only, because it attaches
your provider keys to every request it relays: whoever can reach /v1 spends
them. To serve other hosts, listen on every interface and require a token:
$ export AIRMET_TOKEN=$(openssl rand -hex 32)
$ airmet up --config hedger.yaml --host 0.0.0.0 --auth-token-env AIRMET_TOKEN
Clients then send Authorization: Bearer $AIRMET_TOKEN. In hedger.yaml the
same two settings are proxy.listen: "0.0.0.0:8080" and
proxy.auth_token_env: AIRMET_TOKEN. A bind beyond loopback without a token
still starts, with a warning in the boot log.
With no --policy, the quick path twinlegs at α = 0.3: about 30% of
requests race both legs — the provider and its twin, when there is one — and
the slower leg is not cancelled for being slower: it runs to its own finish
unless the runaway guard stops it for looping or running far past the winner,
so its prompt cache stays warm (the cost is on the OpenAI-compatible
quickstart). --hedge-n 1 serves one leg alone. A
config file races the policy it names, and one that names none gets the same
default.
One thing the quick path cannot do: prove that a cancelled leg is refunded.
That matters once you name a policy that cancels the loser (--policy deferred-p95, immediate or tied): such a policy fires only on legs whose
abort-billing verdict is a measured refund, and --providers legs carry no
route identity to match a verdict against, so under it they are served
without a race and the boot log says so, per leg. Use hedger.yaml with a
route per leg to pick up the measured verdicts, or pass --on-no-refund allow to race regardless — the loud override for a pilot.
npm
$ npm install https://airmet.ai/dl/v0.11.2/airmet-cli-linux-x64-0.11.2.tgz https://airmet.ai/dl/v0.11.2/airmet-0.11.2.tgz
$ npx airmet up --config hedger.yaml
That is the Linux x86_64 command; the install page gives the command for your platform.
import OpenAI from "openai";
import { wrap } from "airmet";
const client = await wrap(new OpenAI(), { model: "gpt-4o-mini" });
The airmet package carries the same binary in a per-platform companion
package (no postinstall download) and a wrap()
helper that starts a sidecar on demand and points your OpenAI client at it.
The Python wheel has the same helper:
client = airmet.wrap(OpenAI(), model="…"). If the binary is missing or the
sidecar cannot start, wrap() returns your client untouched and warns once —
your requests go where they went before, so a failed start does not break
your app.
From 0.11.1 the sidecar wrap() starts lives as long as your app. It has no
idle timeout, it stops when the app exits or is killed, and each process that
calls wrap() starts its own. On 0.11.0 it exits after 10 minutes without a
request, and a client wrapped before then keeps the old address: its next
request is refused. On 0.11.0, in a long-running app, wrap a fresh provider
client again after a quiet spell (not the wrapped one, whose base URL is now
the sidecar's), or pass a longer idle_exit_s (Python) or idleExitS (Node).
Binary
Download airmet-<platform> from https://airmet.ai/dl/v0.11.2/ (linux-amd64,
linux-arm64, darwin-arm64, darwin-amd64, windows-amd64.exe), verify
it against the SHA256SUMS in the same directory, and run it:
$ curl -fsSLO https://airmet.ai/dl/v0.11.2/airmet-linux-amd64 && curl -fsSLO https://airmet.ai/dl/v0.11.2/SHA256SUMS
$ grep " airmet-linux-amd64$" SHA256SUMS | sha256sum -c - && chmod +x airmet-linux-amd64
$ ./airmet-linux-amd64 version
That is Linux. The binary's own line goes to sha256sum on stdin because
BusyBox's sha256sum (Alpine) has no --ignore-missing. A Mac checks with
shasum -a 256 -c --ignore-missing SHA256SUMS. The install page shows the lines for each platform,
Windows included. It is the same file the wheel bundles. The Windows build is
published with every release but has not been tested on Windows yet.
Docker
$ curl -fsSL https://airmet.ai/dl/v0.11.2/airmet-image-v0.11.2.tar.gz | docker load
$ docker run -p 127.0.0.1:8080:8080 \
-v airmet-data:/data \
-v $PWD/hedger.yaml:/data/hedger.yaml \
-e MARKETPLACE_API_KEY \
airmet:v0.11.2 up --config /data/hedger.yaml
The first line fetches the image, which is built for linux/amd64.
The command above mounts a named volume on /data, then hedger.yaml into
it. The install's own state lives there: the telemetry choice, the install id
and a login in /data/config, and the local records under
/data/airmet-results. Every container started on that volume is the same
install, so a --no-telemetry given once stays in force and the first-run
notice is shown once. Without a volume on /data, each new container starts
as a first install: the notice again, a new install id, and telemetry on
unless that run is given --no-telemetry too. Docker fills a new named volume
from the image's /data, which the image's non-root user (uid 65532) owns, so
it starts writable; in Compose, declare it under volumes: the same way. A
host directory mounted on /data instead has to be writable by uid 65532.
When it is not, airmet exits with an error that names the directory and the
fix.
Inside the container the sidecar listens on every interface (the image sets
AIRMET_HOST=0.0.0.0), so the port mapping decides who reaches it:
-p 127.0.0.1:8080:8080 keeps it on this machine. Publishing it wider, with
-p 8080:8080 or a Kubernetes Service, puts it on the network, so pass a token
as well: -e AIRMET_TOKEN to docker run and --auth-token-env AIRMET_TOKEN
to up.
Same binary, same flags, the config file mounted in, keys from the environment,
same :8080/v1 surface (admin, /metrics, /healthz and /readyz are on the
same port).
The image is distroless — the binary, CA certificates and nothing else — and
runs as a non-root user. /readyz answers 200 only while at least one leg is
healthy, and the image's HEALTHCHECK uses it; point your orchestrator's
readiness probe at it too, and no traffic is routed to a pod whose whole pool
is down. Serving keeps no durable state, so
run it as a plain Deployment next to your app: any replica serves any request,
and restarts need no ordering. What a volume on /data keeps is the install's
own state (the last footnote).
Three honest footnotes on what "no durable state" does not mean:
- In-flight races are ephemeral in-process state. If a pod dies mid-race, the raced legs are cancelled by their sockets closing — providers that honor a disconnect stop billing there; those that don't will run those last replies to completion and bill them. A crash costs you at most the in-flight races, never correctness.
- Adaptive policies re-learn after a restart unless the pod is linked to
the served-priors feed or has a warm-state file under
/data: a fresh pod races nothing until it has seen enough traffic (~100 requests) — a brief cold start, not an error. The abort-billing verdicts never go cold: they are baked into the binary. /dataholds the install's own state: the telemetry choice, the install id, the local records, the window log and the egress transcript (airmet trafficreads them). A pod with no volume there starts over each time it starts: it counts as a new install, and an opt-out has to be in its arguments (--no-telemetry), since no saved choice survives.
vLLM / SGLang — your own GPUs
Front the replicas you already run; the replicas race each other. You need at least 2 replicas — never race a single saturated pool. With one replica, the second leg lands on the same queue that made the first request slow, so you double the load and cut nothing. See the vLLM / SGLang quickstart.
LiteLLM as upstream
Keep LiteLLM for auth, routing, and spend tracking; race beneath it by routing the models you want raced through Airmet. See the LiteLLM quickstart.
n8n
No custom node: paste the sidecar URL into your OpenAI credential's Base URL field and every AI node in every workflow runs through the sidecar from the next run. See the n8n quickstart.
IDEs and coding agents
Cursor, Continue, Aider and the other tools that take an OpenAI base URL are one settings change away: point them at the sidecar and keep the model name it advertises. See the IDE quickstart.
MCP
Agents that reach models through MCP servers can race too: point the MCP server's model endpoint at the sidecar exactly as you would any OpenAI-compatible client — the sidecar is one URL, whatever sits in front of it.