Guides
Serving
llamay serve is one process holding one or more models, a
scheduler in front of them, and four API families on one address. This page
is what to set and why, rather than a list of flags with their help strings
repeated.
Starting it
llamay serve # first decoder in the store, 127.0.0.1:11435
llamay serve -m gemma3:270m -addr :8080 # a specific model, a specific address
llamay serve -batch 8 -max-loaded 3 # continuous batching, three models resident
llamay serve -m azmx-one-q4.gguf \
-embed all-minilm.gguf -rerank ms-marco-MiniLM-L6-v2.gguf
With no -m, serve takes the first decoder
in the store, sorted by name so a restart serves the same one. A decoder
rather than simply the first name, because an encoder-only server refuses
every generation route — and on a store holding four decoders and one
all-minilm, sorting alone served the encoder. Both llamay's own
store and Ollama's are searched, which is the same pair -m
resolves against. The model named by -m is pinned, which
is what makes a request that names nothing always have an answer.
On a machine with no models yet, serve starts anyway and
answers the routes that mean something without one: /healthz,
empty /v1/models and /api/tags, and
POST /api/pull. Everything else answers 503
with the reason and the command that fixes it. The moment a model lands —
from that route or from llamay pull in a terminal — the same
process loads it and starts serving properly, with no restart.
It matters because every packaged install starts the server with no
-m, so "empty store" and "fresh install" are the same moment.
This used to exit, and a supervisor that restarts on exit turned that into
a loop every three seconds — while the macOS app, which pulls its first
model through /api/pull, had no server to pull through.
The scheduler
Flags
| Flag | Default | Meaning |
|---|---|---|
-addr | 127.0.0.1:11435 | Listen address. |
-m | first in the store | The pinned model. A name, or a path — a path that exists always wins. |
-concurrency | 2 | Generations allowed to run at once. |
-batch | 0 | Sequences decoded together. 0 runs each generation on its own goroutine. |
-prefill-chunk | 256 | Prompt tokens prefilled per scheduler step, so a long prompt does not stall decodes. |
-prefix-entries | 32 | How many shared prefixes the radix tree keeps. An entry holds page references, so this is a memory bound as much as a hit-rate one. |
-max-loaded | 2 | Models resident at once. A request for another loads it and may evict an idle one. |
-keepalive | 5m | How long an idle model stays loaded. 0 keeps every loaded model forever. |
-kv | f32 | Cache precision: f32, vq8 or q8. The trade. |
-api-key | — | Required on every route but /healthz. Prefer LLAMAY_API_KEY. |
-embed · -rerank | — | An encoder and a cross-encoder served beside the decoder, in the same process. |
-pull | true | Whether POST /api/pull, /api/copy and DELETE /api/delete may write to the store. |
-otlp · -traces | — | OTLP/HTTP collector for spans. Also read from OTEL_EXPORTER_OTLP_ENDPOINT. |
-gpu · -ngl | off | Run the layer stack on the device. -ngl is llama.cpp's spelling of the same thing. |
-v | off | Log every request. |
Model residency
The server loads models on demand. A request naming any model this machine has loads it, evicting one that has gone idle.
llamay serve -m qwen2.5:0.5b -max-loaded 3 -keepalive 5m
curl localhost:11435/api/chat -d '{"model":"gemma3:270m","messages":[…]}' # loads it
curl localhost:11435/api/ps # what is resident
Two properties are worth stating because they are the ones a naive implementation gets wrong. A model is never unloaded while a request is using it — eviction counts references rather than trusting the idle timer to be slower than a generation. And concurrent requests for the same unloaded model produce one load, not one each.
Authentication
-api-key, or LLAMAY_API_KEY, requires a key on every
route but /healthz — which stays open because the thing reading
it is usually a container probe with no way to carry a secret.
A key on the command line is visible in the process list to everyone on the
machine. And without a key the server is open, which is the right default
for something bound to localhost and the wrong one the moment
-addr 0.0.0.0 is typed — along with -pull=false,
since anyone who can reach /api/pull can make the process fetch
a URL and spend the machine's disk on it.
Health, metrics and traces
| Route | What it answers |
|---|---|
GET /healthz | {"status":"ok","model":…,"id":…}. This is identity, not liveness — a 200 from a port tells you something is listening, not what. |
GET /metrics | Prometheus, including the scheduler's own counters: queue depth, admissions, prefill and decode tokens, evictions. |
GET /v1/stats | The same numbers as JSON. |
| OTLP | Spans for the request, the queue wait, prefill and decode, joining the caller's traceparent, on both the sequential and the -batch path. |
Running it as llama-server
make server also produces bin/llamay-server, which
is a link to the same binary: the program reads argv[0] and
preselects serve under that name, so deployment tooling written
for llama-server needs no subcommand. One artifact, two names,
nothing that can drift.