nixpkgs mirror (for testing)
github.com/NixOS/nixpkgs
nix
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