Ollama and opencode setup for the Mac mini

The Ollama LaunchAgent, derived models (mostly to set num_ctx, which
Ollama's OpenAI-compatible API won't take per request), and opencode's
config, now defaulting to qwen3-coder:30b: qwen3:32b tends to end its
turn after thinking without making the tool call it planned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Philip Peterson 2026-10-11 07:57:46 -08:00
commit e2df9cc0d0
7 changed files with 133 additions and 0 deletions

42
README.md Normal file
View file

@ -0,0 +1,42 @@
# local-llm
Local LLM setup on the Mac mini: [Ollama](https://ollama.com) serving models,
[opencode](https://opencode.ai) using them.
```sh
./setup.sh
```
does everything, and is safe to re-run:
- installs `ollama/com.ollama.server.plist` as the LaunchAgent that runs
`ollama serve` (reloaded only when it changed, since that restarts Ollama);
- builds each model in `ollama/models/` (pulling its base model if missing);
- symlinks `~/.config/opencode/opencode.json` to `opencode/opencode.json`, so
edits made either way end up here.
Needs `brew install ollama opencode`.
## Models
| Model | From | Used for |
|-------|------|----------|
| `qwen3-coder:30b-32k` | `qwen3-coder:30b` | opencode's default |
| `qwen3:32b-32k` | `qwen3:32b` | opencode, as an option |
| `inspector` | `qwen3:32b` | 16k-context variant |
Each derived model mostly exists to set `num_ctx`. Ollama's OpenAI-compatible
API, which opencode talks to, ignores any context size sent with a request, so
without it a long agent prompt is silently cut to Ollama's small default
context. A Modelfile's `# name:` line is the name it's created under.
`qwen3:32b` is a thinking model, and in opencode it often ends its turn after
thinking, without making the tool call it planned. `qwen3-coder` was trained
for agentic tool use and doesn't think first, so it doesn't have that
problem, and it's much faster: a mixture of experts with ~3B active parameters.
## Server settings
From the LaunchAgent: listens on all interfaces (`0.0.0.0:11434`), keeps models
loaded indefinitely (`OLLAMA_KEEP_ALIVE=-1`), flash attention on. Logs go to
`/tmp/ollama.log`.

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.ollama.server</string>
<key>ProgramArguments</key><array><string>/opt/homebrew/bin/ollama</string><string>serve</string></array>
<key>EnvironmentVariables</key><dict>
<key>OLLAMA_HOST</key><string>0.0.0.0:11434</string>
<key>OLLAMA_KEEP_ALIVE</key><string>-1</string>
<key>OLLAMA_FLASH_ATTENTION</key><string>1</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardErrorPath</key><string>/tmp/ollama.log</string>
</dict></plist>

View file

@ -0,0 +1,3 @@
# name: inspector
FROM qwen3:32b
PARAMETER num_ctx 16384

View file

@ -0,0 +1,5 @@
# name: qwen3:32b-32k
# General-purpose thinking model. In opencode it often ends its turn after
# thinking without making the tool call it planned; fine for plain chat.
FROM qwen3:32b
PARAMETER num_ctx 32768

View file

@ -0,0 +1,5 @@
# name: qwen3-coder:30b-32k
# opencode's default: trained for agentic tool calling, no thinking phase,
# mixture-of-experts (~3B active params) so it's fast.
FROM qwen3-coder:30b
PARAMETER num_ctx 32768

26
opencode/opencode.json Normal file
View file

@ -0,0 +1,26 @@
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"name": "Ollama (local)",
"options": {
"baseURL": "http://localhost:11434/v1"
},
"models": {
"qwen3-coder:30b-32k": {
"name": "Qwen3 Coder 30B (32k context)",
"tools": true,
"limit": { "context": 32768, "output": 8192 }
},
"qwen3:32b-32k": {
"name": "Qwen3 32B (32k context)",
"tools": true,
"limit": { "context": 32768, "output": 8192 }
}
}
}
},
"model": "ollama/qwen3-coder:30b-32k",
"small_model": "ollama/qwen3-coder:30b-32k"
}

38
setup.sh Executable file
View file

@ -0,0 +1,38 @@
#!/bin/sh
# Makes this Mac match the repo: Ollama's LaunchAgent, the derived models in
# ollama/models, and opencode's config (symlinked, so edits land in the repo).
# Safe to re-run.
set -eu
cd "$(dirname "$0")"
repo=$(pwd)
# Ollama server LaunchAgent. Only reloaded when it changed: reloading
# restarts Ollama and unloads whatever models are in memory.
plist=~/Library/LaunchAgents/com.ollama.server.plist
if ! cmp -s ollama/com.ollama.server.plist "$plist"; then
echo "Installing $plist"
launchctl bootout "gui/$(id -u)" "$plist" 2>/dev/null || true
cp ollama/com.ollama.server.plist "$plist"
launchctl bootstrap "gui/$(id -u)" "$plist"
until curl -fs localhost:11434/api/version >/dev/null; do sleep 1; done
fi
# Derived models: each Modelfile names its model on a "# name:" line.
for f in ollama/models/*.Modelfile; do
name=$(sed -n 's/^# name: *//p' "$f")
base=$(sed -n 's/^FROM *//p' "$f")
[ -n "$name" ] && [ -n "$base" ] || { echo "$f: needs '# name:' and FROM lines" >&2; exit 1; }
ollama show "$base" >/dev/null 2>&1 || ollama pull "$base"
echo "Creating $name from $f"
ollama create "$name" -f "$f" >/dev/null
done
# opencode
mkdir -p ~/.config/opencode
target=~/.config/opencode/opencode.json
if [ -e "$target" ] && [ ! -L "$target" ]; then
mv "$target" "$target.bak"
echo "Moved the old $target to $target.bak"
fi
ln -sfn "$repo/opencode/opencode.json" "$target"
echo "Linked $target"