Build with llamay

Integrating llamay

What it takes to put llamay behind something you have already built. In most cases the whole migration is a base URL, because llamay answers the request shapes your client already sends.

The shape of the work

1 · DECIDE where it runs a laptop, a server you own, or ours 2 · POINT change a URL the SDK you have keeps working 3 · CHOOSE a model by name, or the server's default 4 · HANDLE refusals 400, 404, 429, 503 each mean one thing 5 · SHIP deploy a binary and a systemd unit
Steps two and three are usually the whole job. Four is what separates a prototype from something on call.

1 · Decide where it runs

There are three answers and they are not exclusive — the same client code reaches all of them, because the address is the only difference.

WhereAddressChoose it when
The machine in front of youlocalhost:11435 Development, or an application that must work with no network.
A server you ownYour host, any port The data cannot leave your estate. One binary, no runtime to install.
llamay's hosted serviceapp.llamay.com You want models larger than your hardware holds, and you are willing to send the prompt to run them. See pricing.

The third row is the only one where the prompt leaves your machine, and only when you send it there. A local server answers from local models.

2 · Point a client at it

llamay answers three request shapes on one port. Whichever SDK you already use, the change is the base URL. The API in five minutes runs each of these and shows what came back.

An OpenAI client

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:11435/v1",
    api_key="not-needed-locally",      # any string until the server has keys
)

answer = client.chat.completions.create(
    model="qwen2.5:7b",
    max_tokens=512,
    messages=[{"role": "user", "content": "Summarise this contract."}],
)

An Anthropic client

from anthropic import Anthropic

client = Anthropic(base_url="http://localhost:11435", api_key="local")

answer = client.messages.create(
    model="qwen2.5:7b",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Summarise this contract."}],
)

Or nothing at all

curl http://localhost:11435/v1/chat/completions \
  -H 'content-type: application/json' \
  -d '{"model":"qwen2.5:7b","messages":[{"role":"user","content":"Hello"}]}'

The same server also answers Ollama's /api/chat and /api/tags, so tooling written against Ollama works unmodified. See the HTTP API reference for every route.

What the change looks like in a client you have already written:

Transcript — the whole session as text
~/llamay $ llamay serve -m qwen2.5:0.5b -addr localhost:11439
time=2026-09-18T08:51:03.682-04:00 level=INFO msg="device graph attached" max-tokens=256
time=2026-09-18T08:51:03.682-04:00 level=INFO msg="llamay serving" addr=localhost:11439 mode
l=qwen2.5:0.5b arch=qwen2 max-loaded=2 keepalive=5m0s params="0.36 GB" context=32768 threads
=7 kv=f32 lexicon=0

  OpenAI      POST http://localhost:11439/v1/chat/completions
  Anthropic   POST http://localhost:11439/v1/messages
  Embeddings  POST http://localhost:11439/v1/embeddings     (start with -embed <encoder.gguf
>)
  Rerank      POST http://localhost:11439/rerank            (start with -rerank <reranker.gg
uf>)
  Contexts    POST http://localhost:11439/v1/contexts        (then /{id}/fork?n=8, /{id}/sna
pshot)
  Models      POST http://localhost:11439/api/pull          (and /api/copy, /api/delete)
  Stats       GET  http://localhost:11439/v1/stats


~/llamay $ # the server stays up; the rest of this is a second terminal
~/llamay $ # the official OpenAI client, in a clean virtualenv
~/llamay $ python3.12 -m venv build/demo/work/venv && build/demo/work/venv/bin/pip install -
q openai \
>   && build/demo/work/venv/bin/python -c 'import openai; print(openai.__version__)'

[notice] A new release of pip is available: 25.0.1 -> 26.2.1
[notice] To update, run: python3.12 -m pip install --upgrade pip
3.15.0
~/llamay $ cat scripts/site/demo_client.py
#!/usr/bin/env python3
"""A script written against OpenAI, run against llamay.

Nothing here knows what it is talking to. `OpenAI()` reads OPENAI_BASE_URL and
OPENAI_API_KEY from the environment, which is the documented way to point the
official client at a compatible server, and llamay is one. This file is in the
repository so the demo on /docs/migrate/ can `cat` the thing it then runs.
"""

import os

from openai import OpenAI

client = OpenAI()

print("base_url:", client.base_url, "\n")

chat = client.chat.completions.create(
    model=os.environ.get("MODEL", "qwen2.5:0.5b"),
    messages=[{"role": "user", "content": "In one sentence: what is a KV cache for?"}],
    max_tokens=60,
)
print("chat:", chat.choices[0].message.content.strip())
print("usage:", chat.usage.prompt_tokens, "prompt +",
      chat.usage.completion_tokens, "completion\n")

print("stream: ", end="", flush=True)
for chunk in client.chat.completions.create(
    model=os.environ.get("MODEL", "qwen2.5:0.5b"),
    messages=[{"role": "user", "content": "Count from one to six."}],
    max_tokens=40,
    stream=True,
):
    piece = chunk.choices[0].delta.content
    if piece:
        print(piece, end="", flush=True)
print("\n")

print("models:", ", ".join(m.id for m in client.models.list().data))
~/llamay $ # one environment variable is the entire migration
~/llamay $ OPENAI_BASE_URL=http://localhost:11439/v1 OPENAI_API_KEY=unused \
>   MODEL=qwen2.5:0.5b build/demo/work/venv/bin/python scripts/site/demo_client.py
base_url: http://localhost:11439/v1/

chat: The Cache (KV) Cache is a cache used to store key-value pairs for quick access to data
.
usage: 19 prompt + 21 completion

stream: Sure, here's the sequence from 1 to 6:

1, 2, 3, 4, 5, 6

You can continue counting by following the pattern of increasing

models: all-minilm:latest, gemma2:2b, gemma3:270m, gpt2:q8_0, qwen2.5:0.5b, smollm2:135m, az
mx-code-test:latest
Changing one line in an existing client. A script written against OpenAI, pointed at llamay by its base URL. The request, the response shape and the streaming format are the ones the client already expects. Recorded 2026-09-18 · Apple M4, macOS 26.0.1 · llamay v0.1.58-1-gb14f550 · metal:Apple M4 · 0:26

3 · Choose a model

Name one in model, or send "" for whatever the server was started with. A name the server does not have is a 404 listing what it does have — it never substitutes another. (auto, which picks a model from the question, is a name on the hosted service, not on a local server.)

llamay pull qwen2.5:7b          # by name, once
curl localhost:11435/v1/models  # what is available right now

Which size fits which machine is on getting models.

4 · Handle the refusals

This is the step that separates something demonstrable from something on call. Each refusal has a different correct response — retrying all of them is how a full queue becomes an outage.

StatusMeansDo
400The request is wrong: a body that is not JSON, a schema keyword that cannot be enforced, a flag the server was not started with.Fix it. Never retry.
401 · 403No key, the wrong key, or a caller not cleared for this.Fix the credentials. Do not retry.
404No such model, or no such context (they are closed after two hours unused).For a context, make another and send again.
409The context is busy with another request, or a snapshot is for other weights.Wait, or fork the context; for a snapshot, use the right model.
429The queue is full.Wait for Retry-After and retry, with backoff.
503No model is loaded yet, or the server is refusing work it cannot record.Retry after a pause; check the server.

Every refusal carries a message written to be read by a person. Showing it verbatim is almost always better than mapping it to one of your own. The full list is on errors. The hosted service adds its own — a 402 when a plan does not reach a model — which its messages explain.

And what the server answers, whichever dialect the thing you built speaks:

Transcript — the whole session as text
~/llamay $ llamay serve -m qwen2.5:0.5b -addr localhost:11439
time=2026-09-18T08:49:13.209-04:00 level=INFO msg="device graph attached" max-tokens=256
time=2026-09-18T08:49:13.210-04:00 level=INFO msg="llamay serving" addr=localhost:11439 mode
l=qwen2.5:0.5b arch=qwen2 max-loaded=2 keepalive=5m0s params="0.36 GB" context=32768 threads
=7 kv=f32 lexicon=0

  OpenAI      POST http://localhost:11439/v1/chat/completions
  Anthropic   POST http://localhost:11439/v1/messages
  Embeddings  POST http://localhost:11439/v1/embeddings     (start with -embed <encoder.gguf
>)
  Rerank      POST http://localhost:11439/rerank            (start with -rerank <reranker.gg
uf>)
  Contexts    POST http://localhost:11439/v1/contexts        (then /{id}/fork?n=8, /{id}/sna
pshot)
  Models      POST http://localhost:11439/api/pull          (and /api/copy, /api/delete)
  Stats       GET  http://localhost:11439/v1/stats


~/llamay $ # the server stays up; the rest of this is a second terminal
~/llamay $ # OpenAI
~/llamay $ curl -s http://localhost:11439/v1/chat/completions -H 'content-type: application/
json' \
>   -d '{"model":"qwen2.5:0.5b","messages":[{"role":"user","content":"Say hi."}]}' | jq -r '
.choices[0].message.content'
Hello! How can I assist you today?
~/llamay $ # Anthropic
~/llamay $ curl -s http://localhost:11439/v1/messages -H 'content-type: application/json' \
>   -d '{"model":"qwen2.5:0.5b","max_tokens":40,"messages":[{"role":"user","content":"Say hi
."}]}' | jq -r '.content[0].text'
Hello! How can I assist you today?
~/llamay $ # Ollama
~/llamay $ curl -s http://localhost:11439/api/chat -H 'content-type: application/json' \
>   -d '{"model":"qwen2.5:0.5b","stream":false,"messages":[{"role":"user","content":"Say hi.
"}]}' | jq -r '.message.content'
Hello! How can I assist you today?
~/llamay $ curl -s http://localhost:11439/api/tags | jq '.models[].name'
"all-minilm:latest"
"gemma2:2b"
"gemma3:270m"
"gpt2:q8_0"
"qwen2.5:0.5b"
"smollm2:135m"
"azmx-code-test:latest"
Three APIs, one server, one model. OpenAI, Anthropic and Ollama all answer on the same port against the same loaded weights. Nothing is translated at a proxy; the routes are native. Recorded 2026-09-18 · Apple M4, macOS 26.0.1 · llamay v0.1.58-1-gb14f550 · metal:Apple M4 · 0:27

5 · Deploy it

There is no runtime to install and no container required: a binary, a service, and a directory for models. The Linux packages install the service for you; deploying for a team walks through keys, TLS, the audit record and the unit override. For a machine you set up by hand:

# /etc/systemd/system/llamay.service
[Unit]
Description=llamay
After=network-online.target

[Service]
ExecStart=/usr/local/bin/llamay serve -addr :11435 -api-keys /etc/llamay/keys -pull=false
Environment=LLAMAY_MODELS=/var/lib/llamay/models
Restart=always
User=llamay
StateDirectory=llamay

[Install]
WantedBy=multi-user.target

Bind to a loopback address unless something else needs to reach it, and set keys whenever it listens on anything other than loopback. Serving covers the scheduler, batching and model residency; Backends covers what each kind of hardware will and will not run.

Where to go next