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,45 +1,7 @@
{ 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 = lib.mkMerge [
{
config = {
programs.direnv = {
enable = true;
enableZshIntegration = true;
@@ -62,41 +24,5 @@ in
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
'';
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://opencode.ai/config.json",
"default_agent": "plan",
"plugin": [
"opencode-gemini-auth@latest"
],
"model": "google/gemini-3-pro-preview",
"small_model": "google/gemini-3-flash-preview",
"enabled_providers": [
"google"
]
}

View File

@@ -0,0 +1,75 @@
{
"$schema": "https://opencode.ai/config.json",
"default_agent": "plan",
"enabled_providers": [
"anthropic",
"langdock-openai",
"sparkr",
"sparkl"
],
"model": "anthropic/claude-opus-5-default",
"small_model": "anthropic/claude-sonnet-5-default",
"provider": {
"sparkr": {
"npm": "@ai-sdk/openai-compatible",
"name": "sparkr",
"options": {
"baseURL": "http://192.168.10.110:8000/v1",
"apiKey": "vllm"
},
"models": {
"Qwen/Qwen3-Coder-Next-FP8": {
"name": "Qwen3 Coder Next FP8 (local vLLM)",
"tool_call": true,
"reasoning": false,
"limit": { "context": 262144, "output": 8192 }
}
}
},
"sparkl": {
"npm": "@ai-sdk/openai-compatible",
"name": "sparkl",
"options": {
"baseURL": "http://192.168.10.111:8000/v1",
"apiKey": "vllm"
},
"models": {
"Qwen/Qwen3-30B-A3B": {
"name": "Qwen3 30B A3B (local vLLM)",
"tool_call": true,
"reasoning": true,
"limit": { "context": 16384, "output": 8192 }
}
}
},
"langdock-openai": {
"npm": "@ai-sdk/openai-compatible",
"name": "Langdock OpenAI",
"options": {
"baseURL": "https://api.langdock.com/openai/eu/v1",
"apiKey": "{env: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": "{env: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"
]
}
}
}

View File

@@ -0,0 +1,51 @@
{
"providers": {
"sparkl": {
"baseUrl": "http://192.168.10.111:8000/v1",
"api": "openai-completions",
"apiKey": "vllm",
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": false
},
"models": [
{
"id": "Qwen/Qwen3-30B-A3B",
"name": "Qwen3 30B A3B (local vLLM)",
"reasoning": true,
"input": ["text"],
"contextWindow": 16384,
"maxTokens": 8192,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"compat": {
"thinkingFormat": "qwen-chat-template",
"maxTokensField": "max_tokens"
}
}
]
},
"sparkr": {
"baseUrl": "http://192.168.10.110:8000/v1",
"api": "openai-completions",
"apiKey": "vllm",
"compat": {
"supportsDeveloperRole": false,
"supportsReasoningEffort": false
},
"models": [
{
"id": "Qwen/Qwen3-Coder-Next-FP8",
"name": "Qwen3 Coder Next FP8 (local vLLM)",
"reasoning": false,
"input": ["text"],
"contextWindow": 262144,
"maxTokens": 8192,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
"compat": {
"maxTokensField": "max_tokens"
}
}
]
}
}
}

View File

@@ -1,8 +1,8 @@
{
"lastChangelogVersion": "0.81.1",
"theme": "light",
"defaultProvider": "anthropic",
"defaultModel": "claude-opus-5-default",
"defaultProvider": "sparkr",
"defaultModel": "Qwen/Qwen3-Coder-Next-FP8",
"defaultThinkingLevel": "high",
"hideThinkingBlock": false,
"retry": {