Add OpenCode server to goose ai Flatcar for n8n
Run opencode serve on :4096 (Basic Auth) backed by LiteLLM, wire OPENCODE_SERVER_PASSWORD through Infisical, and bump the ai VM to 8G. Co-authored-by: Nick Trochalakis <[email protected]>
This commit is contained in:
1 parent
8d947a5ae1
commit
d013a7e12a
3 files changed
+85
-1
No files matched your search
@@ -202,6 +202,47 @@ systemd:
|
|||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
# OpenCode headless server for n8n (and anything else on the LAN).
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - OPENCODE_SERVER_PASSWORD in Infisical (see secrets.tmpl)
|
||||||
|
# - model keys in /etc/opencode/opencode.json matching LiteLLM
|
||||||
|
# model_name values (Admin UI or GET /v1/models). Models live in
|
||||||
|
# the LiteLLM database, so this file cannot discover them.
|
||||||
|
#
|
||||||
|
# Listens on :4096 with HTTP Basic Auth (user "opencode"). n8n on
|
||||||
|
# oswald-runner reaches it at http://192.168.7.61:4096 -- install the
|
||||||
|
# n8n-nodes-opencode-ai community package, or use HTTP Request nodes
|
||||||
|
# against the OpenAPI at /doc. LLM calls go through LiteLLM on :4000
|
||||||
|
# using the same host-IP pattern as Open WebUI.
|
||||||
|
- name: opencode.service
|
||||||
|
enabled: true
|
||||||
|
contents: |
|
||||||
|
[Unit]
|
||||||
|
Description=OpenCode server
|
||||||
|
After=docker.service litellm.service
|
||||||
|
Requires=docker.service
|
||||||
|
[Service]
|
||||||
|
TimeoutStartSec=0
|
||||||
|
ExecStartPre=/usr/bin/docker pull ghcr.io/anomalyco/opencode:1.18.32
|
||||||
|
ExecStartPre=-/usr/bin/docker rm --force opencode
|
||||||
|
# /var/lib/opencode is the project workspace the agent edits. It
|
||||||
|
# survives container restarts; a VM taint wipes it (scratch space).
|
||||||
|
ExecStart=/usr/bin/docker run --name opencode \
|
||||||
|
-e OPENCODE_SERVER_USERNAME=opencode \
|
||||||
|
-e OPENCODE_SERVER_PASSWORD=${opencode_server_password} \
|
||||||
|
-e OPENCODE_CONFIG=/etc/opencode/opencode.json \
|
||||||
|
-v /etc/opencode/opencode.json:/etc/opencode/opencode.json:ro \
|
||||||
|
-v /var/lib/opencode:/workspace \
|
||||||
|
-w /workspace \
|
||||||
|
-p 4096:4096 \
|
||||||
|
ghcr.io/anomalyco/opencode:1.18.32 \
|
||||||
|
serve --hostname 0.0.0.0 --port 4096
|
||||||
|
ExecStop=/usr/bin/docker stop opencode
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
# Rebuilds the vector store after a taint. Runs on every boot, but only
|
# Rebuilds the vector store after a taint. Runs on every boot, but only
|
||||||
# acts when Qdrant holds no Open WebUI collections -- that is, when the
|
# acts when Qdrant holds no Open WebUI collections -- that is, when the
|
||||||
# local volume went with the VM.
|
# local volume went with the VM.
|
||||||
@@ -248,6 +289,12 @@ storage:
|
|||||||
# root-owned 0700 directory is what it expects.
|
# root-owned 0700 directory is what it expects.
|
||||||
- path: /var/lib/qdrant
|
- path: /var/lib/qdrant
|
||||||
mode: 0700
|
mode: 0700
|
||||||
|
# OpenCode project workspace. Scratch space: survives container
|
||||||
|
# restarts, not a VM taint.
|
||||||
|
- path: /var/lib/opencode
|
||||||
|
mode: 0755
|
||||||
|
- path: /etc/opencode
|
||||||
|
mode: 0755
|
||||||
files:
|
files:
|
||||||
# LiteLLM proxy settings. Models stay in the database
|
# LiteLLM proxy settings. Models stay in the database
|
||||||
# (STORE_MODEL_IN_DB), so this file carries general_settings only --
|
# (STORE_MODEL_IN_DB), so this file carries general_settings only --
|
||||||
@@ -317,3 +364,37 @@ storage:
|
|||||||
use_x_forwarded_for: true
|
use_x_forwarded_for: true
|
||||||
mcp_trusted_proxy_ranges:
|
mcp_trusted_proxy_ranges:
|
||||||
- 192.168.0.0/24
|
- 192.168.0.0/24
|
||||||
|
# OpenCode provider config. LiteLLM is an OpenAI-compatible proxy, so
|
||||||
|
# we use @ai-sdk/openai-compatible rather than a first-party provider.
|
||||||
|
#
|
||||||
|
# Model keys under "models" must match LiteLLM model_name values exactly
|
||||||
|
# (they are stored in the LiteLLM DB, not in litellm/config.yaml). Edit
|
||||||
|
# this list to match what /v1/models returns, then taint the VM -- or
|
||||||
|
# PATCH http://192.168.7.61:4096/config at runtime. The default model
|
||||||
|
# below is the first key; change "model" when you change the set.
|
||||||
|
#
|
||||||
|
# apiKey is templated from Infisical (LITELLM_MASTER_KEY). Ignition
|
||||||
|
# snippets are plaintext on the node -- same caveat as the other units.
|
||||||
|
- path: /etc/opencode/opencode.json
|
||||||
|
mode: 0600
|
||||||
|
contents:
|
||||||
|
inline: |
|
||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"model": "litellm/claude-sonnet-4",
|
||||||
|
"provider": {
|
||||||
|
"litellm": {
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"name": "LiteLLM",
|
||||||
|
"options": {
|
||||||
|
"baseURL": "http://192.168.7.61:4000/v1",
|
||||||
|
"apiKey": "${litellm_master_key}"
|
||||||
|
},
|
||||||
|
"models": {
|
||||||
|
"claude-sonnet-4": {
|
||||||
|
"name": "Claude Sonnet 4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
{{- if eq $secret.Key "LITELLM_DATABASE_URL" }}"litellm_database_url": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "LITELLM_DATABASE_URL" }}"litellm_database_url": "{{$secret.Value}}",{{ end }}
|
||||||
{{- if eq $secret.Key "LITELLM_MASTER_KEY" }}"litellm_master_key": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "LITELLM_MASTER_KEY" }}"litellm_master_key": "{{$secret.Value}}",{{ end }}
|
||||||
{{- if eq $secret.Key "LITELLM_SALT_KEY" }}"litellm_salt_key": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "LITELLM_SALT_KEY" }}"litellm_salt_key": "{{$secret.Value}}",{{ end }}
|
||||||
|
{{- if eq $secret.Key "OPENCODE_SERVER_PASSWORD" }}"opencode_server_password": "{{$secret.Value}}",{{ end }}
|
||||||
{{- if eq $secret.Key "HEADSCALE_AUTHKEY" }}"headscale_authkey": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "HEADSCALE_AUTHKEY" }}"headscale_authkey": "{{$secret.Value}}",{{ end }}
|
||||||
{{- if eq $secret.Key "OPENWEBUI_DATABASE_URL" }}"openwebui_database_url": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "OPENWEBUI_DATABASE_URL" }}"openwebui_database_url": "{{$secret.Value}}",{{ end }}
|
||||||
{{- if eq $secret.Key "OPENWEBUI_SECRET_KEY" }}"openwebui_secret_key": "{{$secret.Value}}",{{ end }}
|
{{- if eq $secret.Key "OPENWEBUI_SECRET_KEY" }}"openwebui_secret_key": "{{$secret.Value}}",{{ end }}
|
||||||
|
|||||||
+3
-1
@@ -218,10 +218,12 @@ nodes = {
|
|||||||
vmid = 211
|
vmid = 211
|
||||||
ip = "192.168.7.61"
|
ip = "192.168.7.61"
|
||||||
vlan = 7
|
vlan = 7
|
||||||
memory = 4096
|
# 8G: LiteLLM + Open WebUI + Qdrant + open-terminal + OpenCode.
|
||||||
|
memory = 8192
|
||||||
cores = 2
|
cores = 2
|
||||||
disk_size = 32
|
disk_size = 32
|
||||||
qemu_guest_agent = false
|
qemu_guest_agent = false
|
||||||
|
# OPENCODE_SERVER_PASSWORD in Infisical; see butane/ai.yaml.tftpl.
|
||||||
butane_file = "butane/ai.yaml.tftpl"
|
butane_file = "butane/ai.yaml.tftpl"
|
||||||
}
|
}
|
||||||
# Headscale subnet router / exit node. Registers as an ephemeral node,
|
# Headscale subnet router / exit node. Registers as an ephemeral node,
|
||||||
|
|||||||
Reference in new issue
Block a user