LLM — The Brain
The agent runtime (OpenClaw by default) sends every conversation turn to the
LLM configured in /root/config/config.json. Out of the box that is the
Autonomous AI proxy, which serves a catalog of models under one key. You can
switch models on the proxy, or bring your own provider.
The keys
| Key | Default | What it does |
|---|---|---|
llm_base_url | Autonomous proxy | Endpoint the runtime calls. Trailing / is stripped on save. |
llm_api_key | Shipped proxy key | Sent to the endpoint. Also accepted as an admin Bearer token for the device API. |
llm_model | Picked from the catalog at setup | Model id the runtime uses as its primary model. |
llm_disable_thinking | false | OpenClaw only: turns off extended reasoning on every model entry (reasoning: false). Faster, cheaper replies. |
backend_base_url, backend_api_key | Empty (reuse llm_*) | Where the device sends its status heartbeat and alerts. Set these when the brain no longer points at Autonomous — see below. |
claude_code_oauth_token | — | Claude Code runtime only: use a claude.ai subscription instead of an API key. |
There is no key for temperature, max tokens, a fallback model, or the vision model. Context window and max output tokens come from the model catalog (OpenClaw falls back to 200 000 / 8 192).
Change the model (on the Autonomous proxy)
The quickest customisation — no new key needed.
In the AI Brain tab (http://<device-ip>/setting?debug=true#llm) the
Autonomous mode shows the model read-only, so use the API
(see Overview for $TOKEN):
curl -s -X PUT $DEVICE/api/device/config \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"llm_model":"<model-id>"}'
A model-only change is applied live — the runtime's primary model is
swapped without restarting anything. The OpenClaw runtime lists available ids
in /root/.openclaw/openclaw.json under models.providers.autonomous.models.
To turn off extended thinking (there's no UI control for this):
curl -s -X PUT $DEVICE/api/device/config \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"llm_disable_thinking":true}'
Bring your own provider
Custom LLM endpoints are implemented and unit-tested but not yet verified on a device against every provider. Test with your provider before relying on it, and keep Restore defaults handy.
What the endpoint must speak
Each agent runtime talks a specific wire format. The endpoint in
llm_base_url must match the runtime you use
(Agent Runtime):
| Runtime | Wire format your endpoint must serve | Notes |
|---|---|---|
| OpenClaw (default) | OpenAI Chat Completions + GET /models | On a non-Autonomous host, OpenClaw fetches {llm_base_url}/models (OpenAI {"data":[{"id":…}]} shape), registers every model it finds, and re-syncs the list every 30 min. |
| Hermes, PicoClaw | Anthropic Messages | Format is hard-coded; an OpenAI-only endpoint won't work. |
| Claude Code | Anthropic Messages | A trailing /v1 is stripped from the base URL. |
| Codex, OpenCode | OpenAI Responses (/responses) |
So, for example:
| Provider | OpenClaw | Hermes / PicoClaw / Claude Code | Codex / OpenCode |
|---|---|---|---|
| OpenAI | https://api.openai.com/v1 | — | https://api.openai.com/v1 |
| Anthropic | Only through an OpenAI-compatible gateway | https://api.anthropic.com | — |
| OpenRouter | https://openrouter.ai/api/v1 | Anthropic-compatible OpenRouter endpoint | Check that your model supports /responses |
| Ollama / vLLM / LM Studio | http://<host>:<port>/v1 | Only if the server speaks Anthropic Messages | Only if it serves /responses |
If the local server doesn't check auth, any placeholder works for
llm_api_key. But remember that value also becomes an admin credential for
the device API, so don't use something guessable on a shared network.
What else follows llm_base_url
llm_base_url / llm_api_key aren't only the brain. Several other services
use them as their fallback — or, for perception, use them unconditionally.
When you point the brain elsewhere, each of these goes with it unless it has
its own setting:
| Service | Uses | Own setting to keep it on Autonomous |
|---|---|---|
| TTS | tts_base_url / tts_api_key, else llm_* | tts_base_url, tts_api_key |
| STT (Autonomous STT) | stt_base_url / stt_api_key, else llm_* | stt_base_url, stt_api_key (or a deepgram_api_key, English only) |
| Realtime voice | realtime.base_url / realtime.api_key, else <llm_base_url>/ws/gemini or /ws/openai with llm_api_key | realtime.base_url (full URL, including /ws/gemini or /ws/openai), realtime.api_key |
| Status heartbeat, alerts, MQTT endpoint updates | backend_base_url / backend_api_key, else llm_* | backend_base_url, backend_api_key |
| Perception — speaker recognition (My Voice), speech emotion, sound events | always llm_base_url / llm_api_key | None — these stop working with a custom brain |
| Vision describe (image-describing skills) | always {llm_base_url}/v1/messages, Anthropic format, fixed model | None — only works if your endpoint is Anthropic-compatible |
Your provider won't serve the Autonomous voice paths (/ws/audio/transcriptions,
/elevenlabs/…, /ws/gemini), so without the settings above voice stops
working as soon as you switch.
Step 1 — keep voice and status on Autonomous
Do this before changing the brain, while llm_base_url / llm_api_key
still hold the shipped Autonomous values. On the device (SSH or Web CLI), this
copies them into every voice and status key that is still empty:
sudo cp /root/config/config.json /root/config/config.json.bak
sudo jq '
def pin(k; v): if ((.[k] // "") == "") then .[k] = v else . end;
.llm_base_url as $u | .llm_api_key as $k
| pin("tts_base_url"; $u) | pin("tts_api_key"; $k)
| pin("stt_base_url"; $u) | pin("stt_api_key"; $k)
| pin("backend_base_url"; $u) | pin("backend_api_key"; $k)
| if (.realtime.provider // "") as $p | ($p == "gemini" or $p == "openai" or $p == "gptlive")
and ((.realtime.api_key // "") == "")
then .realtime.api_key = $k
| .realtime.base_url = ($u + (if .realtime.provider == "gemini" then "/ws/gemini" else "/ws/openai" end))
| .realtime.pinned = true
else . end
' /root/config/config.json.bak | sudo tee /root/config/config.json >/dev/null
sudo jq '{tts_base_url, stt_base_url, backend_base_url, realtime: .realtime.base_url}' /root/config/config.json
sudo systemctl restart os-server
If you already switched the brain, restore defaults first
(POST /api/device/restore-defaults {"section":"llm"}), then run this.
pinned: true stops os-server from resetting the realtime block to its
defaults on the next start. The realtime base_url includes the provider
path, so if you later change the realtime provider between Gemini and
OpenAI/GPT-Live, update realtime.base_url too.
Step 2 — switch the brain
curl -s -X PUT $DEVICE/api/device/config \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{
"llm_base_url": "https://openrouter.ai/api/v1",
"llm_api_key": "sk-or-…",
"llm_model": "<provider-model-id>"
}'
Or in the UI: AI Brain → Custom — bring your own → fill in base URL,
key and model → Save. Use the API or UI rather than editing config.json:
the first change through them snapshots the shipped values into
autonomous_defaults, which is what Restore defaults uses later.
What happens:
- The shipped values are saved to
autonomous_defaults(first change only). - The runtime config is regenerated (for OpenClaw,
openclaw.json→models.providers.autonomous) and the gateway restarts. - HAL restarts, because it reads the LLM credentials at start.
On OpenClaw, if you don't send llm_model, the first model returned by your
/models becomes the primary. Send a second PUT with llm_model to pick a
different one.
Check that voice still works: say something to the device and watch
sudo journalctl -u hal -f.
Self-hosted example (OpenClaw + Ollama)
After Step 1:
curl -s -X PUT $DEVICE/api/device/config \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"llm_base_url":"http://192.168.1.50:11434/v1","llm_api_key":"a-long-random-string","llm_model":"qwen2.5:14b"}'
Make sure the Ollama host is reachable from the device
(curl http://192.168.1.50:11434/v1/models over SSH) and the model supports
tool calling — the agent relies on tools.
Advanced: extra providers in OpenClaw
Everything in config.json lands in one OpenClaw provider called
autonomous. You can hand-add another provider to
/root/.openclaw/openclaw.json under models.providers and set
agents.defaults.model.primary to <provider>/<model>. OpenClaw keeps a
non-autonomous primary on later refreshes. Restart the gateway with
sudo systemctl restart openclaw.
Go back to Autonomous
curl -s -X POST $DEVICE/api/device/restore-defaults \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"section":"llm"}'
Or AI Brain → Autonomous (included).