llm: reintroduce withPlugins

This was removed in #340510, as the previous implementation didn't work well.

However, it's still the method that's recommended in the patch for the
`llm install` command (`001-disable-install-uninstall-commands.patch`)
and with the additions to `enable` overrides on the top-level `llm`
derivation, there's a need for a place where the LLM plugin "directory"
is specified.

So, I've reintroduced the method with a completely new implementation.
I've also given it some protections against the list of names getting
out of date.

This might be a little too clever, but I think the user-facing interface
for this function is now quite nice.
This commit is contained in:
Philip Taron 2025-05-09 14:04:33 -07:00
parent 794f8c04b0
commit 0d50348b51
2 changed files with 143 additions and 85 deletions

View file

@ -1,81 +1,46 @@
{
lib,
python3Packages,
enable-llm-anthropic ? false,
enable-llm-cmd ? false,
enable-llm-command-r ? false,
enable-llm-deepseek ? false,
enable-llm-fragments-github ? false,
enable-llm-fragments-pypi ? false,
enable-llm-gemini ? false,
enable-llm-grok ? false,
enable-llm-groq ? false,
enable-llm-gguf ? false,
enable-llm-hacker-news ? false,
enable-llm-jq ? false,
enable-llm-mistral ? false,
enable-llm-ollama ? false,
enable-llm-openai-plugin ? false,
enable-llm-openrouter ? false,
enable-llm-sentence-transformers ? false,
enable-llm-templates-fabric ? false,
enable-llm-templates-github ? false,
enable-llm-venice ? false,
enable-llm-video-frames ? false,
}:
# The `...` allows this derivation to be overridden with `enable-<llm-plugin>`.
#
# Example:
#
# ```nix
# llm.override {
# enable-llm-anthropic = true;
# enable-llm-gemini = true;
# enable-llm-cmd = true;
# enable-llm-templates-github = true;
# }
# ```
#
# Whatever names are accepted by `llm.withPlugins` are accepted with an added `enable-` prefix as
# an override of this derivation. The user can also do `llm.withPlugins { llm-anthropic = true; }`.
{ lib, python3Packages, ... }@args:
let
inherit (python3Packages)
toPythonApplication
llm
llm-anthropic
llm-cmd
llm-command-r
llm-deepseek
llm-fragments-github
llm-fragments-pypi
llm-gemini
llm-gguf
llm-grok
llm-groq
llm-hacker-news
llm-jq
llm-mistral
llm-ollama
llm-openai-plugin
llm-openrouter
llm-sentence-transformers
llm-templates-fabric
llm-templates-github
llm-venice
llm-video-frames
;
inherit (python3Packages) llm;
hasEnablePrefix = lib.hasPrefix "enable-";
addEnablePrefix = name: "enable-${name}";
removeEnablePrefix = lib.removePrefix "enable-";
# Filter to just the attributes which are named "enable-<plugin-name>"
enableArgs = lib.filterAttrs (name: value: hasEnablePrefix name) args;
pluginArgs = lib.mapAttrs' (
name: value: lib.nameValuePair (removeEnablePrefix name) value
) enableArgs;
# Provide some diagnostics for the plugin names
pluginNames = lib.attrNames (lib.functionArgs llm.withPlugins);
enableNames = lib.map addEnablePrefix pluginNames;
unknownPluginNames = lib.removeAttrs pluginArgs pluginNames;
unknownNames = lib.map addEnablePrefix (lib.attrNames unknownPluginNames);
unknownNamesDiagnostic = ''
Unknown plugins specified in override: ${lib.concatStringsSep ", " unknownNames}
Valid overrides:
- ${lib.concatStringsSep "\n - " enableNames}
'';
in
toPythonApplication (
llm.overrideAttrs (finalAttrs: {
propagatedBuildInputs =
(finalAttrs.propagatedBuildInputs or [ ])
++ lib.optionals enable-llm-anthropic [ llm-anthropic ]
++ lib.optionals enable-llm-cmd [ llm-cmd ]
++ lib.optionals enable-llm-cmd [ llm-command-r ]
++ lib.optionals enable-llm-deepseek [ llm-deepseek ]
++ lib.optionals enable-llm-fragments-github [ llm-fragments-github ]
++ lib.optionals enable-llm-fragments-pypi [ llm-fragments-pypi ]
++ lib.optionals enable-llm-gemini [ llm-gemini ]
++ lib.optionals enable-llm-gguf [ llm-gguf ]
++ lib.optionals enable-llm-grok [ llm-grok ]
++ lib.optionals enable-llm-groq [ llm-groq ]
++ lib.optionals enable-llm-hacker-news [ llm-hacker-news ]
++ lib.optionals enable-llm-jq [ llm-jq ]
++ lib.optionals enable-llm-mistral [ llm-mistral ]
++ lib.optionals enable-llm-ollama [ llm-ollama ]
++ lib.optionals enable-llm-openai-plugin [ llm-openai-plugin ]
++ lib.optionals enable-llm-openrouter [ llm-openrouter ]
++ lib.optionals enable-llm-sentence-transformers [ llm-sentence-transformers ]
++ lib.optionals enable-llm-templates-fabric [ llm-templates-fabric ]
++ lib.optionals enable-llm-templates-github [ llm-templates-github ]
++ lib.optionals enable-llm-venice [ llm-venice ]
++ lib.optionals enable-llm-video-frames [ llm-video-frames ];
})
)
assert lib.assertMsg (lib.length unknownNames == 0) unknownNamesDiagnostic;
llm.withPlugins pluginArgs

View file

@ -1,5 +1,6 @@
{
lib,
runCommand,
callPackage,
buildPythonPackage,
fetchFromGitHub,
@ -14,6 +15,7 @@
pluggy,
puremagic,
pydantic,
python,
python-ulid,
pyyaml,
sqlite-migrate,
@ -23,6 +25,103 @@
sqlite-utils,
}:
let
# The function signature of `withPlugins` is the list of all the plugins `llm` knows about.
# The plugin directory is at <https://llm.datasette.io/en/stable/plugins/directory.html>
withPluginsArgNames = lib.functionArgs withPlugins;
/**
Make a derivation for `llm` that contains `llm` plus the relevant plugins.
# Type
```
withPlugins ::
{
llm-anthropic :: bool,
llm-gemini :: bool,
...
}
-> derivation
```
See `lib.attrNames (lib.functionArgs llm.withPlugins)` for the total list of plugins supported.
# Examples
:::{.example}
## `llm.withPlugins` usage example
```nix
llm.withPlugins { llm-gemini = true; llm-groq = true; }
=> «derivation /nix/store/<hash>-python3-3.12.10-llm-with-llm-gemini-llm-groq.drv»
```
:::
*/
withPlugins =
# Keep this list up to date with the plugins in python3Packages!
{
llm-anthropic ? false,
llm-cmd ? false,
llm-command-r ? false,
llm-deepseek ? false,
llm-fragments-github ? false,
llm-fragments-pypi ? false,
llm-gemini ? false,
llm-gguf ? false,
llm-grok ? false,
llm-groq ? false,
llm-hacker-news ? false,
llm-jq ? false,
llm-mistral ? false,
llm-ollama ? false,
llm-openai-plugin ? false,
llm-openrouter ? false,
llm-sentence-transformers ? false,
llm-templates-fabric ? false,
llm-templates-github ? false,
llm-venice ? false,
llm-video-frames ? false,
}@args:
let
# Filter to just the attributes which are set to a true value.
setArgs = lib.filterAttrs (name: lib.id) args;
# Make a string with those names separated with a dash.
setArgsStr = lib.concatStringsSep "-" (lib.attrNames setArgs);
# Make the derivation name reflect what's inside it.
drvName = if lib.stringLength setArgsStr == 0 then "llm" else "llm-with-${setArgsStr}";
# Make a python environment with just those plugins.
python-environment = python.withPackages (
ps:
let
# Throw a diagnostic if this list gets out of sync with the names in python3Packages
allPluginsPresent = pluginNames == (lib.attrNames withPluginsArgNames);
pluginNames = lib.attrNames (lib.intersectAttrs ps withPluginsArgNames);
missingNamesList = lib.attrNames (lib.removeAttrs withPluginsArgNames pluginNames);
missingNames = lib.concatStringsSep ", " missingNamesList;
# The relevant plugins are the ones the user asked for.
plugins = lib.intersectAttrs setArgs ps;
in
assert lib.assertMsg allPluginsPresent "Missing these plugins: ${missingNames}";
([ ps.llm ] ++ lib.attrValues plugins)
);
in
# That Python environment produced above contains too many irrelevant binaries, due to how
# Python needs to use propagatedBuildInputs. Let's make one with just what's needed: `llm`.
# Since we include the `passthru` and `meta` information, it's as good as the original
# derivation.
runCommand "${python.name}-${drvName}" { inherit (llm) passthru meta; } ''
mkdir -p $out/bin
ln -s ${python-environment}/bin/llm $out/bin/llm
'';
# Uses the `withPlugins` names to make a Python environment with everything.
withAllPlugins = withPlugins (lib.genAttrs (lib.attrNames withPluginsArgNames) (name: true));
llm = buildPythonPackage rec {
pname = "llm";
version = "0.25";
@ -75,7 +174,8 @@ let
pythonImportsCheck = [ "llm" ];
passthru = {
inherit withPlugins;
inherit withPlugins withAllPlugins;
mkPluginTest = plugin: {
${plugin.pname} = callPackage ./mk-plugin-test.nix { inherit llm plugin; };
};
@ -93,12 +193,5 @@ let
];
};
};
withPlugins = throw ''
llm.withPlugins was confusing to use and has been removed.
Please migrate to using python3.withPackages(ps: [ ps.llm ]) instead.
See https://nixos.org/manual/nixpkgs/stable/#python.withpackages-function for more usage examples.
'';
in
llm