Kieran's opinionated (and probably slightly dumb) nix config

feat: vendor the crush options till they get merge upstream

dunkirk.sh b3aa3e92 b8394907

verified
Changed files
+404 -67
home-manager
+43 -22
flake.lock
···
"type": "github"
}
},
-
"crush": {
-
"inputs": {
-
"nixpkgs": [
-
"nixpkgs"
-
]
-
},
-
"locked": {
-
"lastModified": 1753380422,
-
"narHash": "sha256-olzhQJVVBfH+ooeTcNF9jK/T1iHac3+lu/Stu9Sqhlg=",
-
"ref": "taciturnaxoltol/flake",
-
"rev": "0ccf3d9e6935f7ac9e213707583dcb3e18effc0b",
-
"revCount": 944,
-
"type": "git",
-
"url": "ssh://git@github.com/charmbracelet/crush"
-
},
-
"original": {
-
"ref": "taciturnaxoltol/flake",
-
"type": "git",
-
"url": "ssh://git@github.com/charmbracelet/crush"
-
}
-
},
"ctfd-alerts": {
"inputs": {
"nixpkgs": [
···
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd",
+
"type": "github"
+
},
+
"original": {
+
"owner": "hercules-ci",
+
"repo": "flake-parts",
+
"type": "github"
+
}
+
},
+
"flake-parts_3": {
+
"inputs": {
+
"nixpkgs-lib": [
+
"nur",
+
"nixpkgs"
+
]
+
},
+
"locked": {
+
"lastModified": 1733312601,
+
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
+
"owner": "hercules-ci",
+
"repo": "flake-parts",
+
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
···
"type": "github"
}
},
+
"nur": {
+
"inputs": {
+
"flake-parts": "flake-parts_3",
+
"nixpkgs": [
+
"nixpkgs"
+
]
+
},
+
"locked": {
+
"lastModified": 1754684884,
+
"narHash": "sha256-GH+UMIOJj7u/bW55dOOpD8HpVpc9WfU61iweM2nM68A=",
+
"owner": "nix-community",
+
"repo": "NUR",
+
"rev": "a7f9761c9dd71359cd9a6529078302a83e6deaac",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nix-community",
+
"repo": "NUR",
+
"type": "github"
+
}
+
},
"nuschtosSearch": {
"inputs": {
"flake-utils": "flake-utils_7",
···
"catppuccin": "catppuccin",
"catppuccin-vsc": "catppuccin-vsc",
"claude-desktop": "claude-desktop",
-
"crush": "crush",
"ctfd-alerts": "ctfd-alerts",
"disko": "disko",
"flare": "flare",
···
"nixpkgs": "nixpkgs_5",
"nixpkgs-unstable": "nixpkgs-unstable",
"nixvim": "nixvim",
+
"nur": "nur",
"spicetify-nix": "spicetify-nix",
"terminal-wakatime": "terminal-wakatime",
"zed": "zed"
+7 -5
flake.nix
···
inputs.nixpkgs.follows = "nixpkgs";
};
-
crush = {
-
url = "git+ssh://git@github.com/charmbracelet/crush?ref=taciturnaxoltol/flake";
-
inputs.nixpkgs.follows = "nixpkgs";
-
};
-
flare = {
url = "github:ByteAtATime/flare/feat/nix";
inputs.nixpkgs.follows = "nixpkgs";
};
import-tree.url = "github:vic/import-tree";
+
+
nur = {
+
url = "github:nix-community/NUR";
+
inputs.nixpkgs.follows = "nixpkgs";
+
};
};
outputs =
···
lix-module,
agenix,
home-manager,
+
nur,
...
}@inputs:
let
···
unstable-overlays
{ nixpkgs.hostPlatform = "x86_64-linux"; }
./nixos/machines/moonlark/configuration.nix
+
nur.modules.nixos.default
];
};
};
+301
home-manager/modules/apps/_crush-options.nix
···
+
{ lib }:
+
lib.mkOption {
+
type = lib.types.submodule {
+
options = {
+
providers = lib.mkOption {
+
type = lib.types.attrsOf (
+
lib.types.submodule {
+
options = {
+
name = lib.mkOption {
+
type = lib.types.str;
+
description = "Human-readable name for the provider";
+
};
+
base_url = lib.mkOption {
+
type = lib.types.str;
+
default = "";
+
description = "Base URL for the provider's API";
+
};
+
type = lib.mkOption {
+
type = lib.types.enum [
+
"openai"
+
"anthropic"
+
"gemini"
+
"azure"
+
"vertexai"
+
];
+
default = "openai";
+
description = "Provider type that determines the API format";
+
};
+
api_key = lib.mkOption {
+
type = lib.types.str;
+
default = "";
+
description = "API key for authentication with the provider";
+
};
+
disable = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
description = "Whether this provider is disabled";
+
};
+
system_prompt_prefix = lib.mkOption {
+
type = lib.types.str;
+
default = "";
+
description = "Custom prefix to add to system prompts for this provider";
+
};
+
extra_headers = lib.mkOption {
+
type = lib.types.attrsOf lib.types.str;
+
default = { };
+
description = "Additional HTTP headers to send with requests";
+
};
+
extra_body = lib.mkOption {
+
type = lib.types.attrsOf lib.types.anything;
+
default = { };
+
description = "Additional fields to include in request bodies";
+
};
+
models = lib.mkOption {
+
type = lib.types.listOf (
+
lib.types.submodule {
+
options = {
+
id = lib.mkOption {
+
type = lib.types.str;
+
description = "Model ID";
+
};
+
name = lib.mkOption {
+
type = lib.types.str;
+
description = "Model display name";
+
};
+
cost_per_1m_in = lib.mkOption {
+
type = lib.types.number;
+
default = 0;
+
};
+
cost_per_1m_out = lib.mkOption {
+
type = lib.types.number;
+
default = 0;
+
};
+
cost_per_1m_in_cached = lib.mkOption {
+
type = lib.types.number;
+
default = 0;
+
};
+
cost_per_1m_out_cached = lib.mkOption {
+
type = lib.types.number;
+
default = 0;
+
};
+
context_window = lib.mkOption {
+
type = lib.types.int;
+
default = 128000;
+
};
+
default_max_tokens = lib.mkOption {
+
type = lib.types.int;
+
default = 8192;
+
};
+
can_reason = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
};
+
has_reasoning_efforts = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
};
+
default_reasoning_effort = lib.mkOption {
+
type = lib.types.str;
+
default = "";
+
};
+
supports_attachments = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
};
+
};
+
}
+
);
+
default = [ ];
+
description = "List of models available from this provider";
+
};
+
};
+
}
+
);
+
default = { };
+
description = "AI provider configurations";
+
};
+
+
lsp = lib.mkOption {
+
type = lib.types.attrsOf (
+
lib.types.submodule {
+
options = {
+
command = lib.mkOption {
+
type = lib.types.str;
+
description = "Command to execute for the LSP server";
+
};
+
args = lib.mkOption {
+
type = lib.types.listOf lib.types.str;
+
default = [ ];
+
description = "Arguments to pass to the LSP server command";
+
};
+
options = lib.mkOption {
+
type = lib.types.attrsOf lib.types.anything;
+
default = { };
+
description = "LSP server-specific configuration options";
+
};
+
enabled = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
description = "Whether this LSP server is disabled";
+
};
+
};
+
}
+
);
+
default = { };
+
description = "Language Server Protocol configurations";
+
};
+
+
mcp = lib.mkOption {
+
type = lib.types.attrsOf (
+
lib.types.submodule {
+
options = {
+
command = lib.mkOption {
+
type = lib.types.str;
+
default = "";
+
description = "Command to execute for stdio MCP servers";
+
};
+
env = lib.mkOption {
+
type = lib.types.attrsOf lib.types.str;
+
default = { };
+
description = "Environment variables to set for the MCP server";
+
};
+
args = lib.mkOption {
+
type = lib.types.listOf lib.types.str;
+
default = [ ];
+
description = "Arguments to pass to the MCP server command";
+
};
+
type = lib.mkOption {
+
type = lib.types.enum [
+
"stdio"
+
"sse"
+
"http"
+
];
+
default = "stdio";
+
description = "Type of MCP connection";
+
};
+
url = lib.mkOption {
+
type = lib.types.str;
+
default = "";
+
description = "URL for HTTP or SSE MCP servers";
+
};
+
disabled = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
description = "Whether this MCP server is disabled";
+
};
+
headers = lib.mkOption {
+
type = lib.types.attrsOf lib.types.str;
+
default = { };
+
description = "HTTP headers for HTTP/SSE MCP servers";
+
};
+
};
+
}
+
);
+
default = { };
+
description = "Model Context Protocol server configurations";
+
};
+
+
options = lib.mkOption {
+
type = lib.types.submodule {
+
options = {
+
context_paths = lib.mkOption {
+
type = lib.types.listOf lib.types.str;
+
default = [ ];
+
description = "Paths to files containing context information for the AI";
+
};
+
tui = lib.mkOption {
+
type = lib.types.submodule {
+
options = {
+
compact_mode = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
description = "Enable compact mode for the TUI interface";
+
};
+
};
+
};
+
default = { };
+
description = "Terminal user interface options";
+
};
+
debug = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
description = "Enable debug logging";
+
};
+
debug_lsp = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
description = "Enable debug logging for LSP servers";
+
};
+
disable_auto_summarize = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
description = "Disable automatic conversation summarization";
+
};
+
data_directory = lib.mkOption {
+
type = lib.types.str;
+
default = ".crush";
+
description = "Directory for storing application data (relative to working directory)";
+
};
+
};
+
};
+
default = { };
+
description = "General application options";
+
};
+
+
permissions = lib.mkOption {
+
type = lib.types.submodule {
+
options = {
+
allowed_tools = lib.mkOption {
+
type = lib.types.listOf lib.types.str;
+
default = [ ];
+
description = "List of tools that don't require permission prompts";
+
};
+
};
+
};
+
default = { };
+
description = "Permission settings for tool usage";
+
};
+
+
models = lib.mkOption {
+
type = lib.types.attrsOf (
+
lib.types.submodule {
+
options = {
+
model = lib.mkOption {
+
type = lib.types.str;
+
description = "The model ID as used by the provider API";
+
};
+
provider = lib.mkOption {
+
type = lib.types.str;
+
description = "The model provider ID that matches a key in the providers config";
+
};
+
reasoning_effort = lib.mkOption {
+
type = lib.types.enum [
+
"low"
+
"medium"
+
"high"
+
];
+
default = "";
+
description = "Reasoning effort level for OpenAI models that support it";
+
};
+
max_tokens = lib.mkOption {
+
type = lib.types.int;
+
default = 0;
+
description = "Maximum number of tokens for model responses";
+
};
+
think = lib.mkOption {
+
type = lib.types.bool;
+
default = false;
+
description = "Enable thinking mode for Anthropic models that support reasoning";
+
};
+
};
+
}
+
);
+
default = { };
+
description = "Model configurations";
+
};
+
};
+
};
+
default = { };
+
description = "Crush configuration options";
+
}
+28
home-manager/modules/apps/crush-module.nix
···
+
{
+
config,
+
lib,
+
pkgs,
+
inputs,
+
...
+
}:
+
{
+
imports = [
+
inputs.nur.modules.homeManager.default
+
];
+
+
options.programs.crush = {
+
enable = lib.mkEnableOption "Enable crush";
+
settings = import ./_crush-options.nix { inherit lib; };
+
};
+
+
config = lib.mkIf config.programs.crush.enable {
+
home.packages = [ pkgs.nur.repos.charmbracelet.crush ];
+
home.file.".config/crush/crush.json" = lib.mkIf (config.programs.crush.settings != { }) {
+
text = builtins.toJSON config.programs.crush.settings;
+
};
+
+
# Optionally, add config file sources if needed
+
xdg.configFile."crush/copilot.sh".source = ../../dots/copilot.sh;
+
xdg.configFile."crush/anthropic.sh".source = ../../dots/anthropic.sh;
+
};
+
}
+25 -40
home-manager/modules/apps/crush.nix
···
{
lib,
config,
-
inputs,
...
}:
{
-
imports = [
-
inputs.crush.homeManagerModules.default
-
];
-
options.dots.apps.crush.enable = lib.mkEnableOption "Enable Crush config";
config = lib.mkIf config.dots.apps.crush.enable {
programs.crush = {
···
];
};
};
-
models = {
-
large = {
-
model = "claude-3.7-sonnet";
-
provider = "copilot";
-
};
-
small = {
-
model = "gemini-2.0-flash-001";
-
provider = "copilot";
-
};
-
};
providers = {
copilot = {
name = "Copilot";
···
models = [
{
id = "gpt-4.1";
-
model = "Copilot: GPT 4.1";
+
name = "Copilot: GPT 4.1";
cost_per_1m_in = 0;
cost_per_1m_out = 0;
cost_per_1m_in_cached = 0;
···
}
{
id = "gpt-4o";
-
model = "Copilot: GPT 4o";
+
name = "Copilot: GPT 4o";
cost_per_1m_in = 0;
cost_per_1m_out = 0;
cost_per_1m_in_cached = 0;
···
}
{
id = "claude-sonnet-4";
-
model = "Copilot: Claude Sonnet 4";
+
name = "Copilot: Claude Sonnet 4";
cost_per_1m_in = 0;
cost_per_1m_out = 0;
cost_per_1m_in_cached = 0;
···
}
{
id = "gemini-2.5-pro";
-
model = "Gemini 2.5 Pro";
+
name = "Gemini 2.5 Pro";
cost_per_1m_in = 0;
cost_per_1m_out = 0;
cost_per_1m_in_cached = 0;
···
models = [
{
id = "claude-opus-4-20250514";
-
model = "Claude Opus 4";
-
cost_per_1m_in = 15000;
-
cost_per_1m_out = 75000;
-
cost_per_1m_in_cached = 1125;
-
cost_per_1m_out_cached = 75000;
+
name = "Claude Opus 4";
+
cost_per_1m_in = 15.0;
+
cost_per_1m_out = 75.0;
+
cost_per_1m_in_cached = 1.5;
+
cost_per_1m_out_cached = 75.0;
context_window = 200000;
default_max_tokens = 50000;
can_reason = true;
···
}
{
id = "claude-sonnet-4-20250514";
-
model = "Claude Sonnet 4";
+
name = "Claude Sonnet 4";
cost_per_1m_in = 3000;
cost_per_1m_out = 15000;
cost_per_1m_in_cached = 225;
···
}
{
id = "claude-3-7-sonnet-20250219";
-
model = "Claude 3.7 Sonnet";
-
cost_per_1m_in = 2500;
-
cost_per_1m_out = 12000;
-
cost_per_1m_in_cached = 187;
-
cost_per_1m_out_cached = 12000;
+
name = "Claude 3.7 Sonnet";
+
cost_per_1m_in = 2.5;
+
cost_per_1m_out = 12.0;
+
cost_per_1m_in_cached = 0.187;
+
cost_per_1m_out_cached = 12.0;
context_window = 200000;
default_max_tokens = 128000;
can_reason = true;
···
}
{
id = "claude-3-5-sonnet-20241022";
-
model = "Claude 3.5 Sonnet (Latest)";
-
cost_per_1m_in = 3000;
-
cost_per_1m_out = 15000;
-
cost_per_1m_in_cached = 225;
-
cost_per_1m_out_cached = 15000;
+
name = "Claude 3.5 Sonnet (Latest)";
+
cost_per_1m_in = 3.0;
+
cost_per_1m_out = 15.0;
+
cost_per_1m_in_cached = 0.225;
+
cost_per_1m_out_cached = 15.0;
context_window = 200000;
default_max_tokens = 8192;
can_reason = false;
···
}
{
id = "claude-3-5-haiku-20241022";
-
model = "Claude 3.5 Haiku";
-
cost_per_1m_in = 800;
-
cost_per_1m_out = 4000;
-
cost_per_1m_in_cached = 60;
-
cost_per_1m_out_cached = 4000;
+
name = "Claude 3.5 Haiku";
+
cost_per_1m_in = 0.8;
+
cost_per_1m_out = 4.0;
+
cost_per_1m_in_cached = 0.06;
+
cost_per_1m_out_cached = 4.0;
context_window = 200000;
default_max_tokens = 8192;
can_reason = false;