lemonade
This commit is contained in:
146
modules/nixos/services/lemonade/default.nix
Normal file
146
modules/nixos/services/lemonade/default.nix
Normal 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 ];
|
||||
}
|
||||
136
packages/lemonade/default.nix
Normal file
136
packages/lemonade/default.nix
Normal 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 = [ ];
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -56,6 +56,7 @@ in
|
||||
"headscale"
|
||||
"immich"
|
||||
"jellyfin"
|
||||
"lemonade"
|
||||
"jellyseerr"
|
||||
"lubelogger"
|
||||
"manyfold"
|
||||
|
||||
Reference in New Issue
Block a user