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