From b2f04e4ef714fb6f2776ed95ee9157c34333e6c3 Mon Sep 17 00:00:00 2001 From: Eyal Mizrachi <145741922+Eyalm321@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:11:12 -0400 Subject: [PATCH] fix(deps): make litellm optional on Python 3.14 (#956) (#993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description `litellm` is a hard dependency and its metadata caps `Requires-Python >=3.10,<3.14`, so `pip install headroom-ai` is unsatisfiable on Python 3.14. But litellm is only used for model registry / pricing / non-core providers — all lazily imported behind `ImportError` guards — never on the core compression or Anthropic proxy path. Refs #956 (install half). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Add a `python_version < '3.14'` marker to both litellm declarations (core deps + dev extra); installs unchanged on <=3.13, skipped on 3.14 (matches the existing rapidocr/tomli marker pattern). ## Testing - [x] Unit tests pass (`pytest`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_litellm_optional.py -q 2 passed in 0.10s $ python3.14 -m pip install dist/headroom_ai-0.25.0-cp310-abi3-linux_x86_64.whl Successfully installed headroom-ai-0.25.0 ... # litellm NOT installed $ python3.14 -c "import importlib.util as u; print(u.find_spec('litellm') is not None)" False ``` ## Real Behavior Proof - Environment: fresh venv on CPython 3.14.5, Linux - Exact command / steps: built the abi3 wheel, `pip install` it on Python 3.14, then `import headroom` + start the proxy + send a compressible request - Observed result: install exits 0 with litellm skipped; `import headroom` works; the proxy compresses (29913 -> 27626 tokens). Stock 0.25.0 cannot install on 3.14 at all. - Not tested: litellm-backed features on 3.14 (intentionally unavailable there until litellm supports 3.14) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Claude Opus 4.8 --- pyproject.toml | 8 +++-- tests/test_litellm_optional.py | 54 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/test_litellm_optional.py diff --git a/pyproject.toml b/pyproject.toml index 1e3e473c6..057f03014 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,11 @@ dependencies = [ # Core: lightweight compression (SmartCrusher, ContentRouter, CCR, TOIN) "tiktoken>=0.5.0", # Tokenizer for all compressors "pydantic>=2.0.0", # Config and data models - "litellm>=1.86.2,<2.0", # Model registry, pricing, and provider support + # litellm's own metadata pins requires-python <3.14, and headroom only uses it for + # model registry / pricing / non-core providers — all lazily imported and + # ImportError-guarded. Marking it 3.14-optional lets headroom install on Python 3.14 + # (core compression + the Anthropic proxy path never import litellm). See GH #956. + "litellm>=1.86.2,<2.0; python_version < '3.14'", # model registry, pricing, providers (lazy) "click>=8.1.0", # CLI framework "rich>=13.0.0", # Rich terminal output "opentelemetry-api>=1.24.0", # Safe no-op OTEL API for instrumentation @@ -228,7 +232,7 @@ dev = [ "pre-commit>=3.0.0", "openai>=1.0.0", "anthropic>=0.18.0", - "litellm>=1.86.2,<2.0", + "litellm>=1.86.2,<2.0; python_version < '3.14'", # see core deps note (GH #956) "fastapi>=0.100.0", "uvicorn>=0.23.0,<1.0", "httpx[http2]>=0.24.0", diff --git a/tests/test_litellm_optional.py b/tests/test_litellm_optional.py new file mode 100644 index 000000000..652c39db1 --- /dev/null +++ b/tests/test_litellm_optional.py @@ -0,0 +1,54 @@ +"""litellm must be optional on Python 3.14 (GH #956). + +litellm's metadata pins requires-python <3.14, so a hard dependency makes +`pip install headroom-ai` unsatisfiable on 3.14. headroom only uses litellm for +model registry / pricing / non-core providers (all lazily imported and +ImportError-guarded), so every litellm requirement must carry a marker that +skips it on 3.14, and the core paths must degrade gracefully without it. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +try: + import tomllib +except ModuleNotFoundError: # Python 3.10 + import tomli as tomllib # type: ignore[no-redef] + +from packaging.requirements import Requirement + +PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml" + + +def _litellm_requirements() -> list[Requirement]: + data = tomllib.loads(PYPROJECT.read_text(encoding="utf-8")) + specs = list(data["project"].get("dependencies", [])) + for extra in data["project"].get("optional-dependencies", {}).values(): + specs.extend(extra) + return [r for spec in specs if (r := Requirement(spec)).name == "litellm"] + + +def test_every_litellm_requirement_is_skipped_on_py314() -> None: + reqs = _litellm_requirements() + assert reqs, "expected litellm to be declared in pyproject" + for r in reqs: + assert r.marker is not None, f"{r}: litellm must carry a python_version marker (GH #956)" + assert not r.marker.evaluate({"python_version": "3.14"}), ( + f"{r}: must be skipped on Python 3.14" + ) + assert r.marker.evaluate({"python_version": "3.13"}), ( + f"{r}: must still install on Python 3.13" + ) + + +def test_proxy_cost_degrades_without_litellm(monkeypatch: pytest.MonkeyPatch) -> None: + # With litellm absent (its state on 3.14), the proxy cost path must return + # None rather than raise. + from headroom.proxy import cost + + monkeypatch.setattr(cost, "LITELLM_AVAILABLE", False) + monkeypatch.setattr(cost, "litellm", None) + assert cost._get_litellm_module() is None