Refactor llm config

This commit is contained in:
Martin Pander
2026-07-31 09:31:20 +02:00
parent b9899ef8ea
commit 40223d29dc
10 changed files with 310 additions and 228 deletions

View File

@@ -17,8 +17,7 @@
./dev.nix
./nvim.nix
./task.nix
./opencode.nix
./pi.nix
./llm.nix
];
config = {

View File

@@ -1,102 +1,28 @@
{ config, pkgs, lib, inputs, ... }:
{ pkgs, ... }:
let
cfg = config.dot.llm;
in
{
options.dot.llm = {
enable = lib.mkEnableOption "LLM tools";
claude-code.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable claude-code";
};
opencode.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable opencode";
};
gemini-cli.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable gemini-cli";
};
pi.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable pi";
};
pi.workMode = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Apply work machine (WSL NixOS) specific pi patches";
};
bubblewrap.enable = lib.mkOption {
type = lib.types.bool;
default = pkgs.stdenv.isLinux;
description = "Enable bubblewrap (Linux only)";
config = {
programs.direnv = {
enable = true;
enableZshIntegration = true;
nix-direnv.enable = true;
stdlib = ''
: ''${XDG_CACHE_HOME:=$HOME/.cache}
declare -A direnv_layout_dirs
direnv_layout_dir() {
echo "''${direnv_layout_dirs[$PWD]:=$(
echo -n "$XDG_CACHE_HOME"/direnv/layouts/
echo -n "$PWD" | sha1sum | cut -d ' ' -f 1
)}"
}
'';
};
home.packages = with pkgs; [
visidata
codespelunker
jq
unstable.tuicr
];
};
config = lib.mkMerge [
{
programs.direnv = {
enable = true;
enableZshIntegration = true;
nix-direnv.enable = true;
stdlib = ''
: ''${XDG_CACHE_HOME:=$HOME/.cache}
declare -A direnv_layout_dirs
direnv_layout_dir() {
echo "''${direnv_layout_dirs[$PWD]:=$(
echo -n "$XDG_CACHE_HOME"/direnv/layouts/
echo -n "$PWD" | sha1sum | cut -d ' ' -f 1
)}"
}
'';
};
home.packages = with pkgs; [
visidata
codespelunker
jq
unstable.tuicr
];
}
(lib.mkIf cfg.enable {
home.packages =
(lib.optional cfg.claude-code.enable pkgs.unstable.claude-code) ++
(lib.optional cfg.opencode.enable pkgs.unstable.opencode) ++
(lib.optional cfg.gemini-cli.enable pkgs.unstable.gemini-cli) ++
(lib.optional cfg.pi.enable (
let
pi = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.pi;
in
if cfg.pi.workMode then
# Work machine runs WSL NixOS, which needs the checks disabled and the
# Bun binary's ELF interpreter patched to the correct dynamic linker.
pi.overrideAttrs (oldAttrs: {
doInstallCheck = false;
# Run patchelf after the package is installed/fixed up by nix
postFixup = (oldAttrs.postFixup or "") + ''
# 1. Target the actual Bun binary inside libexec, not the shell wrapper in bin/
TARGET_BIN="$out/libexec/pi/pi"
if [ -f "$TARGET_BIN" ]; then
# 2. Make it writable in the build store so patchelf can edit it
chmod +w "$TARGET_BIN"
# 3. Running patchelf to set the interpreter forces it to rebuild the ELF headers
patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" "$TARGET_BIN"
fi
'';
})
else
pi
)) ++
(lib.optional (cfg.bubblewrap.enable && pkgs.stdenv.isLinux) pkgs.unstable.bubblewrap);
})
];
}

124
modules/home/llm.nix Normal file
View File

@@ -0,0 +1,124 @@
{ config, pkgs, lib, inputs, ... }:
let
cfg = config.dot.llm;
piPath = "${config.dot.dotfilesPath}/modules/pi/agent";
opencodePath = "${config.dot.dotfilesPath}/modules/opencode";
in
{
options.dot.llm = {
enable = lib.mkEnableOption "LLM tools";
claude-code.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable claude-code";
};
gemini-cli.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable gemini-cli";
};
bubblewrap.enable = lib.mkOption {
type = lib.types.bool;
default = pkgs.stdenv.isLinux;
description = "Enable bubblewrap (Linux only)";
};
opencode = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable opencode";
};
enableConfig = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable managed OpenCode configuration";
};
workMode = lib.mkEnableOption "work-specific opencode configuration";
};
pi = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable pi";
};
enableConfig = lib.mkEnableOption "managed pi coding agent configuration" // {
default = true;
};
workMode = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Apply work machine (WSL NixOS) specific pi patches";
};
};
};
config = lib.mkMerge [
(lib.mkIf cfg.enable {
home.packages =
(lib.optional cfg.claude-code.enable pkgs.unstable.claude-code) ++
(lib.optional cfg.opencode.enable pkgs.unstable.opencode) ++
(lib.optional cfg.gemini-cli.enable pkgs.unstable.gemini-cli) ++
(lib.optional cfg.pi.enable (
let
pi = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system}.pi;
in
if cfg.pi.workMode then
# Work machine runs WSL NixOS, which needs the checks disabled and the
# Bun binary's ELF interpreter patched to the correct dynamic linker.
pi.overrideAttrs (oldAttrs: {
doInstallCheck = false;
# Run patchelf after the package is installed/fixed up by nix
postFixup = (oldAttrs.postFixup or "") + ''
# 1. Target the actual Bun binary inside libexec, not the shell wrapper in bin/
TARGET_BIN="$out/libexec/pi/pi"
if [ -f "$TARGET_BIN" ]; then
# 2. Make it writable in the build store so patchelf can edit it
chmod +w "$TARGET_BIN"
# 3. Running patchelf to set the interpreter forces it to rebuild the ELF headers
patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" "$TARGET_BIN"
fi
'';
})
else
pi
)) ++
(lib.optional (cfg.bubblewrap.enable && pkgs.stdenv.isLinux) pkgs.unstable.bubblewrap);
})
# pi coding agent configuration
(lib.mkIf cfg.pi.enableConfig {
home.file = {
".pi/agent/settings.json".source =
config.lib.file.mkOutOfStoreSymlink
"${piPath}/${if cfg.pi.workMode then "settings.work" else "settings.home"}.json";
".pi/agent/extensions".source =
config.lib.file.mkOutOfStoreSymlink "${piPath}/extensions-common";
} // lib.optionalAttrs cfg.pi.workMode {
".pi/agent/extensions-work".source =
config.lib.file.mkOutOfStoreSymlink "${piPath}/extensions-work";
".pi/agent/models.json".source =
config.lib.file.mkOutOfStoreSymlink "${piPath}/models.work.json";
};
})
# OpenCode configuration
(lib.mkIf cfg.opencode.enableConfig {
xdg.configFile."opencode/opencode.json".source =
config.lib.file.mkOutOfStoreSymlink
"${opencodePath}/opencode.${if cfg.opencode.workMode then "work" else "home"}.json";
})
];
}

View File

@@ -1,99 +0,0 @@
{ config, lib, inputs, ... }:
let
cfg = config.dot.llm.opencode;
in
{
options.dot.llm.opencode = {
enableConfig = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable managed OpenCode configuration";
};
workMode = lib.mkEnableOption "work-specific opencode configuration";
};
config = lib.mkIf cfg.enableConfig {
# Declare the sops secret
sops.secrets.langdock_api_key = {
# Uses defaultSopsFile from secrets.nix
};
# Create the OpenCode config.json template with secret substitution
sops.templates."opencode/opencode.json" = {
content = builtins.toJSON (let
baseConfig = {
"$schema" = "https://opencode.ai/config.json";
default_agent = "plan";
};
workConfig = {
enabled_providers = [ "anthropic" "langdock-openai" "ollama" ];
model = "anthropic/claude-opus-4-6-default";
small_model = "anthropic/claude-haiku-4-5-20251001";
provider = {
langdock-openai = {
npm = "@ai-sdk/openai-compatible";
name = "Langdock OpenAI";
options = {
baseURL = "https://api.langdock.com/openai/eu/v1";
apiKey = config.sops.placeholder.langdock_api_key;
};
models = {
"gpt-5.6-sol" = { name = "GPT-5.6 Sol"; };
"gpt-5.6-terra" = { name = "GPT-5.6 Terra"; };
"gpt-5.6-luna" = { name = "GPT-5.6 Luna"; };
};
};
anthropic = {
options = {
baseURL = "https://api.langdock.com/anthropic/eu/v1";
apiKey = config.sops.placeholder.langdock_api_key;
};
models = {
"claude-opus-5-default" = { name = "Opus 5"; };
"claude-opus-4-8-default" = { name = "Opus 4.8"; };
"claude-sonnet-5-default" = { name = "Sonnet 5"; };
};
whitelist = ["claude-opus-4-8-default" "claude-opus-5-default" "claude-sonnet-5-default"];
};
# ollama = {
# npm = "@ai-sdk/openai-compatible";
# name = "Ollama (Local)";
# options = {
# baseURL = "http://192.168.11.23:11434/v1";
# };
# models = {
# "qwen3-coder:30b" = { name = "Qwen 3 Coder 30B"; };
# "codellama:34b-instruct" = { name = "CodeLlama 34B Instruct"; };
# };
# };
};
};
personalConfig = {
plugin = [ "opencode-gemini-auth@latest" ];
model = "google/gemini-3-pro-preview";
small_model = "google/gemini-3-flash-preview";
enabled_providers = [ "google" ];
};
in
# Merge base config with the selected mode config
baseConfig // (if cfg.workMode then workConfig else personalConfig)
);
};
# Link the generated config to the expected location
xdg.configFile."opencode/opencode.json".source = config.lib.file.mkOutOfStoreSymlink config.sops.templates."opencode/opencode.json".path;
# Export the decrypted langdock API key as an env var (work config only).
# The secret lives in a runtime file, so we read it at shell startup rather
# than baking it into the Nix store via home.sessionVariables.
programs.zsh.initContent = lib.mkIf cfg.workMode ''
if [ -r "${config.sops.secrets.langdock_api_key.path}" ]; then
export LANGDOCK_API_KEY="$(cat "${config.sops.secrets.langdock_api_key.path}")"
fi
'';
};
}

View File

@@ -1,27 +0,0 @@
{ config, lib, ... }:
let
cfg = config.dot.llm.pi;
piPath = "${config.dot.dotfilesPath}/modules/pi/agent";
piWorkMode = config.dot.llm.pi.workMode or false;
in
{
options.dot.llm.pi = {
enableConfig = lib.mkEnableOption "managed pi coding agent configuration" // {
default = true;
};
};
config = lib.mkIf cfg.enableConfig {
home.file = {
".pi/agent/settings.json".source =
config.lib.file.mkOutOfStoreSymlink
"${piPath}/${if piWorkMode then "settings.work" else "settings.home"}.json";
".pi/agent/extensions".source =
config.lib.file.mkOutOfStoreSymlink "${piPath}/extensions-common";
} // lib.optionalAttrs piWorkMode {
".pi/agent/extensions-work".source =
config.lib.file.mkOutOfStoreSymlink "${piPath}/extensions-work";
};
};
}

View File

@@ -1,5 +1,11 @@
{ inputs, config, ... }:
{ inputs, config, lib, ... }:
let
# Both pi and opencode talk to Langdock on work machines and expect the API
# key in the environment, so the secret is managed here rather than in the
# individual agent modules.
needsLangdock = config.dot.llm.pi.workMode || config.dot.llm.opencode.workMode;
in
{
imports = [
inputs.sops-nix.homeManagerModules.sops
@@ -12,9 +18,24 @@
age = {
keyFile = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
};
secrets = lib.mkIf needsLangdock {
langdock_api_key = {
# Uses defaultSopsFile from secrets.nix
};
};
};
home.sessionVariables = {
SOPS_AGE_KEY_FILE = config.sops.age.keyFile;
};
# Export the decrypted langdock API key as an env var. The secret lives in a
# runtime file, so it is read at shell startup rather than baked into the Nix
# store via home.sessionVariables.
programs.zsh.initContent = lib.mkIf needsLangdock ''
if [ -r "${config.sops.secrets.langdock_api_key.path}" ]; then
export LANGDOCK_API_KEY="$(cat "${config.sops.secrets.langdock_api_key.path}")"
fi
'';
}