Refactor llm config
This commit is contained in:
@@ -17,8 +17,7 @@
|
||||
./dev.nix
|
||||
./nvim.nix
|
||||
./task.nix
|
||||
./opencode.nix
|
||||
./pi.nix
|
||||
./llm.nix
|
||||
];
|
||||
|
||||
config = {
|
||||
|
||||
@@ -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
124
modules/home/llm.nix
Normal 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";
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -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
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
'';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user