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