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

View file

@ -1,5 +1,6 @@
{ {
lib, lib,
runCommand,
callPackage, callPackage,
buildPythonPackage, buildPythonPackage,
fetchFromGitHub, fetchFromGitHub,
@ -14,6 +15,7 @@
pluggy, pluggy,
puremagic, puremagic,
pydantic, pydantic,
python,
python-ulid, python-ulid,
pyyaml, pyyaml,
sqlite-migrate, sqlite-migrate,
@ -23,6 +25,103 @@
sqlite-utils, sqlite-utils,
}: }:
let 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 { llm = buildPythonPackage rec {
pname = "llm"; pname = "llm";
version = "0.25"; version = "0.25";
@ -75,7 +174,8 @@ let
pythonImportsCheck = [ "llm" ]; pythonImportsCheck = [ "llm" ];
passthru = { passthru = {
inherit withPlugins; inherit withPlugins withAllPlugins;
mkPluginTest = plugin: { mkPluginTest = plugin: {
${plugin.pname} = callPackage ./mk-plugin-test.nix { inherit llm 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 in
llm llm