{ lib, runCommand, callPackage, buildPythonPackage, fetchFromGitHub, pytestCheckHook, pythonOlder, replaceVars, setuptools, click-default-group, condense-json, numpy, openai, pip, pluggy, puremagic, pydantic, python, python-ulid, pyyaml, sqlite-migrate, cogapp, pytest-asyncio, pytest-httpx, pytest-recording, sqlite-utils, syrupy, llm-echo, }: let /** Make a derivation for `llm` that contains `llm` plus the relevant plugins. The function signature of `withPlugins` is the list of all the plugins `llm` knows about. Adding a parameter here requires that it be in `python3Packages` attrset. # 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/-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-docs ? false, llm-echo ? false, llm-fragments-github ? false, llm-fragments-pypi ? false, llm-fragments-reader ? false, llm-fragments-symbex ? false, llm-gemini ? false, llm-gguf ? false, llm-git ? false, llm-github-copilot ? false, llm-grok ? false, llm-groq ? false, llm-hacker-news ? false, llm-jq ? false, llm-llama-server ? false, llm-mistral ? false, llm-ollama ? false, llm-openai-plugin ? false, llm-openrouter ? false, llm-pdf-to-images ? false, llm-perplexity ? false, llm-sentence-transformers ? false, llm-templates-fabric ? false, llm-templates-github ? false, llm-tools-datasette ? false, llm-tools-quickjs ? false, llm-tools-simpleeval ? false, llm-tools-sqlite ? 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 the derivation name reflect what's inside it, up to a certain limit. setArgNames = lib.attrNames setArgs; drvName = let len = builtins.length setArgNames; in if len == 0 then "llm-${llm.version}" else if len > 20 then "llm-${llm.version}-with-${toString len}-plugins" else # Make a string with those names separated with a dash. "llm-${llm.version}-with-${lib.concatStringsSep "-" setArgNames}"; # 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 == withPluginsArgNames; pluginNames = lib.attrNames (lib.intersectAttrs ps withPluginsArgs); missingNamesList = lib.attrNames (lib.removeAttrs withPluginsArgs 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 withPluginsArgNames (name: true)); # The function signature of `withPlugins` is the list of all the plugins `llm` knows about. # The plugin directory is at withPluginsArgs = lib.functionArgs withPlugins; withPluginsArgNames = lib.attrNames withPluginsArgs; # In order to help with usability, we patch `llm install` and `llm uninstall` to tell users how to # customize `llm` with plugins in Nix, including the name of the plugin, its description, and # where it's coming from. listOfPackagedPlugins = builtins.toFile "plugins.txt" ( lib.concatStringsSep "\n " ( map (name: '' # ${python.pkgs.${name}.meta.description} <${python.pkgs.${name}.meta.homepage}> ${name} = true; '') withPluginsArgNames ) ); llm = buildPythonPackage rec { pname = "llm"; version = "0.27.1"; pyproject = true; build-system = [ setuptools ]; disabled = pythonOlder "3.8"; src = fetchFromGitHub { owner = "simonw"; repo = "llm"; tag = version; hash = "sha256-HWzuPhI+oiCKBeiHK7x9Sc54ZB88Py60FzprMLlZGrY="; }; patches = [ ./001-disable-install-uninstall-commands.patch ]; postPatch = '' substituteInPlace llm/cli.py \ --replace-fail "@listOfPackagedPlugins@" "$(< ${listOfPackagedPlugins})" ''; dependencies = [ click-default-group condense-json numpy openai pip pluggy puremagic pydantic python-ulid pyyaml setuptools # for pkg_resources sqlite-migrate sqlite-utils ]; nativeCheckInputs = [ cogapp numpy pytest-asyncio pytest-httpx pytest-recording syrupy pytestCheckHook ]; doCheck = true; # The tests make use of `llm_echo` but that would be a circular dependency. # So we make a local copy in this derivation, as it's a super-simple package of one file. preCheck = '' cp ${llm-echo.src}/llm_echo.py llm_echo.py ''; pytestFlags = [ "-svv" ]; enabledTestPaths = [ "tests/" ]; disabledTests = [ # AssertionError: The following responses are mocked but not requested: # - Match POST request on https://api.openai.com/v1/chat/completions # https://github.com/simonw/llm/issues/1292 "test_gpt4o_mini_sync_and_async" # TypeError: CliRunner.__init__() got an unexpected keyword argument 'mix_stderr # https://github.com/simonw/llm/issues/1293 "test_embed_multi_files_encoding" ]; pythonImportsCheck = [ "llm" ]; passthru = { inherit withPlugins withAllPlugins; mkPluginTest = plugin: { ${plugin.pname} = callPackage ./mk-plugin-test.nix { inherit llm plugin; }; }; # include tests for all the plugins tests = lib.mergeAttrsList (map (name: python.pkgs.${name}.tests) withPluginsArgNames); }; meta = { homepage = "https://github.com/simonw/llm"; description = "Access large language models from the command-line"; changelog = "https://github.com/simonw/llm/releases/tag/${src.tag}"; license = lib.licenses.asl20; mainProgram = "llm"; maintainers = with lib.maintainers; [ aldoborrero mccartykim philiptaron ]; }; }; in llm