A dependency-free (python3 stdlib + systemd + ssh) service that:
- Owns one HTTP port (default
:15108) on the OpenClaw-gateway host and acts as a transparent streaming proxy in front of a remotellama.cppserver (llama-server) running on a GPU box — so your OpenClaw gateway just points athttp://127.0.0.1:15108/v1like any OpenAI-compatible endpoint. - Serves a power panel at
/: buttons to switch the entire SOC stack ON/OFF (llama.cpp server on the GPU host + the OpenClaw SOC gateway), with live status of every component. - Exposes /status (JSON) with live metrics — GPU power draw / utilization / VRAM, host CPU / RAM / disk, and llama.cpp token counters — consumed by the page and by an external watchdog (included).
Built for a two-host split like:
SOC console / operators
│
▼
openclaw-gateway-soc (:18790, LAN)
│ POST /v1/chat/completions (openai-completions provider)
▼
soc-control (this repo) http://<gateway-host>:15108
│ / → power panel
│ /power/on|off → systemctl + ssh actions
│ /v1/*, /health, /metrics → proxied
▼
GPU host / GPU host: llama-server :21402 (gemma-4-31B, 131k ctx, tool calls ✓)
Everything is stdlib-only — no pip installs.
| File | Installs on | Purpose |
|---|---|---|
server.py |
control/gateway host | HTTP control panel + streaming proxy to llama.cpp |
watchdog.sh |
control/gateway host | health checks; prints ALERT / UNCHANGED |
systemd/soc-control.service |
control/gateway host | runs server.py, restart=always |
systemd/soc-llama-31b.service |
GPU host | runs llama-server for the SOC model |
install.sh |
both | role-based installer (control / gpu) |
docs/WORKLOG.md |
— | field log from a real migration (minimax-m3 → llama.cpp), incl. every gotcha |
Discrete mathematics with Laya: examples/ implements the laya × deterministic-engine
pattern — the decision model routes, tags, and gates; examples/dm_core.py computes every
answer exactly (stdlib only). Seven runnable tutorials, a measurable capability probe, and a
fine-tune dataset generator live there; the full study write-up is docs/LAYA-DISCRETE-MATH.md.
-
GPU host: NVIDIA GPUs + CUDA build of llama.cpp (
~/llama.cpp/build/bin/llama-server), a GGUF model that supports tool calling (verified before wiring — see Gotchas),nvidia-smi, and SSH key auth from the gateway host (ssh <gpuhost> 'true'must be passwordless). -
Gateway/control host: OpenClaw gateway running the SOC agents, python3 ≥ 3.10,
ss,systemd --userwith lingering enabled (sudo loginctl enable-linger $USER) on both hosts so everything survives reboots. -
Firewall: open the control port only to your internal/Tailscale ranges, e.g.
sudo ufw allow from 10.0.0.0/8 to any port 15108 sudo ufw allow from 100.64.0.0/10 to any port 15108 sudo ufw allow from 192.168.0.0/16 to any port 15108
All knobs live at the top of server.py / in install.sh:
| Knob | Default | Meaning |
|---|---|---|
UP_HOST / UP_PORT |
100.64.0.61 / 21402 |
GPU host + llama-server port |
SOC_CONTROL_PORT |
15108 |
the public panel/proxy port |
STATE |
~/.openclaw-soc/soc-power |
ON/OFF state file (also read by watchdog) |
| model id | gemma-4-31b-it-q3km |
must match the --alias your llama-server uses |
| watchdog paths | /home/operator/.openclaw-soc/… |
state + power files |
sudo loginctl enable-linger "$USER" # keep user services alive after logout
cp systemd/soc-llama-31b.service ~/.config/systemd/user/
# edit ExecStart -m path / --alias / --ctx-size to match your GGUF + hardware
systemctl --user daemon-reload
systemctl --user enable --now soc-llama-31b.service
curl -fsS localhost:21402/health && echo GPU-READY./install.sh control
curl -fsS localhost:15108/statusinstall.sh control installs soc-control.service + watchdog.sh. If you previously
used a raw socat bridge on the same port, disable it first — the control service owns
the port now:
systemctl --user disable --now soc-llm-bridge.service 2>/dev/null || trueIn the SOC gateway's openclaw.json (OPENCLAW_STATE_DIR instance):
"models": {
"providers": {
"GPU host": {
"baseUrl": "http://127.0.0.1:15108/v1",
"apiKey": "anything-nonempty",
"api": "openai-completions",
"models": [{
"id": "gemma-4-31b-it-q3km",
"name": "gemma-4-31b-it-q3km",
"input": ["text"],
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"contextWindow": 131072,
"maxTokens": 8192,
"reasoning": true,
"api": "openai-completions"
}]
}
}
},
"agents": { "defaults": { "model": { "primary": "GPU host/gemma-4-31b-it-q3km" } } }…and set each agent's "model" to GPU host/gemma-4-31b-it-q3km as well (agents with no
model field inherit the default). The gateway hot-reloads config — no restart needed
while it runs; if it's stopped, it picks the file up on next start.
"reasoning": trueis correct for thinking models (gemma-4-31B emitsreasoning_contentbefore the answer). BudgetmaxTokensfor reasoning + content.
Open http://<gateway-host>:15108/:
- ⏻ TURN ON — starts the llama.cpp unit on the GPU host over SSH, waits until
/healthreturns 200 (≤2 min cold load), then starts the OpenClaw SOC gateway. - ⏼ TURN OFF — stops the SOC gateway first, then the llama.cpp unit. Running turns are cancelled; consoles go down with it. The panel itself stays up (it is the switch).
/status— JSON:power,llama_health,gateway,port_18790,GPU host{gpus:[{power_w,util_pct,vram_used_g,vram_total_g}], cpu_pct, ram, disk},tokens{llamacpp:prompt_tokens_total, tokens_predicted_total, …},control host{cpu_pct, mem, disk}.- Anything else (
/v1/*,/health,/metrics, llama web UI) is proxied to the GPU host — SSE/streaming passes through (900 s upstream timeout, chunk-flushed).
watchdog.sh checks: control port /v1/models + /health, the llama unit state on the
GPU host (tolerates activating), the SOC gateway service, port 18790, and fresh
status=5xx lines in the SOC gateway journal (90 s window). It writes state to
~/.openclaw-soc/watchdog-state and prints exactly:
UNCHANGED— steady (or intentionally powered off)ALERT: SOC LLM … -> working|BROKEN…— a transition
Run it from OpenClaw's scheduler so transitions reach you in chat:
openclaw cron add --name soc-llm-watchdog --every 10m \
--session isolated --session-key "<your-main-session-key>" \
--no-deliver --timeout-seconds 150 \
--message 'Run: bash /home/operator/.openclaw-soc/watchdog.sh
If the output is exactly UNCHANGED, reply with only: NO_REPLY
Otherwise reply with the output plus one line naming the broken/recovered check.'(The session key must be a session whose channel you actually read — e.g. your main
webchat/TUI session. --no-deliver lets the reply route through the session itself;
NO_REPLY is stripped, so it's silent while everything is healthy.)
Intentional power-downs are silent: the watchdog reads the same
~/.openclaw-soc/soc-power file the panel writes, so clicking TURN OFF never pages you.
See docs/WORKLOG.md for the full story. Short version:
- Tool calling is a model property, not a server property. A llama3-8B finetune
returned
500 … does not match the expected peg-native formaton anytools=[…]request; gemma-4-31B passed. Verify with a live call —/v1/modelscapabilities:["completion"]is not authoritative (it lied while tool calls worked). - Thinking models (
reasoning_content) eatmax_tokensbefore anycontent— tiny budgets look like "empty replies". - Never health-check by completing a chat from an external watchdog: with a single
llama.cpp slot it queues behind real turns (minutes at 75k-token contexts) and its
timeout cancels the real turn. Use
/health+/metrics+ journal scan instead. - llama.cpp returns 503 while loading; prompt cache (LCP/LRU slot reuse) makes later turns ~16 s instead of minutes.
- Metric names are
llamacpp:*(colons), e.g.llamacpp:prompt_tokens_total.
- llama.cpp binds
0.0.0.0but the panel+proxy is the only published path; the control service should be firewall-restricted to internal ranges (see above). The API is unauthenticated by design — do not open 15108 to "Anywhere". - The panel's ON/OFF performs systemctl/ssh actions as your user — keep the port off the public internet.
Note on anonymization: internal hostnames are referred to by role (control host / edge host / GPU host), real IP addresses have been replaced with documentation ranges (RFC 5737 TEST-NET, RFC 6598 CGNAT, last octet preserved), and alert-sample IPs/domains are anonymized. Nothing here needs credentials.