Offer a library-only llm-lib wheel that omits the CLI entry point
Hi Simon. I am using llm as a library in a project. However, when llm is installed inside a virtual environment (e.g. via pip install llm or uv add llm), the package always drops an llm console script because [project.scripts] llm = "llm.cli:cli" The issue is that this llm console script shadows any global installation (such as one using pipx, homebrew, or similar).
Normally this is not a problem, but llm's plugin capability makes it so that the behavior of the llm console script ends up varying depending on where I am in the file hierarchy. pip/uv do not offer an option to skip installing console scripts, and PyPA recommends publishers not rely on extras to gate entry points. That means the only practical way have 'library only' consumer experience is to remove the script manually after each sync.
Proposal
- Keep the current distribution (
llm) exactly as-is for CLI users. - Add a companion distribution (suggested name:
llm-lib) that packages the same Python modules but omits[project.scripts]. - Build/publish both wheels from the same repo and release tag so their versions stay in sync.
- Document the intended usage:
-
pipx install llm(orpip install llmif you want the CLI in the environment) -
pip install llm-libwhen you only needimport llminside a venv and want to keep the pipx-managed CLI + plugins untouched.
This pattern is the commonly-used workaround for “optional console scripts” and avoids any breaking change for existing CLI users.
Motivation
- Allows projects that embed
llmas a library to depend on it without clobbering the pipx CLI entry point. - Keeps plugin discovery consistent: the pipx-installed CLI continues to see its plugins, while the virtualenv gets a clean library import.
- Requires minimal code changes (mostly packaging metadata and release automation) compared to attempting installer changes in pip/uv.
I am happy to create a PR if you are interested. Here is how I would approach it:
- Replace the fixed [project] name/[project.scripts] block in pyproject.toml with dynamic fields so setuptools asks Python code for the value at build time. For example:
[project]
dynamic = ["name", "entry-points"]
# keep everything else (dependencies, classifiers, etc.)
[tool.setuptools.dynamic]
name = {attr = "llm._build_variants:get_name"}
entry-points = {attr = "llm._build_variants:get_entry_points"}
Add a tiny helper that is imported and used by setuptools: _build_variants.py:
VARIANT = os.environ.get("LLM_BUILD_VARIANT", "cli") # "cli" or "lib" │
def get_name():
return "llm-lib" if VARIANT == "lib" else "llm"
def get_entry_points():
if VARIANT == "lib": return {} # no console scripts
else: return {"console_scripts": ["llm = llm.cli:cli"]}