···11diff --git a/llm/cli.py b/llm/cli.py
22-index af37feb..18b078a 100644
22+index 5d53e74..c2b4707 100644
33--- a/llm/cli.py
44+++ b/llm/cli.py
55-@@ -1014,18 +1014,7 @@ def templates_path():
55+@@ -2895,30 +2895,38 @@ def display_truncated(text):
66+ help="Include pre-release and development versions",
67 )
77- def install(packages, upgrade, editable, force_reinstall, no_cache_dir):
88- """Install packages from PyPI into the same environment as LLM"""
88+ def install(packages, upgrade, editable, force_reinstall, no_cache_dir, pre):
99+- """Install packages from PyPI into the same environment as LLM"""
910- args = ["pip", "install"]
1011- if upgrade:
1112- args += ["--upgrade"]
···1516- args += ["--force-reinstall"]
1617- if no_cache_dir:
1718- args += ["--no-cache-dir"]
1919+- if pre:
2020+- args += ["--pre"]
1821- args += list(packages)
1922- sys.argv = args
2023- run_module("pip", run_name="__main__")
2121-+ click.echo("Install command has been disabled for Nix. If you want to install extra llm plugins, use llm.withPlugins([]) expression.")
2222-2323-2424++ """Install packages from PyPI into the same environment as LLM. Disabled for nixpkgs."""
2525++ raise click.ClickException(
2626++"""Install command has been disabled for Nix. To install extra `llm` plugins, use the `llm.withPlugins` function.
2727++
2828++Example:
2929++
3030++```nix
3131++llm.withPlugins {
3232++ @listOfPackagedPlugins@
3333++}
3434++```
3535++"""
3636++ )
3737+3838+2439 @cli.command()
2525-@@ -1033,8 +1022,7 @@ def install(packages, upgrade, editable, force_reinstall, no_cache_dir):
4040+ @click.argument("packages", nargs=-1, required=True)
2641 @click.option("-y", "--yes", is_flag=True, help="Don't ask for confirmation")
2742 def uninstall(packages, yes):
2828- """Uninstall Python packages from the LLM environment"""
4343+- """Uninstall Python packages from the LLM environment"""
2944- sys.argv = ["pip", "uninstall"] + list(packages) + (["-y"] if yes else [])
3045- run_module("pip", run_name="__main__")
3131-+ click.echo("Uninstall command has been disabled for Nix. If you want to uninstall extra llm plugins, just remove them from llm.withPlugins([]) list expression.")
3232-3333-4646++ """Uninstall Python packages from the LLM environment. Disabled for nixpkgs."""
4747++ raise click.ClickException(
4848++"""Uninstall command has been disabled for Nix. To remove `llm` plugins, use the `llm.withPlugins` function with the desired set of plugins specified.
4949++
5050++Example:
5151++
5252++```nix
5353++llm.withPlugins {
5454++ @listOfPackagedPlugins@
5555++}
5656++```
5757++"""
5858++ )
5959+6060+3461 @cli.command()
6262+--
6363+2.49.0
6464+
+166-13
pkgs/development/python-modules/llm/default.nix
···11{
22 lib,
33+ runCommand,
44+ callPackage,
35 buildPythonPackage,
46 fetchFromGitHub,
57 pytestCheckHook,
68 pythonOlder,
99+ replaceVars,
710 setuptools,
811 click-default-group,
912 condense-json,
···1316 pluggy,
1417 puremagic,
1518 pydantic,
1919+ python,
1620 python-ulid,
1721 pyyaml,
1822 sqlite-migrate,
1923 cogapp,
2024 pytest-asyncio,
2125 pytest-httpx,
2626+ pytest-recording,
2227 sqlite-utils,
2828+ syrupy,
2929+ llm-echo,
2330}:
2431let
3232+ /**
3333+ Make a derivation for `llm` that contains `llm` plus the relevant plugins.
3434+ The function signature of `withPlugins` is the list of all the plugins `llm` knows about.
3535+ Adding a parameter here requires that it be in `python3Packages` attrset.
3636+3737+ # Type
3838+3939+ ```
4040+ withPlugins ::
4141+ {
4242+ llm-anthropic :: bool,
4343+ llm-gemini :: bool,
4444+ ...
4545+ }
4646+ -> derivation
4747+ ```
4848+4949+ See `lib.attrNames (lib.functionArgs llm.withPlugins)` for the total list of plugins supported.
5050+5151+ # Examples
5252+ :::{.example}
5353+ ## `llm.withPlugins` usage example
5454+5555+ ```nix
5656+ llm.withPlugins { llm-gemini = true; llm-groq = true; }
5757+ => «derivation /nix/store/<hash>-python3-3.12.10-llm-with-llm-gemini-llm-groq.drv»
5858+ ```
5959+6060+ :::
6161+ */
6262+ withPlugins =
6363+ # Keep this list up to date with the plugins in python3Packages!
6464+ {
6565+ llm-anthropic ? false,
6666+ llm-cmd ? false,
6767+ llm-command-r ? false,
6868+ llm-deepseek ? false,
6969+ llm-docs ? false,
7070+ llm-echo ? false,
7171+ llm-fragments-github ? false,
7272+ llm-fragments-pypi ? false,
7373+ llm-fragments-reader ? false,
7474+ llm-fragments-symbex ? false,
7575+ llm-gemini ? false,
7676+ llm-gguf ? false,
7777+ llm-git ? false,
7878+ llm-grok ? false,
7979+ llm-groq ? false,
8080+ llm-hacker-news ? false,
8181+ llm-jq ? false,
8282+ llm-llama-server ? false,
8383+ llm-mistral ? false,
8484+ llm-ollama ? false,
8585+ llm-openai-plugin ? false,
8686+ llm-openrouter ? false,
8787+ llm-pdf-to-images ? false,
8888+ llm-sentence-transformers ? false,
8989+ llm-templates-fabric ? false,
9090+ llm-templates-github ? false,
9191+ llm-tools-datasette ? false,
9292+ llm-tools-quickjs ? false,
9393+ llm-tools-simpleeval ? false,
9494+ llm-tools-sqlite ? false,
9595+ llm-venice ? false,
9696+ llm-video-frames ? false,
9797+ ...
9898+ }@args:
9999+ let
100100+ # Filter to just the attributes which are set to a true value.
101101+ setArgs = lib.filterAttrs (name: lib.id) args;
102102+103103+ # Make the derivation name reflect what's inside it, up to a certain limit.
104104+ setArgNames = lib.attrNames setArgs;
105105+ drvName =
106106+ let
107107+ len = builtins.length setArgNames;
108108+ in
109109+ if len == 0 then
110110+ "llm-${llm.version}"
111111+ else if len > 20 then
112112+ "llm-${llm.version}-with-${toString len}-plugins"
113113+ else
114114+ # Make a string with those names separated with a dash.
115115+ "llm-${llm.version}-with-${lib.concatStringsSep "-" setArgNames}";
116116+117117+ # Make a python environment with just those plugins.
118118+ python-environment = python.withPackages (
119119+ ps:
120120+ let
121121+ # Throw a diagnostic if this list gets out of sync with the names in python3Packages
122122+ allPluginsPresent = pluginNames == withPluginsArgNames;
123123+ pluginNames = lib.attrNames (lib.intersectAttrs ps withPluginsArgs);
124124+ missingNamesList = lib.attrNames (lib.removeAttrs withPluginsArgs pluginNames);
125125+ missingNames = lib.concatStringsSep ", " missingNamesList;
126126+127127+ # The relevant plugins are the ones the user asked for.
128128+ plugins = lib.intersectAttrs setArgs ps;
129129+ in
130130+ assert lib.assertMsg allPluginsPresent "Missing these plugins: ${missingNames}";
131131+ ([ ps.llm ] ++ lib.attrValues plugins)
132132+ );
133133+134134+ in
135135+ # That Python environment produced above contains too many irrelevant binaries, due to how
136136+ # Python needs to use propagatedBuildInputs. Let's make one with just what's needed: `llm`.
137137+ # Since we include the `passthru` and `meta` information, it's as good as the original
138138+ # derivation.
139139+ runCommand "${python.name}-${drvName}" { inherit (llm) passthru meta; } ''
140140+ mkdir -p $out/bin
141141+ ln -s ${python-environment}/bin/llm $out/bin/llm
142142+ '';
143143+144144+ # Uses the `withPlugins` names to make a Python environment with everything.
145145+ withAllPlugins = withPlugins (lib.genAttrs withPluginsArgNames (name: true));
146146+147147+ # The function signature of `withPlugins` is the list of all the plugins `llm` knows about.
148148+ # The plugin directory is at <https://llm.datasette.io/en/stable/plugins/directory.html>
149149+ withPluginsArgs = lib.functionArgs withPlugins;
150150+ withPluginsArgNames = lib.attrNames withPluginsArgs;
151151+152152+ # In order to help with usability, we patch `llm install` and `llm uninstall` to tell users how to
153153+ # customize `llm` with plugins in Nix, including the name of the plugin, its description, and
154154+ # where it's coming from.
155155+ listOfPackagedPlugins = builtins.toFile "plugins.txt" (
156156+ lib.concatStringsSep "\n " (
157157+ map (name: ''
158158+ # ${python.pkgs.${name}.meta.description} <${python.pkgs.${name}.meta.homepage}>
159159+ ${name} = true;
160160+ '') withPluginsArgNames
161161+ )
162162+ );
163163+25164 llm = buildPythonPackage rec {
26165 pname = "llm";
2727- version = "0.25";
166166+ version = "0.26";
28167 pyproject = true;
2916830169 build-system = [ setuptools ];
···35174 owner = "simonw";
36175 repo = "llm";
37176 tag = version;
3838- hash = "sha256-iH1P0VdpwIItY1In7vlM0Sn44Db23TqFp8GZ79/GMJs=";
177177+ hash = "sha256-KTlNajuZrR0kBX3LatepsNM3PfRVsQn+evEfXTu6juE=";
39178 };
4017941180 patches = [ ./001-disable-install-uninstall-commands.patch ];
42181182182+ postPatch = ''
183183+ substituteInPlace llm/cli.py \
184184+ --replace-fail "@listOfPackagedPlugins@" "$(< ${listOfPackagedPlugins})"
185185+ '';
186186+43187 dependencies = [
44188 click-default-group
45189 condense-json
···61205 numpy
62206 pytest-asyncio
63207 pytest-httpx
208208+ pytest-recording
209209+ syrupy
64210 pytestCheckHook
65211 ];
6621267213 doCheck = true;
68214215215+ # The tests make use of `llm_echo` but that would be a circular dependency.
216216+ # So we make a local copy in this derivation, as it's a super-simple package of one file.
217217+ preCheck = ''
218218+ cp ${llm-echo.src}/llm_echo.py llm_echo.py
219219+ '';
220220+69221 pytestFlagsArray = [
70222 "-svv"
71223 "tests/"
···74226 pythonImportsCheck = [ "llm" ];
7522776228 passthru = {
7777- inherit withPlugins;
229229+ inherit withPlugins withAllPlugins;
230230+231231+ mkPluginTest = plugin: {
232232+ ${plugin.pname} = callPackage ./mk-plugin-test.nix { inherit llm plugin; };
233233+ };
234234+235235+ # include tests for all the plugins
236236+ tests = lib.mergeAttrsList (map (name: python.pkgs.${name}.tests) withPluginsArgNames);
78237 };
792388080- meta = with lib; {
239239+ meta = {
81240 homepage = "https://github.com/simonw/llm";
82241 description = "Access large language models from the command-line";
83242 changelog = "https://github.com/simonw/llm/releases/tag/${src.tag}";
8484- license = licenses.asl20;
243243+ license = lib.licenses.asl20;
85244 mainProgram = "llm";
8686- maintainers = with maintainers; [
245245+ maintainers = with lib.maintainers; [
87246 aldoborrero
88247 mccartykim
248248+ philiptaron
89249 ];
90250 };
91251 };
9292-9393- withPlugins = throw ''
9494- llm.withPlugins was confusing to use and has been removed.
9595- Please migrate to using python3.withPackages(ps: [ ps.llm ]) instead.
9696-9797- See https://nixos.org/manual/nixpkgs/stable/#python.withpackages-function for more usage examples.
9898- '';
99252in
100253llm
···16461646 quicklispPackagesGCL = throw "Lisp packages have been redesigned. See 'lisp-modules' in the nixpkgs manual."; # Added 2024-05-07
16471647 quicklispPackagesSBCL = throw "Lisp packages have been redesigned. See 'lisp-modules' in the nixpkgs manual."; # Added 2024-05-07
16481648 quickserve = throw "'quickserve' has been removed because its upstream is unavailable"; # Added 2025-05-10
16491649+ qv2ray = throw "'qv2ray' has been removed as it was unmaintained"; # Added 2025-06-03
16491650 qxw = throw "'qxw' has been removed due to lack of maintenance upstream. Consider using 'crosswords' instead"; # Added 2024-10-19
1650165116511652 ### R ###