Self-hosted models
Takuto Core can drive a self-hosted, OpenAI-compatible model server — LM Studio, Ollama, vLLM, and similar — through the OpenCode provider. This keeps source code and prompts entirely on infrastructure you control. This page covers the setup and the failure modes we hit wiring up LM Studio with Docker Desktop, so you don’t have to re-derive them.
Basic setup
-
Set the provider to OpenCode in Configuration → AI Settings.
-
Set the Model field. OpenCode’s
-mflag expects<providerId>/<modelId>, and Takuto Core‘s generated config always names the providerself_hosted, so the value must look like:self_hosted/<model-id-as-reported-by-the-server>Model id reported by the server Set in Takuto qwen/qwen2.5-vl-7bself_hosted/qwen/qwen2.5-vl-7blmstudio-community/Llama-3.1-8B-Instruct-GGUFself_hosted/lmstudio-community/Llama-3.1-8B-Instruct-GGUF -
Set the Base URL to the OpenAI-compatible endpoint, including the trailing
/v1:http://host.docker.internal:1234/v1 -
If your server needs no real API key (LM Studio, Ollama usually don’t), tick Allow shared default token so Takuto Core injects a placeholder bearer.
-
Set Context limit / Output limit to match your model — local endpoints can’t report these, so OpenCode relies on the values you provide.
The model server side
Make the server reachable from containers:
- LM Studio: turn on Developer → Local Server → “Serve on Local Network”.
Without it LM Studio binds only to
127.0.0.1and no container can reach it. - macOS firewall: either off, or with the model server explicitly allowed.
Confirm it’s bound to all interfaces, not just loopback:
lsof -nP -iTCP -sTCP:LISTEN | grep 1234
# Want: *:1234 (LISTEN) — bound to all interfaces
# If it shows 127.0.0.1:1234, "Serve on Local Network" is OFF.
The bridge sidecar (gVisor workaround)
You only need this when the model server runs on the host Mac and Docker Desktop uses the gVisor network stack (the 4.34+ default). You do not need it for cloud providers, nor when the model server runs as a container inside the Takuto Core compose network — in that case point the Base URL at the service name (e.g.
http://lm-studio:1234/v1) and skip the bridge.
With gVisor, traffic from containers to private IPs on the Mac’s LAN — including
host.docker.internal (which resolves to 192.168.65.254 inside the container) — can
silently time out, while public IPs work fine. Check your network type:
ps ax | grep com.docker.virtualization | grep -o 'networkType [a-z]*'
# networkType gvisor ← affected
Confirm it’s the gVisor problem
If the server binds to *:1234, the firewall is off, public internet works from a
container, but both host.docker.internal:1234 and the Mac’s LAN IP time out from
inside the container — it’s the gVisor stack.
# Public internet works from a container:
docker exec <takuto-container> sh -c 'wget -q -O - -T 3 https://1.1.1.1 | head -3'
# host.docker.internal resolves but connections time out:
docker exec <takuto-container> sh -c 'curl -v -m 3 http://host.docker.internal:1234/v1/models 2>&1 | tail -5'
# * Connection timed out after 3001 milliseconds
Fix: the lm-bridge socat sidecar
Takuto Core ships a tiny socat container that attaches to both the default bridge
network (which reaches the host correctly under gVisor) and the compose network (so
DinD-nested workers can route to it). Bring it up with a single flag — the make
targets below are the Takuto Core engine repo’s convenience wrapper (the
build-your-own-container path); under the hood LM_BRIDGE=1
just merges docker-compose.lm-bridge.yml into the Compose stack:
make start BACKEND=postgres LM_BRIDGE=1
That starts maestro-lm-bridge at the pinned IP 172.20.0.250, forwarding
TCP/1234 → host.docker.internal:${LM_HOST_PORT:-1234}. Then set the Base URL in
AI Settings to:
http://172.20.0.250:1234/v1
For a non-default port (Ollama’s 11434, for example):
LM_HOST_PORT=11434 make start BACKEND=postgres LM_BRIDGE=1
Smoke test once the bridge is up
docker exec maestro-core-dind-1 docker run --rm --entrypoint /bin/bash \
maestro:latest -c \
'exec 3<>/dev/tcp/172.20.0.250/1234;
printf "GET /v1/models HTTP/1.0\r\nHost: x\r\n\r\n" >&3;
timeout 5 cat <&3 | head -5'
A 200 OK followed by JSON means the worker path is healthy.
Checklist for OpenCode error: unknown error
When the dashboard shows the cryptic OpenCode error: unknown error, walk these in
order — each is a real failure mode:
- Model field starts with
self_hosted/. - Allow shared default token is ticked (unless you saved a per-user bearer).
When it’s off and no bearer is saved, no
opencode.jsonis mounted into the worker and OpenCode exits right after its first-run database migration. - Base URL ends with
/v1. - The model server has “Serve on Local Network” on (
*:<port>, not127.0.0.1:<port>). - Docker Desktop networking is healthy — run the smoke test; if it times out, use the
LM_BRIDGEsidecar above.
If all five are green and the run still fails, run opencode run manually inside the
container with --print-logs --log-level WARN to see OpenCode’s own stderr instead of
the hidden “unknown error”.