From d013a7e12a6edbbc59506ae0a9c3d5f5baa2573b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 23 Sep 2026 23:44:10 +0000 Subject: [PATCH 1/2] 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 --- butane/ai.yaml.tftpl | 81 ++++++++++++++++++++++++++++++++++++++++++++ secrets.tmpl | 1 + terraform.tfvars | 4 ++- 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/butane/ai.yaml.tftpl b/butane/ai.yaml.tftpl index 042c04b..a88c5fa 100644 --- a/butane/ai.yaml.tftpl +++ b/butane/ai.yaml.tftpl @@ -202,6 +202,47 @@ systemd: RestartSec=5s [Install] 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 # acts when Qdrant holds no Open WebUI collections -- that is, when the # local volume went with the VM. @@ -248,6 +289,12 @@ storage: # root-owned 0700 directory is what it expects. - path: /var/lib/qdrant 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: # LiteLLM proxy settings. Models stay in the database # (STORE_MODEL_IN_DB), so this file carries general_settings only -- @@ -317,3 +364,37 @@ storage: use_x_forwarded_for: true mcp_trusted_proxy_ranges: - 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" + } + } + } + } + } diff --git a/secrets.tmpl b/secrets.tmpl index 1b3adb9..c7a2c1d 100644 --- a/secrets.tmpl +++ b/secrets.tmpl @@ -14,6 +14,7 @@ {{- 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_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 "OPENWEBUI_DATABASE_URL" }}"openwebui_database_url": "{{$secret.Value}}",{{ end }} {{- if eq $secret.Key "OPENWEBUI_SECRET_KEY" }}"openwebui_secret_key": "{{$secret.Value}}",{{ end }} diff --git a/terraform.tfvars b/terraform.tfvars index 5c1c609..84e5c73 100644 --- a/terraform.tfvars +++ b/terraform.tfvars @@ -218,10 +218,12 @@ nodes = { vmid = 211 ip = "192.168.7.61" vlan = 7 - memory = 4096 + # 8G: LiteLLM + Open WebUI + Qdrant + open-terminal + OpenCode. + memory = 8192 cores = 2 disk_size = 32 qemu_guest_agent = false + # OPENCODE_SERVER_PASSWORD in Infisical; see butane/ai.yaml.tftpl. butane_file = "butane/ai.yaml.tftpl" } # Headscale subnet router / exit node. Registers as an ephemeral node, From 5884eea4d815d274df5400c16c8497ace4f11c1c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 23 Sep 2026 23:44:43 +0000 Subject: [PATCH 2/2] Keep ai Flatcar memory at 4G Do not bump RAM for the OpenCode addition. Co-authored-by: Nick Trochalakis --- terraform.tfvars | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/terraform.tfvars b/terraform.tfvars index 84e5c73..9ee9336 100644 --- a/terraform.tfvars +++ b/terraform.tfvars @@ -218,8 +218,7 @@ nodes = { vmid = 211 ip = "192.168.7.61" vlan = 7 - # 8G: LiteLLM + Open WebUI + Qdrant + open-terminal + OpenCode. - memory = 8192 + memory = 4096 cores = 2 disk_size = 32 qemu_guest_agent = false