From 2013804b176ac498e4f1d06a3316899b95232b05 Mon Sep 17 00:00:00 2001 From: mjallen18 Date: Wed, 25 Mar 2026 19:59:49 -0500 Subject: [PATCH] lemonade --- modules/nixos/services/lemonade/default.nix | 146 ++++++++++++++++++ packages/lemonade/default.nix | 136 ++++++++++++++++ systems/x86_64-linux/jallen-nas/apps.nix | 6 + .../x86_64-linux/jallen-nas/nas-defaults.nix | 1 + 4 files changed, 289 insertions(+) create mode 100644 modules/nixos/services/lemonade/default.nix create mode 100644 packages/lemonade/default.nix diff --git a/modules/nixos/services/lemonade/default.nix b/modules/nixos/services/lemonade/default.nix new file mode 100644 index 0000000..0082805 --- /dev/null +++ b/modules/nixos/services/lemonade/default.nix @@ -0,0 +1,146 @@ +{ + config, + lib, + pkgs, + namespace, + ... +}: +let + inherit (lib) mkForce getExe; + inherit (lib.${namespace}) mkModule mkOpt; + + name = "lemonade"; + cfg = config.${namespace}.services.${name}; + + # lemonade-server serve args built from config options + serveArgs = lib.concatStringsSep " " ( + [ + "serve" + "--no-tray" + "--port ${toString cfg.port}" + "--host ${cfg.host}" + "--log-level ${cfg.logLevel}" + ] + ++ lib.optional (cfg.maxLoadedModels != 1) "--max-loaded-models ${toString cfg.maxLoadedModels}" + ++ lib.optional (cfg.extraModelsDir != null) "--extra-models-dir ${cfg.extraModelsDir}" + ++ cfg.extraArgs + ); + + lemonadeConfig = mkModule { + inherit config name; + description = "Lemonade local LLM server"; + + options = { + # Override mkModule's default port of 80 with lemonade's actual default. + port = mkOpt lib.types.int 8000 "Port lemonade-router listens on"; + + host = mkOpt lib.types.str "127.0.0.1" "Address lemonade-router binds to"; + + logLevel = mkOpt (lib.types.enum [ + "critical" + "error" + "warning" + "info" + "debug" + "trace" + ]) "info" "Log level for lemonade-router"; + + maxLoadedModels = mkOpt lib.types.int 1 "Maximum number of models to keep loaded simultaneously"; + + extraModelsDir = + mkOpt (lib.types.nullOr lib.types.str) null + "Extra directory scanned for local GGUF model files"; + + extraArgs = + mkOpt (lib.types.listOf lib.types.str) [ ] + "Extra arguments passed verbatim to lemonade-server serve"; + + modelsDir = + mkOpt lib.types.str "/var/lib/${name}/models" + "Directory where downloaded models are stored (exposed as HF_HOME)"; + + apiKeyFile = + mkOpt (lib.types.nullOr lib.types.str) null + "Path to a file containing the LEMONADE_API_KEY (e.g. a sops secret path)"; + }; + + moduleConfig = { + # Install the package system-wide so lemonade-server / lemonade-router are + # available in PATH for interactive use alongside the daemon. + environment.systemPackages = [ pkgs.${namespace}.lemonade ]; + + systemd.services.${name} = { + description = "Lemonade local LLM server"; + wantedBy = [ "multi-user.target" ]; + after = [ + "network.target" + "network-online.target" + ]; + wants = [ "network-online.target" ]; + + # lemonade-server discover lemonade-router by reading /proc/self/exe, + # so we must use ExecStart with the real binary, not a shell wrapper. + serviceConfig = { + Type = "simple"; + ExecStart = "${getExe pkgs.${namespace}.lemonade} ${serveArgs}"; + + User = name; + Group = name; + DynamicUser = mkForce false; + + # Models and HuggingFace cache land under modelsDir. + # HF_HOME overrides the default ~/.cache/huggingface location. + Environment = [ + "HF_HOME=${cfg.modelsDir}" + "XDG_RUNTIME_DIR=/run/${name}" + ]; + + # Load an API key from a secrets file if provided. + EnvironmentFile = lib.optional (cfg.apiKeyFile != null) cfg.apiKeyFile; + + # Runtime directory for PID file / lock file (created automatically + # by systemd and owned by the service user). + RuntimeDirectory = name; + RuntimeDirectoryMode = "0755"; + + # Persistent state: models cache. + StateDirectory = name; + StateDirectoryMode = "0750"; + + # Home directory for the service user (needed by some HF tooling). + WorkingDirectory = "/var/lib/${name}"; + + Restart = "on-failure"; + RestartSec = "5s"; + + StandardOutput = "journal"; + StandardError = "journal"; + SyslogIdentifier = name; + + # Hardening — lemonade needs network access and subprocess execution + # for spawning llama.cpp / whisper.cpp backends. + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [ + "/var/lib/${name}" + "/run/${name}" + ]; + }; + }; + + users.users.${name} = { + isSystemUser = true; + group = name; + home = "/var/lib/${name}"; + createHome = true; + description = "Lemonade LLM server daemon"; + }; + users.groups.${name} = { }; + }; + }; +in +{ + imports = [ lemonadeConfig ]; +} diff --git a/packages/lemonade/default.nix b/packages/lemonade/default.nix new file mode 100644 index 0000000..c6d7c50 --- /dev/null +++ b/packages/lemonade/default.nix @@ -0,0 +1,136 @@ +{ + lib, + stdenv, + fetchFromGitHub, + cmake, + ninja, + pkg-config, + # C++ build-time dependencies + curl, + openssl, + zlib, + nlohmann_json, + libwebsockets, + cli11, + # Linux system libraries (optional, enable richer features) + systemd, + libdrm, + libcap, +}: + +let + # cpp-httplib is not yet in nixpkgs; pre-fetch for CMake FetchContent override. + # The CMakeLists.txt version gate requires >= 0.26.0. + cpp-httplib-src = fetchFromGitHub { + owner = "yhirose"; + repo = "cpp-httplib"; + rev = "v0.26.0"; + hash = "sha256-+VPebnFMGNyChM20q4Z+kVOyI/qDLQjRsaGS0vo8kDM="; + }; +in + +stdenv.mkDerivation rec { + pname = "lemonade"; + version = "10.0.1"; + + src = fetchFromGitHub { + owner = "lemonade-sdk"; + repo = "lemonade"; + rev = "v${version}"; + hash = "sha256-aswK7OXMWTFUNHrrktf1Vx3nvTkLWMEhAgWlil1Zu2c="; + }; + + nativeBuildInputs = [ + cmake + ninja + pkg-config + ]; + + buildInputs = [ + curl + openssl + zlib + nlohmann_json + libwebsockets + cli11 + systemd + libdrm + libcap + ]; + + cmakeFlags = [ + # Disable the web app (requires Node.js / npm at build time) + "-DBUILD_WEB_APP=OFF" + # Disable Linux tray app (requires GTK3 + AppIndicator3; optional) + "-DREQUIRE_LINUX_TRAY=OFF" + "-DCMAKE_BUILD_TYPE=Release" + # Prevent FetchContent from reaching the network (sandbox-safe) + "-DFETCHCONTENT_FULLY_DISCONNECTED=ON" + # Provide pre-fetched sources for deps not in nixpkgs + "-DFETCHCONTENT_SOURCE_DIR_HTTPLIB=${cpp-httplib-src}" + ]; + + installPhase = '' + runHook preInstall + + mkdir -p "$out/bin" + + # HTTP server — pure OpenAI-compatible REST API, no CLI interface + install -Dm755 lemonade-router "$out/bin/lemonade-router" + + # Console CLI client — primary user-facing tool (serve, list, pull, run, + # status, stop, logs, recipes, …); manages the lemonade-router process + install -Dm755 lemonade-server "$out/bin/lemonade-server" + + # Standalone HTTP-only CLI client (newer, lightweight alternative to + # lemonade-server for scripting; requires a running lemonade-router) + if [ -f lemonade ]; then + install -Dm755 lemonade "$out/bin/lemonade" + fi + + # Resources: model registry (server_models.json), backend version config, + # and static web UI assets served by lemonade-router. + # + # get_resource_path() in path_utils.cpp resolves files as: + # /resources/ (first check, highest priority) + # /usr/share/lemonade-server/resources/ (fallback) + # + # We install the real files under share/ and symlink bin/resources → + # ../share/lemonade-server/resources so the first check succeeds regardless + # of whether the Nix store path is in any of the hardcoded fallback prefixes. + if [ -d resources ]; then + mkdir -p "$out/share/lemonade-server/resources" + cp -r resources/. "$out/share/lemonade-server/resources/" + ln -s "$out/share/lemonade-server/resources" "$out/bin/resources" + fi + + runHook postInstall + ''; + + meta = with lib; { + description = "Local LLM server that serves optimized LLMs from GPUs and NPUs"; + longDescription = '' + Lemonade helps users discover and run local AI apps by serving + optimized LLMs, images, and speech directly from their own GPUs and + NPUs. It exposes an OpenAI-compatible REST API at + http://localhost:8000/api/v1 and bundles a web UI, model manager, + and CLI client. + + Binaries: + lemonade-router — HTTP server (OpenAI-compatible API on :8000) + lemonade-server — CLI client: serve, list, pull, run, status, stop, … + lemonade — standalone HTTP-only CLI for scripting + + Typical usage: + lemonade-server serve # start the server (headless on Linux) + lemonade-server list # browse available models + lemonade-server pull Gemma-3-4b-it-GGUF + lemonade-server run Gemma-3-4b-it-GGUF + ''; + homepage = "https://lemonade-server.ai"; + license = licenses.asl20; + mainProgram = "lemonade-server"; + platforms = platforms.linux; + maintainers = [ ]; + }; +} diff --git a/systems/x86_64-linux/jallen-nas/apps.nix b/systems/x86_64-linux/jallen-nas/apps.nix index c21d249..9d92362 100755 --- a/systems/x86_64-linux/jallen-nas/apps.nix +++ b/systems/x86_64-linux/jallen-nas/apps.nix @@ -132,6 +132,12 @@ in port = 2283; reverseProxy = enabled; }; + lemonade = { + enable = true; + port = 8001; + modelsDir = "/media/nas/main/ai/lemonade/models"; + reverseProxy = disabled; + }; jellyfin = { enable = true; port = 8096; diff --git a/systems/x86_64-linux/jallen-nas/nas-defaults.nix b/systems/x86_64-linux/jallen-nas/nas-defaults.nix index f348525..1f2c314 100644 --- a/systems/x86_64-linux/jallen-nas/nas-defaults.nix +++ b/systems/x86_64-linux/jallen-nas/nas-defaults.nix @@ -56,6 +56,7 @@ in "headscale" "immich" "jellyfin" + "lemonade" "jellyseerr" "lubelogger" "manyfold"