fix(install): add orjson to [proxy] extra for LiteLLM provider backends (#2074)

## 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 <syf2211@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
This commit is contained in:
石岳峰 2026-07-13 21:37:28 +08:00 committed by GitHub
parent 099c66432b
commit 4f3d5ab341
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 32 additions and 0 deletions

View file

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Fixed ### 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` - The dashboard's per-request metadata (the `recent_requests` / `request_logs`
tail and the `config` block with upstream URLs) is gated to loopback callers 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 via `_request_is_loopback`. When Headroom runs in a bridge-network container

View file

@ -66,6 +66,9 @@ dependencies = [
proxy = [ proxy = [
"fastapi>=0.100.0", "fastapi>=0.100.0",
"uvicorn>=0.23.0,<1.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", "httpx[http2]>=0.24.0",
"openai>=2.14.0", # OpenAI API format support "openai>=2.14.0", # OpenAI API format support
"mcp>=1.0.0", # MCP server (headroom_compress, retrieve, stats) "mcp>=1.0.0", # MCP server (headroom_compress, retrieve, stats)

View file

@ -22,6 +22,8 @@ SYS_PLATFORM_MARKER = "sys_platform"
PLATFORM_MACHINE_MARKER = "platform_machine" PLATFORM_MACHINE_MARKER = "platform_machine"
TORCH_PACKAGE_NAME = "torch" TORCH_PACKAGE_NAME = "torch"
TORCH_TRANSITIVE_PACKAGE_NAMES = frozenset({"sentence-transformers"}) TORCH_TRANSITIVE_PACKAGE_NAMES = frozenset({"sentence-transformers"})
ORJSON_PACKAGE_NAME = "orjson"
PROXY_EXTRA = "proxy"
UV_LOCK_FILE = "uv.lock" 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 locked_torch_transitive_dependency_names
assert TORCH_PACKAGE_NAME not in selected_all_dependency_names assert TORCH_PACKAGE_NAME not in selected_all_dependency_names
assert selected_all_dependency_names.isdisjoint(locked_torch_transitive_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

4
uv.lock generated
View file

@ -1558,6 +1558,7 @@ all = [
{ name = "openpyxl" }, { name = "openpyxl" },
{ name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "opentelemetry-sdk" }, { name = "opentelemetry-sdk" },
{ name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
{ name = "pillow" }, { name = "pillow" },
{ name = "rapidocr", marker = "python_full_version >= '3.13'" }, { name = "rapidocr", marker = "python_full_version >= '3.13'" },
{ name = "rapidocr-onnxruntime", 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.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 = "onnxruntime", version = "1.26.0", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.14'" },
{ name = "openai" }, { name = "openai" },
{ name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
{ name = "sqlite-vec" }, { name = "sqlite-vec" },
{ name = "transformers" }, { name = "transformers" },
{ name = "uvicorn" }, { 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.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 = "onnxruntime", version = "1.26.0", source = { registry = "https://pypi.org/simple/" }, marker = "python_full_version >= '3.14'" },
{ name = "openai" }, { name = "openai" },
{ name = "orjson", marker = "platform_python_implementation != 'PyPy'" },
{ name = "sqlite-vec" }, { name = "sqlite-vec" },
{ name = "transformers" }, { name = "transformers" },
{ name = "uvicorn" }, { name = "uvicorn" },
@ -1784,6 +1787,7 @@ requires-dist = [
{ name = "openai", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "openai", marker = "extra == 'dev'", specifier = ">=1.0.0" },
{ name = "openai", marker = "extra == 'evals'", specifier = ">=1.0.0" }, { name = "openai", marker = "extra == 'evals'", specifier = ">=1.0.0" },
{ name = "openai", marker = "extra == 'proxy'", specifier = ">=2.14.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 == 'dev'", specifier = ">=3.1.0" },
{ name = "openpyxl", marker = "extra == 'spreadsheet'", specifier = ">=3.1.0" }, { name = "openpyxl", marker = "extra == 'spreadsheet'", specifier = ">=3.1.0" },
{ name = "opentelemetry-api", specifier = ">=1.24.0" }, { name = "opentelemetry-api", specifier = ">=1.24.0" },