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

feat: vendor the crush options till they get merge upstream

dunkirk.sh b3aa3e92 b8394907

verified
+404 -67
+43 -22
flake.lock
··· 104 104 "type": "github" 105 105 } 106 106 }, 107 - "crush": { 108 - "inputs": { 109 - "nixpkgs": [ 110 - "nixpkgs" 111 - ] 112 - }, 113 - "locked": { 114 - "lastModified": 1753380422, 115 - "narHash": "sha256-olzhQJVVBfH+ooeTcNF9jK/T1iHac3+lu/Stu9Sqhlg=", 116 - "ref": "taciturnaxoltol/flake", 117 - "rev": "0ccf3d9e6935f7ac9e213707583dcb3e18effc0b", 118 - "revCount": 944, 119 - "type": "git", 120 - "url": "ssh://git@github.com/charmbracelet/crush" 121 - }, 122 - "original": { 123 - "ref": "taciturnaxoltol/flake", 124 - "type": "git", 125 - "url": "ssh://git@github.com/charmbracelet/crush" 126 - } 127 - }, 128 107 "ctfd-alerts": { 129 108 "inputs": { 130 109 "nixpkgs": [ ··· 251 230 "owner": "hercules-ci", 252 231 "repo": "flake-parts", 253 232 "rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd", 233 + "type": "github" 234 + }, 235 + "original": { 236 + "owner": "hercules-ci", 237 + "repo": "flake-parts", 238 + "type": "github" 239 + } 240 + }, 241 + "flake-parts_3": { 242 + "inputs": { 243 + "nixpkgs-lib": [ 244 + "nur", 245 + "nixpkgs" 246 + ] 247 + }, 248 + "locked": { 249 + "lastModified": 1733312601, 250 + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", 251 + "owner": "hercules-ci", 252 + "repo": "flake-parts", 253 + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", 254 254 "type": "github" 255 255 }, 256 256 "original": { ··· 883 883 "type": "github" 884 884 } 885 885 }, 886 + "nur": { 887 + "inputs": { 888 + "flake-parts": "flake-parts_3", 889 + "nixpkgs": [ 890 + "nixpkgs" 891 + ] 892 + }, 893 + "locked": { 894 + "lastModified": 1754684884, 895 + "narHash": "sha256-GH+UMIOJj7u/bW55dOOpD8HpVpc9WfU61iweM2nM68A=", 896 + "owner": "nix-community", 897 + "repo": "NUR", 898 + "rev": "a7f9761c9dd71359cd9a6529078302a83e6deaac", 899 + "type": "github" 900 + }, 901 + "original": { 902 + "owner": "nix-community", 903 + "repo": "NUR", 904 + "type": "github" 905 + } 906 + }, 886 907 "nuschtosSearch": { 887 908 "inputs": { 888 909 "flake-utils": "flake-utils_7", ··· 933 954 "catppuccin": "catppuccin", 934 955 "catppuccin-vsc": "catppuccin-vsc", 935 956 "claude-desktop": "claude-desktop", 936 - "crush": "crush", 937 957 "ctfd-alerts": "ctfd-alerts", 938 958 "disko": "disko", 939 959 "flare": "flare", ··· 948 968 "nixpkgs": "nixpkgs_5", 949 969 "nixpkgs-unstable": "nixpkgs-unstable", 950 970 "nixvim": "nixvim", 971 + "nur": "nur", 951 972 "spicetify-nix": "spicetify-nix", 952 973 "terminal-wakatime": "terminal-wakatime", 953 974 "zed": "zed"
+7 -5
flake.nix
··· 77 77 inputs.nixpkgs.follows = "nixpkgs"; 78 78 }; 79 79 80 - crush = { 81 - url = "git+ssh://git@github.com/charmbracelet/crush?ref=taciturnaxoltol/flake"; 82 - inputs.nixpkgs.follows = "nixpkgs"; 83 - }; 84 - 85 80 flare = { 86 81 url = "github:ByteAtATime/flare/feat/nix"; 87 82 inputs.nixpkgs.follows = "nixpkgs"; 88 83 }; 89 84 90 85 import-tree.url = "github:vic/import-tree"; 86 + 87 + nur = { 88 + url = "github:nix-community/NUR"; 89 + inputs.nixpkgs.follows = "nixpkgs"; 90 + }; 91 91 }; 92 92 93 93 outputs = ··· 98 98 lix-module, 99 99 agenix, 100 100 home-manager, 101 + nur, 101 102 ... 102 103 }@inputs: 103 104 let ··· 138 139 unstable-overlays 139 140 { nixpkgs.hostPlatform = "x86_64-linux"; } 140 141 ./nixos/machines/moonlark/configuration.nix 142 + nur.modules.nixos.default 141 143 ]; 142 144 }; 143 145 };
+301
home-manager/modules/apps/_crush-options.nix
··· 1 + { lib }: 2 + lib.mkOption { 3 + type = lib.types.submodule { 4 + options = { 5 + providers = lib.mkOption { 6 + type = lib.types.attrsOf ( 7 + lib.types.submodule { 8 + options = { 9 + name = lib.mkOption { 10 + type = lib.types.str; 11 + description = "Human-readable name for the provider"; 12 + }; 13 + base_url = lib.mkOption { 14 + type = lib.types.str; 15 + default = ""; 16 + description = "Base URL for the provider's API"; 17 + }; 18 + type = lib.mkOption { 19 + type = lib.types.enum [ 20 + "openai" 21 + "anthropic" 22 + "gemini" 23 + "azure" 24 + "vertexai" 25 + ]; 26 + default = "openai"; 27 + description = "Provider type that determines the API format"; 28 + }; 29 + api_key = lib.mkOption { 30 + type = lib.types.str; 31 + default = ""; 32 + description = "API key for authentication with the provider"; 33 + }; 34 + disable = lib.mkOption { 35 + type = lib.types.bool; 36 + default = false; 37 + description = "Whether this provider is disabled"; 38 + }; 39 + system_prompt_prefix = lib.mkOption { 40 + type = lib.types.str; 41 + default = ""; 42 + description = "Custom prefix to add to system prompts for this provider"; 43 + }; 44 + extra_headers = lib.mkOption { 45 + type = lib.types.attrsOf lib.types.str; 46 + default = { }; 47 + description = "Additional HTTP headers to send with requests"; 48 + }; 49 + extra_body = lib.mkOption { 50 + type = lib.types.attrsOf lib.types.anything; 51 + default = { }; 52 + description = "Additional fields to include in request bodies"; 53 + }; 54 + models = lib.mkOption { 55 + type = lib.types.listOf ( 56 + lib.types.submodule { 57 + options = { 58 + id = lib.mkOption { 59 + type = lib.types.str; 60 + description = "Model ID"; 61 + }; 62 + name = lib.mkOption { 63 + type = lib.types.str; 64 + description = "Model display name"; 65 + }; 66 + cost_per_1m_in = lib.mkOption { 67 + type = lib.types.number; 68 + default = 0; 69 + }; 70 + cost_per_1m_out = lib.mkOption { 71 + type = lib.types.number; 72 + default = 0; 73 + }; 74 + cost_per_1m_in_cached = lib.mkOption { 75 + type = lib.types.number; 76 + default = 0; 77 + }; 78 + cost_per_1m_out_cached = lib.mkOption { 79 + type = lib.types.number; 80 + default = 0; 81 + }; 82 + context_window = lib.mkOption { 83 + type = lib.types.int; 84 + default = 128000; 85 + }; 86 + default_max_tokens = lib.mkOption { 87 + type = lib.types.int; 88 + default = 8192; 89 + }; 90 + can_reason = lib.mkOption { 91 + type = lib.types.bool; 92 + default = false; 93 + }; 94 + has_reasoning_efforts = lib.mkOption { 95 + type = lib.types.bool; 96 + default = false; 97 + }; 98 + default_reasoning_effort = lib.mkOption { 99 + type = lib.types.str; 100 + default = ""; 101 + }; 102 + supports_attachments = lib.mkOption { 103 + type = lib.types.bool; 104 + default = false; 105 + }; 106 + }; 107 + } 108 + ); 109 + default = [ ]; 110 + description = "List of models available from this provider"; 111 + }; 112 + }; 113 + } 114 + ); 115 + default = { }; 116 + description = "AI provider configurations"; 117 + }; 118 + 119 + lsp = lib.mkOption { 120 + type = lib.types.attrsOf ( 121 + lib.types.submodule { 122 + options = { 123 + command = lib.mkOption { 124 + type = lib.types.str; 125 + description = "Command to execute for the LSP server"; 126 + }; 127 + args = lib.mkOption { 128 + type = lib.types.listOf lib.types.str; 129 + default = [ ]; 130 + description = "Arguments to pass to the LSP server command"; 131 + }; 132 + options = lib.mkOption { 133 + type = lib.types.attrsOf lib.types.anything; 134 + default = { }; 135 + description = "LSP server-specific configuration options"; 136 + }; 137 + enabled = lib.mkOption { 138 + type = lib.types.bool; 139 + default = false; 140 + description = "Whether this LSP server is disabled"; 141 + }; 142 + }; 143 + } 144 + ); 145 + default = { }; 146 + description = "Language Server Protocol configurations"; 147 + }; 148 + 149 + mcp = lib.mkOption { 150 + type = lib.types.attrsOf ( 151 + lib.types.submodule { 152 + options = { 153 + command = lib.mkOption { 154 + type = lib.types.str; 155 + default = ""; 156 + description = "Command to execute for stdio MCP servers"; 157 + }; 158 + env = lib.mkOption { 159 + type = lib.types.attrsOf lib.types.str; 160 + default = { }; 161 + description = "Environment variables to set for the MCP server"; 162 + }; 163 + args = lib.mkOption { 164 + type = lib.types.listOf lib.types.str; 165 + default = [ ]; 166 + description = "Arguments to pass to the MCP server command"; 167 + }; 168 + type = lib.mkOption { 169 + type = lib.types.enum [ 170 + "stdio" 171 + "sse" 172 + "http" 173 + ]; 174 + default = "stdio"; 175 + description = "Type of MCP connection"; 176 + }; 177 + url = lib.mkOption { 178 + type = lib.types.str; 179 + default = ""; 180 + description = "URL for HTTP or SSE MCP servers"; 181 + }; 182 + disabled = lib.mkOption { 183 + type = lib.types.bool; 184 + default = false; 185 + description = "Whether this MCP server is disabled"; 186 + }; 187 + headers = lib.mkOption { 188 + type = lib.types.attrsOf lib.types.str; 189 + default = { }; 190 + description = "HTTP headers for HTTP/SSE MCP servers"; 191 + }; 192 + }; 193 + } 194 + ); 195 + default = { }; 196 + description = "Model Context Protocol server configurations"; 197 + }; 198 + 199 + options = lib.mkOption { 200 + type = lib.types.submodule { 201 + options = { 202 + context_paths = lib.mkOption { 203 + type = lib.types.listOf lib.types.str; 204 + default = [ ]; 205 + description = "Paths to files containing context information for the AI"; 206 + }; 207 + tui = lib.mkOption { 208 + type = lib.types.submodule { 209 + options = { 210 + compact_mode = lib.mkOption { 211 + type = lib.types.bool; 212 + default = false; 213 + description = "Enable compact mode for the TUI interface"; 214 + }; 215 + }; 216 + }; 217 + default = { }; 218 + description = "Terminal user interface options"; 219 + }; 220 + debug = lib.mkOption { 221 + type = lib.types.bool; 222 + default = false; 223 + description = "Enable debug logging"; 224 + }; 225 + debug_lsp = lib.mkOption { 226 + type = lib.types.bool; 227 + default = false; 228 + description = "Enable debug logging for LSP servers"; 229 + }; 230 + disable_auto_summarize = lib.mkOption { 231 + type = lib.types.bool; 232 + default = false; 233 + description = "Disable automatic conversation summarization"; 234 + }; 235 + data_directory = lib.mkOption { 236 + type = lib.types.str; 237 + default = ".crush"; 238 + description = "Directory for storing application data (relative to working directory)"; 239 + }; 240 + }; 241 + }; 242 + default = { }; 243 + description = "General application options"; 244 + }; 245 + 246 + permissions = lib.mkOption { 247 + type = lib.types.submodule { 248 + options = { 249 + allowed_tools = lib.mkOption { 250 + type = lib.types.listOf lib.types.str; 251 + default = [ ]; 252 + description = "List of tools that don't require permission prompts"; 253 + }; 254 + }; 255 + }; 256 + default = { }; 257 + description = "Permission settings for tool usage"; 258 + }; 259 + 260 + models = lib.mkOption { 261 + type = lib.types.attrsOf ( 262 + lib.types.submodule { 263 + options = { 264 + model = lib.mkOption { 265 + type = lib.types.str; 266 + description = "The model ID as used by the provider API"; 267 + }; 268 + provider = lib.mkOption { 269 + type = lib.types.str; 270 + description = "The model provider ID that matches a key in the providers config"; 271 + }; 272 + reasoning_effort = lib.mkOption { 273 + type = lib.types.enum [ 274 + "low" 275 + "medium" 276 + "high" 277 + ]; 278 + default = ""; 279 + description = "Reasoning effort level for OpenAI models that support it"; 280 + }; 281 + max_tokens = lib.mkOption { 282 + type = lib.types.int; 283 + default = 0; 284 + description = "Maximum number of tokens for model responses"; 285 + }; 286 + think = lib.mkOption { 287 + type = lib.types.bool; 288 + default = false; 289 + description = "Enable thinking mode for Anthropic models that support reasoning"; 290 + }; 291 + }; 292 + } 293 + ); 294 + default = { }; 295 + description = "Model configurations"; 296 + }; 297 + }; 298 + }; 299 + default = { }; 300 + description = "Crush configuration options"; 301 + }
+28
home-manager/modules/apps/crush-module.nix
··· 1 + { 2 + config, 3 + lib, 4 + pkgs, 5 + inputs, 6 + ... 7 + }: 8 + { 9 + imports = [ 10 + inputs.nur.modules.homeManager.default 11 + ]; 12 + 13 + options.programs.crush = { 14 + enable = lib.mkEnableOption "Enable crush"; 15 + settings = import ./_crush-options.nix { inherit lib; }; 16 + }; 17 + 18 + config = lib.mkIf config.programs.crush.enable { 19 + home.packages = [ pkgs.nur.repos.charmbracelet.crush ]; 20 + home.file.".config/crush/crush.json" = lib.mkIf (config.programs.crush.settings != { }) { 21 + text = builtins.toJSON config.programs.crush.settings; 22 + }; 23 + 24 + # Optionally, add config file sources if needed 25 + xdg.configFile."crush/copilot.sh".source = ../../dots/copilot.sh; 26 + xdg.configFile."crush/anthropic.sh".source = ../../dots/anthropic.sh; 27 + }; 28 + }
+25 -40
home-manager/modules/apps/crush.nix
··· 1 1 { 2 2 lib, 3 3 config, 4 - inputs, 5 4 ... 6 5 }: 7 6 { 8 - imports = [ 9 - inputs.crush.homeManagerModules.default 10 - ]; 11 - 12 7 options.dots.apps.crush.enable = lib.mkEnableOption "Enable Crush config"; 13 8 config = lib.mkIf config.dots.apps.crush.enable { 14 9 programs.crush = { ··· 39 34 ]; 40 35 }; 41 36 }; 42 - models = { 43 - large = { 44 - model = "claude-3.7-sonnet"; 45 - provider = "copilot"; 46 - }; 47 - small = { 48 - model = "gemini-2.0-flash-001"; 49 - provider = "copilot"; 50 - }; 51 - }; 52 37 providers = { 53 38 copilot = { 54 39 name = "Copilot"; ··· 63 48 models = [ 64 49 { 65 50 id = "gpt-4.1"; 66 - model = "Copilot: GPT 4.1"; 51 + name = "Copilot: GPT 4.1"; 67 52 cost_per_1m_in = 0; 68 53 cost_per_1m_out = 0; 69 54 cost_per_1m_in_cached = 0; ··· 76 61 } 77 62 { 78 63 id = "gpt-4o"; 79 - model = "Copilot: GPT 4o"; 64 + name = "Copilot: GPT 4o"; 80 65 cost_per_1m_in = 0; 81 66 cost_per_1m_out = 0; 82 67 cost_per_1m_in_cached = 0; ··· 89 74 } 90 75 { 91 76 id = "claude-sonnet-4"; 92 - model = "Copilot: Claude Sonnet 4"; 77 + name = "Copilot: Claude Sonnet 4"; 93 78 cost_per_1m_in = 0; 94 79 cost_per_1m_out = 0; 95 80 cost_per_1m_in_cached = 0; ··· 102 87 } 103 88 { 104 89 id = "gemini-2.5-pro"; 105 - model = "Gemini 2.5 Pro"; 90 + name = "Gemini 2.5 Pro"; 106 91 cost_per_1m_in = 0; 107 92 cost_per_1m_out = 0; 108 93 cost_per_1m_in_cached = 0; ··· 128 113 models = [ 129 114 { 130 115 id = "claude-opus-4-20250514"; 131 - model = "Claude Opus 4"; 132 - cost_per_1m_in = 15000; 133 - cost_per_1m_out = 75000; 134 - cost_per_1m_in_cached = 1125; 135 - cost_per_1m_out_cached = 75000; 116 + name = "Claude Opus 4"; 117 + cost_per_1m_in = 15.0; 118 + cost_per_1m_out = 75.0; 119 + cost_per_1m_in_cached = 1.5; 120 + cost_per_1m_out_cached = 75.0; 136 121 context_window = 200000; 137 122 default_max_tokens = 50000; 138 123 can_reason = true; ··· 141 126 } 142 127 { 143 128 id = "claude-sonnet-4-20250514"; 144 - model = "Claude Sonnet 4"; 129 + name = "Claude Sonnet 4"; 145 130 cost_per_1m_in = 3000; 146 131 cost_per_1m_out = 15000; 147 132 cost_per_1m_in_cached = 225; ··· 154 139 } 155 140 { 156 141 id = "claude-3-7-sonnet-20250219"; 157 - model = "Claude 3.7 Sonnet"; 158 - cost_per_1m_in = 2500; 159 - cost_per_1m_out = 12000; 160 - cost_per_1m_in_cached = 187; 161 - cost_per_1m_out_cached = 12000; 142 + name = "Claude 3.7 Sonnet"; 143 + cost_per_1m_in = 2.5; 144 + cost_per_1m_out = 12.0; 145 + cost_per_1m_in_cached = 0.187; 146 + cost_per_1m_out_cached = 12.0; 162 147 context_window = 200000; 163 148 default_max_tokens = 128000; 164 149 can_reason = true; ··· 167 152 } 168 153 { 169 154 id = "claude-3-5-sonnet-20241022"; 170 - model = "Claude 3.5 Sonnet (Latest)"; 171 - cost_per_1m_in = 3000; 172 - cost_per_1m_out = 15000; 173 - cost_per_1m_in_cached = 225; 174 - cost_per_1m_out_cached = 15000; 155 + name = "Claude 3.5 Sonnet (Latest)"; 156 + cost_per_1m_in = 3.0; 157 + cost_per_1m_out = 15.0; 158 + cost_per_1m_in_cached = 0.225; 159 + cost_per_1m_out_cached = 15.0; 175 160 context_window = 200000; 176 161 default_max_tokens = 8192; 177 162 can_reason = false; ··· 180 165 } 181 166 { 182 167 id = "claude-3-5-haiku-20241022"; 183 - model = "Claude 3.5 Haiku"; 184 - cost_per_1m_in = 800; 185 - cost_per_1m_out = 4000; 186 - cost_per_1m_in_cached = 60; 187 - cost_per_1m_out_cached = 4000; 168 + name = "Claude 3.5 Haiku"; 169 + cost_per_1m_in = 0.8; 170 + cost_per_1m_out = 4.0; 171 + cost_per_1m_in_cached = 0.06; 172 + cost_per_1m_out_cached = 4.0; 188 173 context_window = 200000; 189 174 default_max_tokens = 8192; 190 175 can_reason = false;