From 4f3d5ab341466297cb1b37104d93b338c4947c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9F=B3=E5=B2=B3=E5=B3=B0?= <132282304+syf2211@users.noreply.github.com> Date: Mon, 13 Jul 2026 21:37:28 +0800 Subject: [PATCH] fix(install): add orjson to [proxy] extra for LiteLLM provider backends (#2074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Add `orjson` to the `[proxy]` extra so `uv tool install "headroom-ai[all]"` installs a runtime dependency required by LiteLLM provider backends (e.g. OpenRouter). ## Type of Change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Changes Made - Add `orjson>=3.9.14; platform_python_implementation != 'PyPy'` to `[proxy]`. - Regression test in `tests/test_optional_dependencies.py`. - Minimal `uv.lock` update (proxy/all optional-deps + metadata only). ## Motivation Fixes #2056. `headroom-ai[all]` installs `litellm` (core dep) but not `orjson`. LiteLLM provider backends import `orjson` at runtime; LiteLLM only declares it under `litellm[proxy]`, not base deps. Headroom does not depend on `litellm[proxy]` (would pull the full LiteLLM proxy server stack). ## Testing - `uv run --extra dev python -m pytest tests/test_optional_dependencies.py -q` — 2 passed - `uvx --from ruff==0.15.17 ruff check tests/test_optional_dependencies.py` - `uvx --from ruff==0.15.17 ruff format --check tests/test_optional_dependencies.py` ## Real Behavior Proof - **Setup:** Ubuntu, Python 3.12.3 - **Verified:** dependency graph test confirms `orjson` is selected for `[proxy]` and `[all]` extras after this patch. - **Not tested locally:** full `uv tool install` end-to-end (local sdist build requires Rust/C++ toolchain unavailable in this environment). Reporter workaround `uv tool install ... --with orjson` confirms the missing transitive dep diagnosis. ## Review Readiness - [x] I have performed a self-review of my code - [x] This PR is ready for human review ## Notes - Reporter used Python 3.14.4; `litellm` is intentionally skipped on 3.14 (GH #956). This PR fixes the missing `orjson` install path for supported Python versions using `[all]`. Co-authored-by: syf2211 Co-authored-by: JerrettDavis Co-authored-by: Tejas Chopra --- CHANGELOG.md | 1 + pyproject.toml | 3 +++ tests/test_optional_dependencies.py | 24 ++++++++++++++++++++++++ uv.lock | 4 ++++ 4 files changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e1e3ee17..1d9b4a6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Fixed +- **install:** include `orjson` in the `[proxy]` extra so `uv tool install "headroom-ai[all]"` satisfies LiteLLM OpenRouter/provider backends that import it at runtime ([#2056](https://github.com/headroomlabs-ai/headroom/issues/2056)). - The dashboard's per-request metadata (the `recent_requests` / `request_logs` tail and the `config` block with upstream URLs) is gated to loopback callers via `_request_is_loopback`. When Headroom runs in a bridge-network container diff --git a/pyproject.toml b/pyproject.toml index 2b0ca38a4..7d9894d5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,9 @@ dependencies = [ proxy = [ "fastapi>=0.100.0", "uvicorn>=0.23.0,<1.0", + # LiteLLM provider backends (e.g. openrouter) expect orjson at runtime but + # litellm only declares it under its own [proxy] extra (GH #2056). + "orjson>=3.9.14; platform_python_implementation != 'PyPy'", "httpx[http2]>=0.24.0", "openai>=2.14.0", # OpenAI API format support "mcp>=1.0.0", # MCP server (headroom_compress, retrieve, stats) diff --git a/tests/test_optional_dependencies.py b/tests/test_optional_dependencies.py index d9420b6cf..1cc5df910 100644 --- a/tests/test_optional_dependencies.py +++ b/tests/test_optional_dependencies.py @@ -22,6 +22,8 @@ SYS_PLATFORM_MARKER = "sys_platform" PLATFORM_MACHINE_MARKER = "platform_machine" TORCH_PACKAGE_NAME = "torch" TORCH_TRANSITIVE_PACKAGE_NAMES = frozenset({"sentence-transformers"}) +ORJSON_PACKAGE_NAME = "orjson" +PROXY_EXTRA = "proxy" UV_LOCK_FILE = "uv.lock" @@ -103,3 +105,25 @@ def test_all_extra_does_not_require_torch_on_macos_x86_64() -> None: assert locked_torch_transitive_dependency_names assert TORCH_PACKAGE_NAME not in selected_all_dependency_names assert selected_all_dependency_names.isdisjoint(locked_torch_transitive_dependency_names) + + +def test_proxy_extra_includes_orjson_for_litellm_backends() -> None: + """`headroom-ai[all]` must ship orjson for LiteLLM provider backends (GH #2056).""" + + pyproject = tomllib.loads((ROOT / PYPROJECT_FILE).read_text(encoding="utf-8")) + optional_deps = pyproject["project"]["optional-dependencies"] + environment = default_environment() + + selected_proxy_dependency_names = _selected_dependency_names_for_extra( + optional_deps, + PROXY_EXTRA, + environment, + ) + selected_all_dependency_names = _selected_dependency_names_for_extra( + optional_deps, + ALL_EXTRA, + environment, + ) + + assert ORJSON_PACKAGE_NAME in selected_proxy_dependency_names + assert ORJSON_PACKAGE_NAME in selected_all_dependency_names diff --git a/uv.lock b/uv.lock index ecec8b6f7..090232bff 100644 --- a/uv.lock +++ b/uv.lock @@ -1558,6 +1558,7 @@ all = [ { name = "openpyxl" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-sdk" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, { name = "pillow" }, { name = "rapidocr", marker = "python_full_version >= '3.13'" }, { name = "rapidocr-onnxruntime", marker = "python_full_version < '3.13'" }, @@ -1673,6 +1674,7 @@ proxy = [ { name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.14'" }, { name = "onnxruntime", version = "1.26.0", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.14'" }, { name = "openai" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, { name = "sqlite-vec" }, { name = "transformers" }, { name = "uvicorn" }, @@ -1689,6 +1691,7 @@ proxy-prod = [ { name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version < '3.14'" }, { name = "onnxruntime", version = "1.26.0", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.14'" }, { name = "openai" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, { name = "sqlite-vec" }, { name = "transformers" }, { name = "uvicorn" }, @@ -1784,6 +1787,7 @@ requires-dist = [ { name = "openai", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "openai", marker = "extra == 'evals'", specifier = ">=1.0.0" }, { name = "openai", marker = "extra == 'proxy'", specifier = ">=2.14.0" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy' and extra == 'proxy'", specifier = ">=3.9.14" }, { name = "openpyxl", marker = "extra == 'dev'", specifier = ">=3.1.0" }, { name = "openpyxl", marker = "extra == 'spreadsheet'", specifier = ">=3.1.0" }, { name = "opentelemetry-api", specifier = ">=1.24.0" },