Managed
Atomic Agent downloads the llama.cpp binary and a GGUF model, picks a GPU, starts the daemon, and watches its health. One command and you’re running. Set localModels.mode: "managed".
Atomic Agent runs language models on your own machine by default. There are no per-token fees, your prompts never leave your computer, and you can swap models whenever you like. Under the hood it talks to llama.cpp’s llama-server over a small HTTP API.
This page explains the two ways to get a local model running — managed (Atomic Agent downloads and runs llama.cpp for you) and external (you point it at a llama-server you run yourself) — plus GPU acceleration, device selection, and the other surfaces that share the same model. If you’d rather call a hosted model, see cloud providers at the end.
Managed
Atomic Agent downloads the llama.cpp binary and a GGUF model, picks a GPU, starts the daemon, and watches its health. One command and you’re running. Set localModels.mode: "managed".
External
You already run llama-server (or any OpenAI-compatible llama.cpp build) somewhere. Atomic Agent just connects to its URL. Set localModels.mode: "external".
The mode lives in your config file at <stateDir>/config.json:
{ "localModels": { "mode": "managed", // or "external" "url": "http://127.0.0.1:8080", "managed": { "modelId": "qwen-3.5-4b", "port": 19091 } }}In managed mode, Atomic Agent owns the full lifecycle of llama.cpp. The atomic-agent models commands drive everything.
# See which models are available and what's installedatomic-agent models list
# Download a model's GGUF weights (idempotent — safe to re-run)atomic-agent models pull qwen-3.5-4b
# Make it the active modelatomic-agent models use qwen-3.5-4b
# Start the daemon (downloads the llama.cpp backend if missing)atomic-agent models start
# Check it's aliveatomic-agent models statusThe default model for a fresh install is qwen-3.5-4b. The catalog spans the Qwen 3.5, 3.6, and 3.8 generations plus the Gemma 4 family — run atomic-agent models list for the current set rather than trusting a count here, since the catalog moves between releases.
Every chat model in the catalog is vision-capable, paired with an mmproj projector. There’s no separate “vision models” shortlist to pick from: whichever model you run can look at images, and the only cost is the extra memory the projector needs.
models start actually doesWhen you start a managed daemon, Atomic Agent walks through a fixed sequence:
llama-server detached, writes a PID file, and polls http://localhost:<port>/health until it returns ok (30s timeout).llama-server on a separate port for hybrid memory recall.flowchart TD
Start["atomic-agent models start"] --> Verify["Verify backend + GGUF<br/>(download if missing)"]
Verify --> GPU["Resolve GPU device<br/>(Vulkan / nvidia-smi)"]
GPU --> Spawn["Spawn llama-server detached<br/>write PID file"]
Spawn --> Health["Poll /health<br/>until ok or 30s timeout"]
Health -->|ok| ChatUp["Chat daemon up<br/>(primary)"]
Health -->|timeout| Fail["Fail + print log tail"]
ChatUp --> Embed{"Embeddings<br/>enabled?"}
Embed -->|yes| EmbedDaemon["Spawn embedding daemon<br/>on separate port"]
Embed -->|no| Done["Ready"]
EmbedDaemon -->|ok| Done
EmbedDaemon -->|fail| Degrade["Chat continues<br/>FTS5-only memory fallback"]
Degrade --> Done
atomic-agent models stop # SIGTERM, wait 3s, then SIGKILL; remove PID fileatomic-agent models update # update the llama.cpp backend binaryatomic-agent models remove <id> # delete a model's weights from diskatomic-agent models devices # list detected GPU devicesatomic-agent models use-device auto # or: cpu | Vulkan0 | Vulkan1 ...Embedding models have their own subcommands:
atomic-agent models list-embeddingsatomic-agent models pull-embedding nomic-embed-text-v1.5atomic-agent models use-embedding nomic-embed-text-v1.5atomic-agent models use-embedding --disableThe default embedding model is nomic-embed-text-v1.5. The catalog carries five: one Nomic, three BGE (bge-small-en-v1.5, bge-base-en-v1.5, and the multilingual bge-m3), and one Jina long-document model. Reach for bge-m3 if your notes aren’t all in English.
If you already run llama-server (for example with a custom build, a shared GPU box, or a model Atomic Agent doesn’t bundle), use external mode and just point at it.
{ "localModels": { "mode": "external", "url": "http://localhost:8080" }}Atomic Agent will probe that URL’s /health and /props endpoints at startup to detect the model profile and plan KV-cache slots. Everything else — tool grammar, hot-swap detection, prompting — works the same as managed mode.
Atomic Agent uses Vulkan for GPU acceleration in managed mode, which covers all major vendors (NVIDIA, AMD, Intel) through a single path. It enumerates devices by running llama-server --list-devices and parsing the Vulkan table, with an nvidia-smi fallback for pre-download VRAM checks.
atomic-agent models devicesauto (default) — Atomic Agent picks the best GPU by VRAM, preferring a discrete GPU over an integrated one even if the integrated GPU reports more memory.Vulkan0, Vulkan1.cpu — skip the GPU entirely.atomic-agent models use-device autoatomic-agent models use-device Vulkan0atomic-agent models use-device cpuThe setting persists as localModels.managed.device in config.
Atomic Agent computes a VRAM budget before loading:
localModels.managed.contextSize defaults to 0, which means auto: Atomic Agent measures the free VRAM it has after the weights load and fits --ctx-size to what’s actually left. On a busy GPU you get a smaller window instead of a failed load. Set a positive integer to pin the context yourself when you’d rather have a predictable window than a guaranteed start.
localModels.managed.stopOnExit defaults to true: when the last CLI session exits, the managed daemon shuts down with it rather than lingering in the background holding VRAM. Set it to false if you want the daemon to survive across invocations — worth it when you’re running many short commands and don’t want to pay the model load each time.
Whatever model you’re running — managed or external — is shared across every way you talk to Atomic Agent. They all connect to the same llama-server.
Run an OpenAI-compatible HTTP server and point any OpenAI SDK at it:
atomic-agent serve --host 127.0.0.1 --port 8787 --api-key <key>Then call the chat completions endpoint:
curl http://127.0.0.1:8787/v1/chat/completions \ -H "Authorization: Bearer <key>" \ -H "Content-Type: application/json" \ -d '{ "model": "atomic-agent", "messages": [{ "role": "user", "content": "List the files here." }], "stream": false }'Notes:
POST /v1/chat/completions supports both streaming (stream: true, SSE) and synchronous responses.GET /v1/models returns a single atomic-agent entry and needs no auth.GET /health is a no-auth liveness probe (also checks the underlying llama-server).X-Atomic-Session-Id header.ATOMIC_AGENT_API_KEY environment variable.For desktop apps, Atomic Agent ships a sidecar process that speaks newline-delimited JSON (NDJSON) over stdin/stdout. A Tauri (or any) host spawns it, pipes the streams, sends typed requests, and reads back streamed events.
atomic-agent-sidecarstart_session, send_message, cancel, approval_response, get_session, skill_*, ping, shutdown.step_started, tool_call_started, assistant_delta, assistant_reply, approval_request, and more.localModels.url / localModels.managed.port config as every other front end.The sidecar uses the same managed/external model under the hood — it’s just another front end for the runtime.
You can drive the same agent — and the same local model — from your phone via a single-user Telegram channel.
{ "telegram": { "enabled": true }}Put your bot token in <stateDir>/.env as TELEGRAM_BOT_TOKEN (never in config.json — the token lives in the secrets file, mode 0600). Then pair as the owner and chat. Dangerous tool calls arrive as inline approve/deny buttons.
/help show commands and setup/status active session, turn count, last error/new start a fresh session/cancel abort the running turnTelegram is intentionally single-user: once paired, only the owner’s messages are accepted; everyone else is dropped.
| Key | Meaning |
|---|---|
localModels.mode | managed (Atomic Agent runs llama.cpp) or external (you do) |
localModels.url | HTTP URL of the chat server |
localModels.managed.modelId | Active model in managed mode (e.g. qwen-3.5-4b) |
localModels.managed.port | Chat daemon port (default 19091) |
localModels.managed.device | auto, cpu, or a device id like Vulkan0 |
localModels.managed.contextSize | --ctx-size; 0 (default) fits it to free VRAM |
localModels.managed.stopOnExit | Stop the daemon when the last CLI session exits (default true) |
localModels.managed.autoUpdate | Auto-update the backend binary |
localModels.completionMaxTokens | Max new tokens per completion (default 8192) |
localModels.embeddings.enabled | Run the secondary embedding daemon |
localModels.embeddings.modelId | Embedding model (e.g. nomic-embed-text-v1.5) |
localModels.embeddings.port | Embedding daemon port (default 19092) |
Useful environment variables:
| Variable | Effect |
|---|---|
ATOMIC_AGENT_LLAMA_API_KEY | Bearer token for the llama-server (optional) |
ATOMIC_AGENT_LLAMA_MAX_TOKENS | Max new tokens per completion — sets localModels.completionMaxTokens (default 8192, clamped 64–131072) |
ATOMIC_AGENT_LLAMA_TEMPERATURE / _TOP_P / _TOP_K / _SEED | Sampling overrides (parsed at startup) |
ATOMIC_AGENT_STATE_DIR | Root state directory holding config.json, .env, and model files |
Local llama.cpp is the default and the point of the product, but it isn’t the only option. Atomic Agent ships six provider kinds, and you can mix them:
| Kind | What it talks to |
|---|---|
llama-server | Local llama.cpp — the default |
openai-compatible | Any OpenAI-shaped endpoint (vLLM, LM Studio, a hosted API) |
qwen-openai-compatible | The same, with Qwen’s tagged tool-call dialect |
openrouter | OpenRouter’s model marketplace |
aimlapi | AIMLAPI |
gemini | Google Gemini |
You rarely need to type a base URL. The TUI’s provider wizard ships eleven pre-verified presets — pick a vendor by name and it fills in the endpoint for you. Every one resolves to the ordinary openai-compatible kind, so a preset is a shortcut, not a seventh provider kind, and model lists still come from the server’s own /v1/models rather than a bundled list that goes stale.
| Preset | Base URL | API-key env var |
|---|---|---|
| Cerebras | https://api.cerebras.ai | CEREBRAS_API_KEY |
| DeepSeek | https://api.deepseek.com | DEEPSEEK_API_KEY |
| Fireworks AI | https://api.fireworks.ai/inference | FIREWORKS_API_KEY |
| Groq | https://api.groq.com/openai | GROQ_API_KEY |
| LM Studio (local) | http://localhost:1234 | LMSTUDIO_API_KEY |
| Mistral | https://api.mistral.ai | MISTRAL_API_KEY |
| Nous Research | https://inference-api.nousresearch.com | NOUS_API_KEY |
| Ollama (local) | http://localhost:11434 | OLLAMA_API_KEY |
| Ollama Cloud | https://ollama.com | OLLAMA_CLOUD_API_KEY |
| Together AI | https://api.together.xyz | TOGETHER_API_KEY |
| xAI (Grok) | https://api.x.ai | XAI_API_KEY |
Each preset has its own env var, deliberately. An earlier shared OPENAI_COMPAT_API_KEY meant connecting a second vendor overwrote the first one’s key. Now Groq and Together can hold keys at the same time without touching each other, and adding a second key for the same vendor gets a numbered entry id (groq-2) that still resolves to the right service.
Two behaviours worth knowing:
Authorization header at all./v1/models unauthenticated, so you can browse the catalog first and add credentials later. The rest want a key before they’ll tell you anything.Providers are declared in an llm.providers[] block, with activeTextProvider choosing which one handles a turn:
{ "llm": { "activeTextProvider": "local", "providers": [ { "id": "local", "kind": "llama-server" }, { "id": "or", "kind": "openrouter", "apiKey": "sk-or-...", "defaultChatModel": "qwen/qwen3-max" } ] }}That example is the short form. A provider entry carries considerably more than id / kind / apiKey:
| Field | Meaning |
|---|---|
id | Name you refer to in activeTextProvider, activeEmbeddingProvider, and llm.fallback.chain |
kind | One of the six kinds above |
baseUrl | API root for the OpenAI-shaped kinds. Stored without the /v1 suffix — call sites append it |
url | llama-server base URL override (llama-server kind only) |
apiKey | Credential. Empty string when the endpoint is local and unauthenticated |
defaultChatModel | Model used when a turn doesn’t name one |
defaultEmbeddingModel | Model used for embedding requests |
headers | Extra HTTP headers sent verbatim on every request |
supportsTools | Whether the endpoint handles parallel tool calls (defaults to true) |
supportsVision | Whether the endpoint accepts images (defaults to true) |
requestTimeoutMs | Per-request timeout |
promptCache | auto, off, or explicit-markers |
providerPreferences | Free-form object passed through to the provider (OpenRouter routing preferences and similar) |
userModels[] | Per-model capability and pricing declarations — see below |
Each provider entry can carry a userModels[] array describing individual models. Every entry needs an id and a kind (chat or embedding); the rest is optional:
{ "id": "vllm", "kind": "openai-compatible", "baseUrl": "https://vllm.internal.example", "defaultChatModel": "qwen/qwen3-32b", "userModels": [ { "id": "qwen/qwen3-32b", "kind": "chat", "contextWindow": 32768, "supportsVision": false, "supportsTools": "basic", "supportsPromptCache": false, "pricing": { "input": 0.15, "output": 0.6 } } ]}supportsTools is a four-way setting — none, basic, parallel, or strict — not a boolean, and embedding entries can also declare dim. Pricing takes input and output, plus optional cacheRead and cacheWrite.
Resolution runs in a fixed order: userModels beats the built-in catalog, which beats the defaults. That last step is the one to watch. A model nobody declared and the catalog doesn’t recognise silently resolves to a 128,000-token context window with parallel tool support and no vision — plausible-looking numbers that are simply wrong if your endpoint serves an 8K window or can’t do parallel calls. Nothing warns you; you find out when requests start getting truncated or tool calls come back malformed.
userModels[] is also the only way to make cost tracking price a model the catalog doesn’t know. That’s the fix for the $0 readout described just below — declare pricing here and the numbers become real.
llm.toolTransport sits next to providers[] and decides how the model is asked for a tool call. It takes three values, and the default is easy to misread:
| Value | Behaviour |
|---|---|
auto (default) | Defers to whatever the active provider declares. Not a synonym for grammar |
grammar | Force GBNF-constrained decoding, whatever the provider says |
native_tools | Force the provider’s own function-calling API |
Because auto follows the provider, the effective transport changes when you switch providers — and it changes again mid-chain when a fallback hands the turn to someone else. A local llama-server turn constrained by grammar and a hosted turn using native function calling are both auto. If you’re debugging tool-call behaviour that appears only on one provider, pin the transport explicitly before you go hunting anywhere else.
Two things come with the cloud path.
Cost tracking accumulates spend per turn once you set llm.costTracking.enabled: true, so a remote provider’s bill is visible rather than a surprise. It prices a turn from the model’s published rates; for a model the built-in catalog doesn’t carry, declare the rates yourself in that provider’s userModels[].
A fallback chain lets a request roll to the next provider when the active one errors — useful for riding out a hosted outage without dropping back to a cold local load, though it also means a failure can quietly move your prompt to a different vendor. Put only providers you’re equally comfortable with in one chain.
Note that llm.fallback.appendLocal defaults to true: your local llama-server is silently appended as the last link, so a cloud outage can hand the turn to a local model whose answers differ sharply in quality. Set it to false if you would rather the request fail than switch. The chain also behaves as a circuit breaker — after failureThreshold consecutive failures (default 3) the provider is skipped for a cooldown that escalates 30s, 60s, 300s before the primary is probed again.
atomic-agent models start polls /health for 30 seconds; on timeout it prints the tail of the llama-server log. Re-run atomic-agent models status and check the log for load errors.sudo) and you try to stop it as yourself, Atomic Agent raises a ForeignDaemonError rather than orphaning the process. Stop it as the original user.GITHUB_TOKEN for heavy use or CI. GH_TOKEN works everywhere GITHUB_TOKEN does — model downloads, llama.cpp backend installs, and the app update check all read GITHUB_TOKEN first and fall back to GH_TOKEN, so the token the gh CLI already put in your environment is enough.CLI reference
Full Atomic Agent models, run, and serve command surface.
Configuration
Every config.json block and ATOMIC_AGENT_* environment variable.
Memory
How the embedding daemon powers hybrid recall over your notes.
HTTP API
OpenAI-compatible endpoints, headers, and streaming details.