This commit is contained in:
mjallen18
2026-03-25 19:59:49 -05:00
parent 7fcbd0bb7c
commit 2013804b17
4 changed files with 289 additions and 0 deletions

View File

@@ -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 ];
}

View File

@@ -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:
# <exe_dir>/resources/<file> (first check, highest priority)
# /usr/share/lemonade-server/resources/<file> (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 = [ ];
};
}

View File

@@ -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;

View File

@@ -56,6 +56,7 @@ in
"headscale"
"immich"
"jellyfin"
"lemonade"
"jellyseerr"
"lubelogger"
"manyfold"