A minimal self-hosted server for the Atuin AI protocol, backed by any OpenAI-compatible chat-completions endpoint — Ollama, vLLM, LM Studio, llama.cpp, LiteLLM, OpenRouter, or a compatible cloud API. It runs the same engine the hosted Atuin AI service serves, composed with stateless defaults: no accounts, no database, no usage limits, no recording.
The Atuin CLI talks to it unchanged: point ai.endpoint at this server
and chat runs against your own models.
cp config.example.toml config.toml # then edit
docker run \
-v ./config.toml:/etc/atuin-ai/config.toml \
-p 8080:8080 \
ghcr.io/atuinsh/atuin-ai-server:latestOr build your own image:
docker build -t atuin-ai-server .To reach an engine running on the Docker host (e.g. local Ollama), use
host.docker.internal as the server's endpoint:
endpoint = "http://host.docker.internal:11434/v1"Requires Erlang/OTP, Elixir, and Gleam (versions in the repository's
.tool-versions file).
cp config.example.toml config.toml # then edit
mix deps.get
mix run --no-haltOne TOML file, path given by CHAT_CONFIG (default ./config.toml; the
Docker image defaults to /etc/atuin-ai/config.toml).
port = 8080
# The OpenAI-compatible chat-completions endpoint. Accepted with or
# without the trailing /chat/completions; a query string is passed
# through to every request.
endpoint = "http://localhost:11434/v1"
default_model = "llama" # optional; defaults to the first model
[[models]]
alias = "llama" # what CLI users select
name = "Llama 3.2" # display name in the model list
description = "Local llama3.2 via Ollama" # extra text in the model list
model = "llama3.2:latest" # sent as the request's `model`The model (and serving engine) must support tool calling; the chat protocol drives tools on every turn.
Local engines usually need none; with no api_key, no Authorization
header is sent. Otherwise, you can specify a key in three formats:
api_key = "sk-..." # inline
[api_key]
env = "CHAT_API_KEY" # from the environment
[api_key]
cmd = { run = "op", args = ["read", "op://vault/item/key"] }
# from a command's stdoutBy default the key is sent as Authorization: Bearer <key>.
[request.headers] replaces the default Authorization header entirely;
spell out every auth header you need. {{api_key}} expands wherever it
appears, so the secret stays in api_key while the headers control its
shape (e.g. Azure-style api-key auth):
[request.headers]
api-key = "{{api_key}}"[request.body] merges extra fields into every chat-completions request,
for engine-specific options. stream, model, messages, and tools
are owned by the server and rejected here. Most engines only report
token usage on streamed responses when asked, so the example config
ships:
[request.body]
stream_options = { include_usage = true }[web_tools] enables the server-side web tools. These are the same
providers the hosted service uses. Each configured key enables its tool:
brave_api_key enables web_search (Brave Search API),
firecrawl_api_key enables web_scrape (Firecrawl). Keys take the same
three forms as api_key. Scraping goes through Firecrawl rather than
fetching model-chosen URLs directly, so requests to arbitrary URLs
originate from Firecrawl's network, not yours.
[web_tools]
brave_api_key = { env = "BRAVE_API_KEY" }
firecrawl_api_key = { env = "FIRECRAWL_API_KEY" }Set the AUTH_TOKEN environment variable to require
Authorization: Bearer <token> on every request. Unset means open
access. Set api_token in Atuin AI's config section to match.
# ~/.config/atuin/config.toml
[ai]
endpoint = "http://localhost:8080"
# api_token = "..." # must match AUTH_TOKEN when setPOST /api/cli/chat— the streaming chat endpointGET /api/cli/models— the configured model list
- Chat-completions APIs only. Models that are exclusively served via
OpenAI's newer
/v1/responsesprotocol are not supported. - Model failures surface in the stream: a misconfigured model name or unreachable endpoint fails the request immediately with the upstream's status and error message in the Atuin AI TUI.
Apache-2.0; see LICENSE. The engine lives in atuin-ai-core.