Commit 2befbb27bab2
Changed files (8)
secrets/okinawa/gemini-api-key.age
@@ -0,0 +1,10 @@
+age-encryption.org/v1
+-> piv-p256 ItIHHA A3fHEtxe9/4OeuW8FXm/e5KsVzgbTGB2ARiQccP0hyA7
+kE0gbISO1Z/VpjmB4kk0gBSMMEj7bZhZZDewkc57P6I
+-> piv-p256 cUinNw A/gvIJ6g6KK/jgrs9Mt5NBwy231oJ7r6DntZdk8bS9yi
+TGcVqkqYuwrmceVvFSsiIWws7lbBAPHPP5cV6e6GrrA
+-> ssh-ed25519 3Z+PEA fq9mL8HZljoOC2Kgihde9ZP90IrCrOVT3YRUHjvR7Sc
+UEkzZY1pDy8QRZ7E4MXuMPgZQjfU/OvpsSEmwe1o340
+--- D2wfYs58FZBT1qfjbMHYbP3kdx38ZFwYTVSv7qY38wM
+�1I'���?�4s���Q#S�7��'xd�h���,�79�<{��ҙ��x�g�p�&�)
+�dݎ1�H��
\ No newline at end of file
secrets/okinawa/groq-api-key.age
Binary file
secrets/okinawa/opencode-password.age
@@ -0,0 +1,9 @@
+age-encryption.org/v1
+-> piv-p256 ItIHHA A9JTl1FqobWybB9TSTUgTg8aZf2Kj0NgBbR0oREkH9iu
+71cLdZRzQ4yC8D6cRBc0NNAXapnHg4igyYRjaV1ljbA
+-> piv-p256 cUinNw A4qgekq6P7RnQZ2DkGtnLA+JqERDYIEi418bDsX4wDtV
+5dLW2ha+MwhauzUMLurYeGCdL5TZEJWUWQt7Rg7tN+U
+-> ssh-ed25519 3Z+PEA OvO7e/qMLqcHvuJysqLNvhVAFF0bnguffDRqMA0wZT4
++Wdn+iaQqjQFf5/b44ok/VPxkaPVJI61G0n/agtNUzc
+--- FlO3nWgpodCUheE2ycLSaK+wPk+g/spCRksrj0GXJ2s
+��D�3��m�U���{��J$Uj×�~�ʜ ��6�;y��rG��|xn�
\ No newline at end of file
secrets/okinawa/openrouter-api-key.age
@@ -0,0 +1,9 @@
+age-encryption.org/v1
+-> piv-p256 ItIHHA A+J7Q0Svnkf+Qup3i3vWAKvhOLQ7v2WUWgA/SKsChX15
+s7bw+A9YuQLWy1dd/PFqnOAW6LP6NZPq1UDn6C7sNAk
+-> piv-p256 cUinNw AxJJna3MTbqDA9y7OtyzgpQkYymE+M+UoeOI2AUZqoKO
+zV6WYn51PUWfccLsM1Z7GvSjaxzP+6I3LB0IA8kCinM
+-> ssh-ed25519 3Z+PEA tODvvH4TtPeWcAT6QLQbQfit+Daljkq/5Y0tLGXpojU
+h01f48auvvVrGX49hOVsz4Jt1IBdCI4qfeB5O0Mwsi8
+--- gTronstY7QZJzmTsOnMeBPYvx2QQ1hV2OMRyPwno0BU
+��_�
�&�E7*�����^(?�s?t:�Zn������A2� �okՃ���w�� ͜陂�8=��T������� �EQ͉�xW����/�� �A&������
\ No newline at end of file
systems/okinawa/extra.nix
@@ -1,11 +1,14 @@
{
pkgs,
+ inputs,
libx,
globals,
+ config,
...
}:
{
imports = [
+ inputs.daneel.nixosModules.daneel
../common/programs/direnv.nix
../common/programs/git.nix
../common/programs/nix-ld.nix
@@ -65,6 +68,159 @@
endpointPublicKey = "${globals.machines.kerkouane.net.vpn.pubkey}";
};
+ # Age secrets for OpenCode web
+ age.secrets = {
+ "opencode-password" = {
+ file = ../../secrets/okinawa/opencode-password.age;
+ mode = "400";
+ owner = "vincent";
+ };
+ "groq-api-key" = {
+ file = ../../secrets/okinawa/groq-api-key.age;
+ mode = "400";
+ owner = "vincent";
+ };
+ "openrouter-api-key" = {
+ file = ../../secrets/okinawa/openrouter-api-key.age;
+ mode = "400";
+ owner = "vincent";
+ };
+ "gemini-api-key" = {
+ file = ../../secrets/okinawa/gemini-api-key.age;
+ mode = "400";
+ owner = "vincent";
+ };
+ "xmpp-research-bot-password" = {
+ file = ../../secrets/okinawa/xmpp-research-bot-password.age;
+ mode = "400";
+ owner = "vincent";
+ };
+ };
+
+ # Daneel XMPP Research Bot
+ services.daneel = {
+ enable = true;
+ xmppJid = "researchbot@xmpp.sbr.pm";
+ ownerJid = "vincent@xmpp.sbr.pm";
+ xmppPasswordFile = config.age.secrets."xmpp-research-bot-password".path;
+ geminiApiKeyFile = config.age.secrets."gemini-api-key".path;
+ inboxPath = "/home/vincent/desktop/org/inbox.org";
+ debug = true;
+ user = "vincent";
+ group = "users";
+ };
+
+ # OpenCode web interface for remote AI coding
+ # Accessible via opencode.sbr.pm through rhea's Traefik reverse proxy
+ systemd.services.opencode-web =
+ let
+ opencode-config = pkgs.writeText "opencode.json" (
+ builtins.toJSON {
+ "$schema" = "https://opencode.ai/config.json";
+ provider = {
+ # Local llama-server (OpenAI-compatible API)
+ llama-server = {
+ npm = "@ai-sdk/openai-compatible";
+ name = "llama-server (local)";
+ options = {
+ baseURL = "http://127.0.0.1:8090/v1";
+ apiKey = "local-test";
+ };
+ models = {
+ "Qwen/Qwen3-Coder-30B-A3B-Instruct-GGUF:Q4_K_M" = {
+ name = "Qwen3 Coder 30B MoE";
+ };
+ "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF:Q4_K_M" = {
+ name = "Qwen 2.5 Coder 7B";
+ };
+ "Qwen/Qwen3-8B-GGUF:Q4_K_M" = {
+ name = "Qwen3 8B";
+ };
+ "bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF:Q4_K_M" = {
+ name = "DeepSeek R1 Distill 7B";
+ };
+ "unsloth/Phi-4-mini-instruct-GGUF:Q4_K_M" = {
+ name = "Phi-4 mini 3.8B";
+ };
+ "Qwen/Qwen3-Coder-Next-GGUF:Q3_K_M" = {
+ name = "Qwen3 Coder Next 80B MoE";
+ };
+ };
+ };
+ # Vertex AI (Claude on Google Cloud)
+ google-vertex = {
+ options = {
+ project = "cloudability-it-gemini";
+ location = "us-east5";
+ };
+ };
+ };
+ server = {
+ hostname = "0.0.0.0";
+ port = 5555;
+ };
+ }
+ );
+ in
+ {
+ description = "OpenCode web interface";
+ after = [
+ "network.target"
+ "llama-cpp.service"
+ ];
+ wantedBy = [ "multi-user.target" ];
+
+ serviceConfig = {
+ Type = "simple";
+ WorkingDirectory = "/home/vincent";
+ ExecStart = "${pkgs.opencode}/bin/opencode web";
+ Restart = "on-failure";
+ RestartSec = 10;
+
+ User = "vincent";
+ Group = "users";
+
+ # Environment file for secrets (built at service start)
+ EnvironmentFile = "/run/opencode/env";
+ };
+
+ environment = {
+ HOME = "/home/vincent";
+ OPENCODE_SERVER_USERNAME = "opencode";
+ # Config file location
+ XDG_CONFIG_HOME = "/run/opencode/config";
+ GOOGLE_VERTEX_PROJECT = "cloudability-it-gemini";
+ GOOGLE_VERTEX_LOCATION = "us-east5";
+ };
+
+ # Build environment file from secrets and copy config
+ preStart = ''
+ mkdir -p /run/opencode/config/opencode
+ cp ${opencode-config} /run/opencode/config/opencode/opencode.json
+
+ # Build env file from secrets
+ {
+ echo "OPENCODE_SERVER_PASSWORD=$(cat ${config.age.secrets."opencode-password".path})"
+ echo "GROQ_API_KEY=$(cat ${config.age.secrets."groq-api-key".path})"
+ echo "OPENROUTER_API_KEY=$(cat ${config.age.secrets."openrouter-api-key".path})"
+ echo "GOOGLE_GENERATIVE_AI_API_KEY=$(cat ${config.age.secrets."gemini-api-key".path})"
+ } > /run/opencode/env
+ chmod 400 /run/opencode/env
+ '';
+ };
+
+ # Ensure /run/opencode directory exists
+ systemd.tmpfiles.rules = [
+ "d /run/opencode 0755 vincent users -"
+ "d /run/opencode/config 0755 vincent users -"
+ ];
+
+ # Firewall: OpenCode web + llama-server (VPN access)
+ networking.firewall.allowedTCPPorts = [
+ 5555 # OpenCode web
+ 8090 # llama-server
+ ];
+
# llama.cpp server for local LLM inference with Vulkan GPU (RX 6700S)
# Router mode: serves multiple models with on-demand loading (--models-max 1)
# Only one model loaded in VRAM at a time; auto-swaps on request
@@ -123,7 +279,7 @@
ExecStart = builtins.concatStringsSep " " [
"${llama-cpp-vulkan}/bin/llama-server"
"--log-disable"
- "--host 127.0.0.1"
+ "--host 0.0.0.0"
"--port 8090"
"--models-preset ${modelsPreset}"
"--models-max 1"
systems/rhea/extra.nix
@@ -341,11 +341,10 @@ in
];
lidarr = mkRouter "lidarr" [ "lidarr.sbr.pm" ];
homepage = mkRouter "homepage" [ "homepage.sbr.pm" ];
- # Ollama LLM service (VPN-only, no auth needed)
- ollama = mkRouter "ollama" [
- "ollama.sbr.pm"
- "llm.sbr.pm"
- ];
+ # OpenCode web interface on okinawa (VPN-only)
+ opencode = mkRouter "opencode" [ "opencode.sbr.pm" ];
+ # llama-server on okinawa (VPN-only, OpenAI-compatible API)
+ llm = mkRouter "llm" [ "llm.sbr.pm" ];
# SearXNG metasearch engine on aomi
search = mkRouter "search" [
"search.sbr.pm"
@@ -373,7 +372,8 @@ in
homepage = mkService "http://${builtins.head globals.machines.aion.net.ips}:3001";
audiobookshelf = mkService "http://${builtins.head globals.machines.aion.net.ips}:13378";
lidarr = mkService "http://${builtins.head globals.machines.aion.net.ips}:8686";
- ollama = mkService "http://${builtins.head globals.machines.aomi.net.ips}:8000";
+ opencode = mkService "http://${builtins.head globals.machines.okinawa.net.vpn.ips}:5555";
+ llm = mkService "http://${builtins.head globals.machines.okinawa.net.vpn.ips}:8090";
search = mkService "http://${builtins.head globals.machines.aomi.net.ips}:8888";
};
middlewares =
globals.nix
@@ -631,11 +631,10 @@ _: {
n8n.host = "rhea";
paperless.host = "rhea";
grafana.host = "rhea";
- # Ollama LLM service on aomi (routed through rhea/traefik)
- ollama = {
- host = "rhea";
- aliases = [ "llm" ];
- };
+ # OpenCode web interface on okinawa (routed through rhea/traefik)
+ opencode.host = "rhea";
+ # llama-server on okinawa (routed through rhea/traefik)
+ llm.host = "rhea";
# SearXNG metasearch engine on aomi (routed through rhea/traefik)
search = {
host = "rhea";
secrets.nix
@@ -161,6 +161,14 @@ in
"secrets/aomi/xmpp-research-bot-password.age".publicKeys = users ++ [ aomi ];
"secrets/aomi/gemini-api-key.age".publicKeys = users ++ [ aomi ];
"secrets/aomi/searxng-secret-key.age".publicKeys = users ++ [ aomi ];
+
+ # OpenCode web on okinawa
+ "secrets/okinawa/opencode-password.age".publicKeys = users ++ [ okinawa ];
+ "secrets/okinawa/groq-api-key.age".publicKeys = users ++ [ okinawa ];
+ "secrets/okinawa/openrouter-api-key.age".publicKeys = users ++ [ okinawa ];
+ "secrets/okinawa/gemini-api-key.age".publicKeys = users ++ [ okinawa ];
+ # Daneel XMPP bot on okinawa
+ "secrets/okinawa/xmpp-research-bot-password.age".publicKeys = users ++ [ okinawa ];
"secrets/rhea/restic-aix-password.age".publicKeys = users ++ [ rhea ];
# Harmonia binary cache signing keys