at master 7.3 kB view raw
1{ 2 lib, 3 runCommand, 4 callPackage, 5 buildPythonPackage, 6 fetchFromGitHub, 7 pytestCheckHook, 8 pythonOlder, 9 replaceVars, 10 setuptools, 11 click-default-group, 12 condense-json, 13 numpy, 14 openai, 15 pip, 16 pluggy, 17 puremagic, 18 pydantic, 19 python, 20 python-ulid, 21 pyyaml, 22 sqlite-migrate, 23 cogapp, 24 pytest-asyncio, 25 pytest-httpx, 26 pytest-recording, 27 sqlite-utils, 28 syrupy, 29 llm-echo, 30}: 31let 32 /** 33 Make a derivation for `llm` that contains `llm` plus the relevant plugins. 34 The function signature of `withPlugins` is the list of all the plugins `llm` knows about. 35 Adding a parameter here requires that it be in `python3Packages` attrset. 36 37 # Type 38 39 ``` 40 withPlugins :: 41 { 42 llm-anthropic :: bool, 43 llm-gemini :: bool, 44 ... 45 } 46 -> derivation 47 ``` 48 49 See `lib.attrNames (lib.functionArgs llm.withPlugins)` for the total list of plugins supported. 50 51 # Examples 52 :::{.example} 53 ## `llm.withPlugins` usage example 54 55 ```nix 56 llm.withPlugins { llm-gemini = true; llm-groq = true; } 57 => «derivation /nix/store/<hash>-python3-3.12.10-llm-with-llm-gemini-llm-groq.drv» 58 ``` 59 60 ::: 61 */ 62 withPlugins = 63 # Keep this list up to date with the plugins in python3Packages! 64 { 65 llm-anthropic ? false, 66 llm-cmd ? false, 67 llm-command-r ? false, 68 llm-deepseek ? false, 69 llm-docs ? false, 70 llm-echo ? false, 71 llm-fragments-github ? false, 72 llm-fragments-pypi ? false, 73 llm-fragments-reader ? false, 74 llm-fragments-symbex ? false, 75 llm-gemini ? false, 76 llm-gguf ? false, 77 llm-git ? false, 78 llm-github-copilot ? false, 79 llm-grok ? false, 80 llm-groq ? false, 81 llm-hacker-news ? false, 82 llm-jq ? false, 83 llm-llama-server ? false, 84 llm-mistral ? false, 85 llm-ollama ? false, 86 llm-openai-plugin ? false, 87 llm-openrouter ? false, 88 llm-pdf-to-images ? false, 89 llm-perplexity ? false, 90 llm-sentence-transformers ? false, 91 llm-templates-fabric ? false, 92 llm-templates-github ? false, 93 llm-tools-datasette ? false, 94 llm-tools-quickjs ? false, 95 llm-tools-simpleeval ? false, 96 llm-tools-sqlite ? false, 97 llm-venice ? false, 98 llm-video-frames ? false, 99 ... 100 }@args: 101 let 102 # Filter to just the attributes which are set to a true value. 103 setArgs = lib.filterAttrs (name: lib.id) args; 104 105 # Make the derivation name reflect what's inside it, up to a certain limit. 106 setArgNames = lib.attrNames setArgs; 107 drvName = 108 let 109 len = builtins.length setArgNames; 110 in 111 if len == 0 then 112 "llm-${llm.version}" 113 else if len > 20 then 114 "llm-${llm.version}-with-${toString len}-plugins" 115 else 116 # Make a string with those names separated with a dash. 117 "llm-${llm.version}-with-${lib.concatStringsSep "-" setArgNames}"; 118 119 # Make a python environment with just those plugins. 120 python-environment = python.withPackages ( 121 ps: 122 let 123 # Throw a diagnostic if this list gets out of sync with the names in python3Packages 124 allPluginsPresent = pluginNames == withPluginsArgNames; 125 pluginNames = lib.attrNames (lib.intersectAttrs ps withPluginsArgs); 126 missingNamesList = lib.attrNames (lib.removeAttrs withPluginsArgs pluginNames); 127 missingNames = lib.concatStringsSep ", " missingNamesList; 128 129 # The relevant plugins are the ones the user asked for. 130 plugins = lib.intersectAttrs setArgs ps; 131 in 132 assert lib.assertMsg allPluginsPresent "Missing these plugins: ${missingNames}"; 133 ([ ps.llm ] ++ lib.attrValues plugins) 134 ); 135 136 in 137 # That Python environment produced above contains too many irrelevant binaries, due to how 138 # Python needs to use propagatedBuildInputs. Let's make one with just what's needed: `llm`. 139 # Since we include the `passthru` and `meta` information, it's as good as the original 140 # derivation. 141 runCommand "${python.name}-${drvName}" { inherit (llm) passthru meta; } '' 142 mkdir -p $out/bin 143 ln -s ${python-environment}/bin/llm $out/bin/llm 144 ''; 145 146 # Uses the `withPlugins` names to make a Python environment with everything. 147 withAllPlugins = withPlugins (lib.genAttrs withPluginsArgNames (name: true)); 148 149 # The function signature of `withPlugins` is the list of all the plugins `llm` knows about. 150 # The plugin directory is at <https://llm.datasette.io/en/stable/plugins/directory.html> 151 withPluginsArgs = lib.functionArgs withPlugins; 152 withPluginsArgNames = lib.attrNames withPluginsArgs; 153 154 # In order to help with usability, we patch `llm install` and `llm uninstall` to tell users how to 155 # customize `llm` with plugins in Nix, including the name of the plugin, its description, and 156 # where it's coming from. 157 listOfPackagedPlugins = builtins.toFile "plugins.txt" ( 158 lib.concatStringsSep "\n " ( 159 map (name: '' 160 # ${python.pkgs.${name}.meta.description} <${python.pkgs.${name}.meta.homepage}> 161 ${name} = true; 162 '') withPluginsArgNames 163 ) 164 ); 165 166 llm = buildPythonPackage rec { 167 pname = "llm"; 168 version = "0.27.1"; 169 pyproject = true; 170 171 build-system = [ setuptools ]; 172 173 disabled = pythonOlder "3.8"; 174 175 src = fetchFromGitHub { 176 owner = "simonw"; 177 repo = "llm"; 178 tag = version; 179 hash = "sha256-HWzuPhI+oiCKBeiHK7x9Sc54ZB88Py60FzprMLlZGrY="; 180 }; 181 182 patches = [ ./001-disable-install-uninstall-commands.patch ]; 183 184 postPatch = '' 185 substituteInPlace llm/cli.py \ 186 --replace-fail "@listOfPackagedPlugins@" "$(< ${listOfPackagedPlugins})" 187 ''; 188 189 dependencies = [ 190 click-default-group 191 condense-json 192 numpy 193 openai 194 pip 195 pluggy 196 puremagic 197 pydantic 198 python-ulid 199 pyyaml 200 setuptools # for pkg_resources 201 sqlite-migrate 202 sqlite-utils 203 ]; 204 205 nativeCheckInputs = [ 206 cogapp 207 numpy 208 pytest-asyncio 209 pytest-httpx 210 pytest-recording 211 syrupy 212 pytestCheckHook 213 ]; 214 215 doCheck = true; 216 217 # The tests make use of `llm_echo` but that would be a circular dependency. 218 # So we make a local copy in this derivation, as it's a super-simple package of one file. 219 preCheck = '' 220 cp ${llm-echo.src}/llm_echo.py llm_echo.py 221 ''; 222 223 pytestFlags = [ 224 "-svv" 225 ]; 226 227 enabledTestPaths = [ 228 "tests/" 229 ]; 230 231 pythonImportsCheck = [ "llm" ]; 232 233 passthru = { 234 inherit withPlugins withAllPlugins; 235 236 mkPluginTest = plugin: { 237 ${plugin.pname} = callPackage ./mk-plugin-test.nix { inherit llm plugin; }; 238 }; 239 240 # include tests for all the plugins 241 tests = lib.mergeAttrsList (map (name: python.pkgs.${name}.tests) withPluginsArgNames); 242 }; 243 244 meta = { 245 homepage = "https://github.com/simonw/llm"; 246 description = "Access large language models from the command-line"; 247 changelog = "https://github.com/simonw/llm/releases/tag/${src.tag}"; 248 license = lib.licenses.asl20; 249 mainProgram = "llm"; 250 maintainers = with lib.maintainers; [ 251 aldoborrero 252 mccartykim 253 philiptaron 254 ]; 255 }; 256 }; 257in 258llm