Skip to main content

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​

KeyDefaultWhat it does
llm_base_urlAutonomous proxyEndpoint the runtime calls. Trailing / is stripped on save.
llm_api_keyShipped proxy keySent to the endpoint. Also accepted as an admin Bearer token for the device API.
llm_modelPicked from the catalog at setupModel id the runtime uses as its primary model.
llm_disable_thinkingfalseOpenClaw only: turns off extended reasoning on every model entry (reasoning: false). Faster, cheaper replies.
backend_base_url, backend_api_keyEmpty (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​

Experimental

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):

RuntimeWire format your endpoint must serveNotes
OpenClaw (default)OpenAI Chat Completions + GET /modelsOn 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, PicoClawAnthropic MessagesFormat is hard-coded; an OpenAI-only endpoint won't work.
Claude CodeAnthropic MessagesA trailing /v1 is stripped from the base URL.
Codex, OpenCodeOpenAI Responses (/responses)

So, for example:

ProviderOpenClawHermes / PicoClaw / Claude CodeCodex / OpenCode
OpenAIhttps://api.openai.com/v1—https://api.openai.com/v1
AnthropicOnly through an OpenAI-compatible gatewayhttps://api.anthropic.com—
OpenRouterhttps://openrouter.ai/api/v1Anthropic-compatible OpenRouter endpointCheck that your model supports /responses
Ollama / vLLM / LM Studiohttp://<host>:<port>/v1Only if the server speaks Anthropic MessagesOnly 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:

ServiceUsesOwn setting to keep it on Autonomous
TTStts_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 voicerealtime.base_url / realtime.api_key, else <llm_base_url>/ws/gemini or /ws/openai with llm_api_keyrealtime.base_url (full URL, including /ws/gemini or /ws/openai), realtime.api_key
Status heartbeat, alerts, MQTT endpoint updatesbackend_base_url / backend_api_key, else llm_*backend_base_url, backend_api_key
Perception — speaker recognition (My Voice), speech emotion, sound eventsalways llm_base_url / llm_api_keyNone — these stop working with a custom brain
Vision describe (image-describing skills)always {llm_base_url}/v1/messages, Anthropic format, fixed modelNone — 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:

  1. The shipped values are saved to autonomous_defaults (first change only).
  2. The runtime config is regenerated (for OpenClaw, openclaw.json → models.providers.autonomous) and the gateway restarts.
  3. 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).