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

  1. Set the provider to OpenCode in Configuration → AI Settings.

  2. Set the Model field. OpenCode’s -m flag expects <providerId>/<modelId>, and Takuto Core‘s generated config always names the provider self_hosted, so the value must look like:

    self_hosted/<model-id-as-reported-by-the-server>
    Model id reported by the serverSet in Takuto
    qwen/qwen2.5-vl-7bself_hosted/qwen/qwen2.5-vl-7b
    lmstudio-community/Llama-3.1-8B-Instruct-GGUFself_hosted/lmstudio-community/Llama-3.1-8B-Instruct-GGUF
  3. Set the Base URL to the OpenAI-compatible endpoint, including the trailing /v1:

    http://host.docker.internal:1234/v1
  4. 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.

  5. 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.1 and 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:

  1. Model field starts with self_hosted/.
  2. Allow shared default token is ticked (unless you saved a per-user bearer). When it’s off and no bearer is saved, no opencode.json is mounted into the worker and OpenCode exits right after its first-run database migration.
  3. Base URL ends with /v1.
  4. The model server has “Serve on Local Network” on (*:<port>, not 127.0.0.1:<port>).
  5. Docker Desktop networking is healthy — run the smoke test; if it times out, use the LM_BRIDGE sidecar 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”.