2026-01-06 23:16:58 -08:00
|
|
|
|
[build-system]
|
refactor: single-wheel maturin build backend (fixes #355)
Eliminates the dual-package architecture that was the root cause of #355.
`pip install headroom-ai` now produces ONE wheel containing both the Python
source (headroom/*.py) and the compiled Rust extension (headroom/_core.so).
No more separate `headroom-core-py` package, no more chicken-and-egg with
PyPI publication, no more wheelhouse / PIP_FIND_LINKS / composite-action
plumbing in CI.
This is the canonical pattern used by cryptography, polars, ruff,
pydantic-core, and other Rust-as-core Python packages. Honors the
"Rust as core engine" direction.
## What changed
- pyproject.toml: `[build-system]` swapped from hatchling to maturin.
`[tool.hatch.*]` deleted; `[tool.maturin]` added pointing at
`crates/headroom-py/Cargo.toml` for the cdylib. `python-source = "."`
picks up the root `headroom/` package directly (dashboard HTML
templates and other non-Python files included automatically).
- crates/headroom-py/pyproject.toml: deleted. The crate is no longer a
separate published package; its Cargo.toml stays as the cdylib build
target invoked via `[tool.maturin] manifest-path`.
- crates/headroom-py/python/: deleted (placeholder layout for the old
separate package).
## CI updates
- ci.yml: `test` / `test-extras` / `test-agno` jobs simplified — Rust
toolchain set up before `pip install -e .` (which now invokes maturin
via build-system). Removed the "build wheel + symlink .so" dance.
`build` job swapped from `python -m build` (hatch) to
`maturin build` + `maturin sdist`.
- release.yml: collapsed dual-package matrix into one. New `build-wheels`
matrix produces cross-platform wheels for cp310/11/12/13 ×
{linux x86_64, linux aarch64, macos x86_64, macos aarch64}. New
`collect-dist` aggregator merges artifacts. publish-pypi consumes the
merged dist.
- init-native-e2e.yml: dropped windows-latest from the matrix —
upstream `esaxx-rs` (/MT) and `ort-sys` (/MD) link with conflicting
MSVC C runtime libraries, so the Rust extension cannot build for
win_amd64 today. Tracked as a follow-up; not a blocker for Linux+macOS.
- headroom-e2e-setup: composite action now sets up Rust toolchain +
Swatinem/rust-cache before `pip install -e .[proxy]`.
- eval.yml, publish.yml, rust.yml: same pattern — rust toolchain before
install. rust.yml's wheels job builds from root pyproject.toml (no
more `-m crates/headroom-py/Cargo.toml`).
- e2e/init/Dockerfile, e2e/wrap/Dockerfile: install rust + maturin in
the build stage; copy `crates/` + workspace `Cargo.toml/lock` so the
install can build the extension. Dropped `HEADROOM_REQUIRE_RUST_CORE=false`
from wrap-e2e — the image now ships the full Rust core.
- Dockerfile (main): simplified — no more Layer 2/3 dance with
`headroom-core-py` install + symlink. Single `uv pip install` builds
+ installs everything.
- .devcontainer/Dockerfile: rust toolchain + libssl-dev + maturin
added so `uv sync` builds the extension inside the devcontainer.
## Lockfile + script
- uv.lock: regenerated. No `headroom-core-py` entries remain.
- scripts/build_rust_extension.sh: simplified from a symlink-into-tree
workaround to a thin wrapper around `pip install -e .`. The maturin
build-backend handles placement automatically.
## Local validation (all green on macOS aarch64)
1. Clean venv `pip install -e .` → `from headroom._core import …` works.
2. `maturin build --release` → 13.8 MB wheel, 336 files including
`headroom/_core.cpython-311-darwin.so` (32 MB cdylib) and
`headroom/dashboard/templates/dashboard.html`.
3. `pip install <wheel>` in fresh venv → import works.
4. Wheel contents verified via `unzip -l`.
5. `pytest tests/test_transforms/test_diff_compressor.py` — 29 passed.
6. `pytest tests/test_relevance.py` — 30 passed.
7. `cargo build --workspace` + `cargo test --workspace` — all green.
8. `make ci-precheck` — 176 Python tests + Rust + commitlint green.
## Migration notes
Users on `pip install headroom-ai` get the Rust core automatically
(linux + macos wheels). sdist installs require rust toolchain available
locally — pip will build via maturin.
Closes #355
Supersedes #357 (workarounds-based fix abandoned in favor of
architectural fix)
2026-05-03 13:16:41 -07:00
|
|
|
|
requires = ["maturin>=1.5,<2.0"]
|
|
|
|
|
|
build-backend = "maturin"
|
2026-01-06 23:16:58 -08:00
|
|
|
|
|
|
|
|
|
|
[project]
|
2026-01-10 14:51:08 -08:00
|
|
|
|
name = "headroom-ai"
|
2026-06-21 22:28:55 -07:00
|
|
|
|
version = "0.27.0"
|
2026-01-07 11:36:44 -08:00
|
|
|
|
description = "The Context Optimization Layer for LLM Applications - Cut costs by 50-90%"
|
2026-01-06 23:16:58 -08:00
|
|
|
|
readme = "README.md"
|
2026-01-07 11:36:44 -08:00
|
|
|
|
license = "Apache-2.0"
|
2026-01-06 23:16:58 -08:00
|
|
|
|
requires-python = ">=3.10"
|
|
|
|
|
|
authors = [
|
2026-01-07 11:36:44 -08:00
|
|
|
|
{ name = "Headroom Contributors" }
|
|
|
|
|
|
]
|
|
|
|
|
|
maintainers = [
|
|
|
|
|
|
{ name = "Headroom Contributors" }
|
2026-01-06 23:16:58 -08:00
|
|
|
|
]
|
|
|
|
|
|
keywords = [
|
|
|
|
|
|
"llm",
|
|
|
|
|
|
"openai",
|
2026-01-07 11:36:44 -08:00
|
|
|
|
"anthropic",
|
|
|
|
|
|
"claude",
|
|
|
|
|
|
"gpt",
|
2026-01-06 23:16:58 -08:00
|
|
|
|
"context",
|
|
|
|
|
|
"token",
|
|
|
|
|
|
"optimization",
|
2026-01-07 11:36:44 -08:00
|
|
|
|
"compression",
|
2026-01-06 23:16:58 -08:00
|
|
|
|
"caching",
|
2026-01-07 11:36:44 -08:00
|
|
|
|
"proxy",
|
|
|
|
|
|
"ai",
|
|
|
|
|
|
"machine-learning",
|
2026-01-06 23:16:58 -08:00
|
|
|
|
]
|
|
|
|
|
|
classifiers = [
|
2026-01-07 11:36:44 -08:00
|
|
|
|
"Development Status :: 4 - Beta",
|
2026-01-06 23:16:58 -08:00
|
|
|
|
"Intended Audience :: Developers",
|
2026-01-07 11:36:44 -08:00
|
|
|
|
"License :: OSI Approved :: Apache Software License",
|
|
|
|
|
|
"Operating System :: OS Independent",
|
2026-01-06 23:16:58 -08:00
|
|
|
|
"Programming Language :: Python :: 3",
|
|
|
|
|
|
"Programming Language :: Python :: 3.10",
|
|
|
|
|
|
"Programming Language :: Python :: 3.11",
|
|
|
|
|
|
"Programming Language :: Python :: 3.12",
|
2026-06-09 15:33:16 +01:00
|
|
|
|
"Programming Language :: Python :: 3.13",
|
|
|
|
|
|
"Programming Language :: Python :: 3.14",
|
2026-01-07 11:36:44 -08:00
|
|
|
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
2026-01-06 23:16:58 -08:00
|
|
|
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
2026-01-07 11:36:44 -08:00
|
|
|
|
"Typing :: Typed",
|
2026-01-06 23:16:58 -08:00
|
|
|
|
]
|
|
|
|
|
|
dependencies = [
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# Core: lightweight compression (SmartCrusher, ContentRouter, CCR, TOIN)
|
|
|
|
|
|
"tiktoken>=0.5.0", # Tokenizer for all compressors
|
|
|
|
|
|
"pydantic>=2.0.0", # Config and data models
|
2026-06-16 22:11:12 -04:00
|
|
|
|
# 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)
|
Add Click-based CLI with memory management commands
Refactor CLI from argparse to Click for better extensibility:
- New headroom/cli/ package with modular command structure
- Memory commands: list, show, stats, edit, delete, prune, purge, export, import
- Rich terminal output with tables, colors, and formatted stats
- Duration parsing for --older-than and --since flags (7d, 2w, 1m)
- Comprehensive tests using Click's CliRunner (55 tests, no mocks)
CLI structure:
headroom proxy - Start optimization proxy (migrated from argparse)
headroom memory ... - Memory management (new)
headroom evals ... - Evaluation commands (migrated, now under evals group)
Backwards compatibility maintained via headroom/cli.py shim.
2026-01-29 21:19:27 -08:00
|
|
|
|
"click>=8.1.0", # CLI framework
|
|
|
|
|
|
"rich>=13.0.0", # Rich terminal output
|
2026-04-09 21:20:34 -05:00
|
|
|
|
"opentelemetry-api>=1.24.0", # Safe no-op OTEL API for instrumentation
|
fix: bundle ast-grep/difftastic/scc + generic tool_result interceptor framework
What this does, in plain terms:
Headroom's proxy now ships with three CLI tools (ast-grep, difftastic,
scc) that it can use to shrink tool_result payloads before they reach
the model. The goal is simple: when Claude Code (or Codex, Aider, etc.)
asks the model to reason about a big file or diff, we swap the verbose
output for a compact, same-meaning version. Fewer tokens per turn, same
answers, lower bill.
Today a single interceptor is wired: ast-grep on Read. When an agent
reads a large code file, the proxy replaces the file body with an
outline of its top-level functions/classes plus docstrings. In live
tests that cut prompt tokens 74–76% on both OpenAI and Anthropic,
same answer either way.
How it works:
- `pip install headroom-ai` now installs ast-grep via a PyPI wheel
(core dep). difftastic and scc are fetched once at proxy startup
from pinned upstream GitHub releases and cached per-user.
- A generic registry (`headroom/proxy/interceptors/`) lets us add more
tool-aware rewrites in one file each: declare `matches()` and
`transform()`, call `register()`, done. No proxy or metrics plumbing
per tool.
- Safety rails built in: pass-through when a Read specifies a line
range; second Read of the same file in a conversation returns full
content (progressive disclosure); any failing interceptor logs and
skips, never crashes a request.
Opt-in for now:
- Off by default while this ships. Turn on with
`headroom proxy --intercept-tool-results` or
`HEADROOM_INTERCEPT_ENABLED=1`, so we can measure before flipping
defaults.
What users see after turning it on:
- First `headroom wrap claude` boot is ~5s longer (binaries fetched).
Every subsequent run is cache-only.
- Existing `transforms_applied` field in metrics gets entries like
`interceptor:ast-grep`, so savings show up in current dashboards
and HTML reports with no UI change.
Other housekeeping in this PR:
- uv.lock moved to .gitignore — regenerated locally per environment.
- 35 unit + integration tests, ruff + mypy clean.
- Dead-code audit done: removed `binaries.run()`, `needs_filesystem`
plumbing, unused `_kind` tuple elements, unused `tool_output`
parameter, and the never-set HEADROOM_SKIP_TOOLS_BOOTSTRAP env.
2026-04-20 13:41:29 -07:00
|
|
|
|
"ast-grep-cli>=0.30.0", # AST-aware code slicing (CodeCompressor); binary wheel
|
2026-04-21 20:42:56 -05:00
|
|
|
|
"tomli>=2.0.0; python_version < '3.11'", # tomllib backport for helper scripts
|
2026-01-06 23:16:58 -08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
[project.optional-dependencies]
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# Proxy server (most common install: pip install headroom-ai[proxy])
|
2026-01-07 11:36:44 -08:00
|
|
|
|
proxy = [
|
|
|
|
|
|
"fastapi>=0.100.0",
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
|
"uvicorn>=0.23.0,<1.0",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
"httpx[http2]>=0.24.0",
|
|
|
|
|
|
"openai>=2.14.0", # OpenAI API format support
|
2026-03-16 22:56:30 -07:00
|
|
|
|
"mcp>=1.0.0", # MCP server (headroom_compress, retrieve, stats)
|
2026-03-25 01:04:26 -07:00
|
|
|
|
"magika>=0.6.0", # ML content detection for ContentRouter
|
2026-03-30 16:01:51 -07:00
|
|
|
|
"zstandard>=0.20.0", # Decompress zstd request bodies (Codex, etc.)
|
2026-03-30 22:00:12 -07:00
|
|
|
|
"websockets>=13.0", # WebSocket proxy for /v1/responses (Codex gpt-5.4+)
|
2026-03-30 22:50:13 -07:00
|
|
|
|
"onnxruntime>=1.16.0", # Kompress ONNX INT8 text compression (no torch needed)
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
|
"transformers>=4.30.0,<6.0", # Tokenizer only (for Kompress)
|
2026-04-11 19:30:38 -07:00
|
|
|
|
"watchdog>=4.0.0", # File watcher for live code graph reindexing (--code-graph)
|
2026-04-12 09:17:15 -07:00
|
|
|
|
"sqlite-vec>=0.1.6", # Vector index for memory (--memory). Lightweight, no torch.
|
2026-01-07 11:36:44 -08:00
|
|
|
|
]
|
2026-06-08 02:49:14 -04:00
|
|
|
|
# Production ASGI/WSGI server — Unix-only (gunicorn does not support Windows).
|
|
|
|
|
|
# Kept separate from [proxy] so that dev, CI, and Windows users are not forced
|
|
|
|
|
|
# to install a non-functional package. Production deployments should use:
|
|
|
|
|
|
# pip install headroom-ai[proxy,proxy-prod]
|
|
|
|
|
|
proxy-prod = [
|
|
|
|
|
|
"headroom-ai[proxy]",
|
|
|
|
|
|
"gunicorn>=21.0.0; sys_platform != 'win32'",
|
|
|
|
|
|
]
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# AST-based code compression (tree-sitter)
|
fix(code): pin tree-sitter-language-pack <1.0 so code compression works (#1234)
## Description
The `[code]` extra requires `tree-sitter-language-pack>=0.10.0` with no
upper bound, so it now resolves to the 1.x line.
tree-sitter-language-pack 1.0 (2026-03-21) is a breaking rewrite whose
`get_language()` / `get_parser()` return the pack's own binding types
instead of standalone `tree_sitter.Language` / `tree_sitter.Parser`. As
a result `headroom/transforms/code_compressor.py::_get_parser()` raises,
the exception is caught upstream, and AST code compression silently
falls back to passthrough (0% reduction, no error surfaced) on a fresh
`pip install headroom-ai[code]`. This caps the dependency below the
breaking rewrite and pins the matching tree-sitter range, which is the
line the existing code is written against.
Closes #1232
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Pin `tree-sitter-language-pack>=0.10.0,<1.0` in the `[code]` extra
(was `>=0.10.0`).
- Add an explicit `tree-sitter>=0.25.2,<0.26` pin to document the
supported range (0.13.0 already requires `tree-sitter>=0.25.2`).
- Add an inline comment explaining why the `<1.0` cap is required, to
prevent a future re-bump.
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
I did not run the full pytest / ruff / mypy suite for this change (it is
a dependency-constraint pin); I verified the actual runtime behavior the
pin restores. See Real Behavior Proof.
### Test Output
```text
# BEFORE (resolved tree-sitter-language-pack 1.9.1): code compression no-ops
CodeAwareCompressor().compress(<real .py>) -> compression_ratio = 1.0 (0% on every file sampled)
# AFTER (tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2), headroom code unchanged:
is_tree_sitter_available(): True
# 60 varied real Python files (headroom, litellm, pydantic, openai), default CodeCompressorConfig:
compressed OK (valid + reduced): 31 (52%)
rejected for invalid syntax: 17 (28%) -> returns original, never serves broken code
no reduction / too small: 12 (20%)
reduction when it worked: min 4.4% median 37.2% max 88.8%
# All compressed outputs re-parsed clean with ast.parse().
```
## Real Behavior Proof
- Environment: Python 3.12, headroom-ai 0.26.0. Before:
tree-sitter-language-pack 1.9.1 (what `[code]` resolves today). After:
tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2 (what this pin
resolves).
- Exact command / steps: `pip install "headroom-ai[code]"`; then run
`CodeAwareCompressor(CodeCompressorConfig()).compress(src)` over a
sample of real `.py` files and re-tokenize before/after with tiktoken
(cl100k_base), re-parsing each output with `ast.parse`.
- Observed result: with the unpinned (1.x) resolution, every sampled
file returned `compression_ratio == 1.0` (0%, silent passthrough). With
the pinned (0.x) resolution and no code changes,
`is_tree_sitter_available()` is True and 31/60 files compressed validly
at a ~37% median (up to ~89%); all compressed outputs re-parsed clean.
- Not tested: the full pytest / ruff / mypy suite; per-language rates
for JS/TS/Go/Rust/Java/C/C++ (they share the same `_get_parser()` path,
so the fix applies, but I measured Python specifically); the ~28%
invalid-syntax rejections are a separate pre-existing robustness issue
tracked in #1233, not addressed here.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A (dependency-constraint change).
## Additional Notes
- This is the minimal fix to restore functionality. The proper
longer-term fix is to migrate `_get_parser()` and the AST walker to the
tree-sitter-language-pack 1.x API, after which the `<1.0` cap can be
lifted; happy to follow up with that if preferred.
- Unchecked checklist items, with rationale: no docs change needed
(constraint-only); no new tests added (a corpus-based
compress-and-reparse regression test would be valuable but belongs with
the robustness work in #1233); I did not run the full local unit-test
suite for a dependency pin; CHANGELOG appears to be release-please
managed, so I left it untouched.
- I am not a maintainer; this came out of an independent evaluation of
the `[code]` path. Pinning `<1.0` parks the project on the
now-superseded 0.x pack, which is the tradeoff for a one-line fix today.
Co-authored-by: mitralone <5514599+mitralone@users.noreply.github.com>
2026-06-21 20:06:59 +03:00
|
|
|
|
# NOTE: cap below 1.0. tree-sitter-language-pack 1.x is a breaking rewrite whose
|
|
|
|
|
|
# get_language()/get_parser() return the pack's own binding types instead of
|
|
|
|
|
|
# standalone tree_sitter.Language/Parser, so _get_parser() in
|
|
|
|
|
|
# transforms/code_compressor.py fails and code compression silently no-ops.
|
|
|
|
|
|
# The 0.x line (>=0.10,<1.0) returns standalone tree_sitter objects as expected.
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
code = [
|
fix(code): pin tree-sitter-language-pack <1.0 so code compression works (#1234)
## Description
The `[code]` extra requires `tree-sitter-language-pack>=0.10.0` with no
upper bound, so it now resolves to the 1.x line.
tree-sitter-language-pack 1.0 (2026-03-21) is a breaking rewrite whose
`get_language()` / `get_parser()` return the pack's own binding types
instead of standalone `tree_sitter.Language` / `tree_sitter.Parser`. As
a result `headroom/transforms/code_compressor.py::_get_parser()` raises,
the exception is caught upstream, and AST code compression silently
falls back to passthrough (0% reduction, no error surfaced) on a fresh
`pip install headroom-ai[code]`. This caps the dependency below the
breaking rewrite and pins the matching tree-sitter range, which is the
line the existing code is written against.
Closes #1232
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Pin `tree-sitter-language-pack>=0.10.0,<1.0` in the `[code]` extra
(was `>=0.10.0`).
- Add an explicit `tree-sitter>=0.25.2,<0.26` pin to document the
supported range (0.13.0 already requires `tree-sitter>=0.25.2`).
- Add an inline comment explaining why the `<1.0` cap is required, to
prevent a future re-bump.
## Testing
<!-- Check what you actually ran, then paste the real command output
below. -->
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
I did not run the full pytest / ruff / mypy suite for this change (it is
a dependency-constraint pin); I verified the actual runtime behavior the
pin restores. See Real Behavior Proof.
### Test Output
```text
# BEFORE (resolved tree-sitter-language-pack 1.9.1): code compression no-ops
CodeAwareCompressor().compress(<real .py>) -> compression_ratio = 1.0 (0% on every file sampled)
# AFTER (tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2), headroom code unchanged:
is_tree_sitter_available(): True
# 60 varied real Python files (headroom, litellm, pydantic, openai), default CodeCompressorConfig:
compressed OK (valid + reduced): 31 (52%)
rejected for invalid syntax: 17 (28%) -> returns original, never serves broken code
no reduction / too small: 12 (20%)
reduction when it worked: min 4.4% median 37.2% max 88.8%
# All compressed outputs re-parsed clean with ast.parse().
```
## Real Behavior Proof
- Environment: Python 3.12, headroom-ai 0.26.0. Before:
tree-sitter-language-pack 1.9.1 (what `[code]` resolves today). After:
tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2 (what this pin
resolves).
- Exact command / steps: `pip install "headroom-ai[code]"`; then run
`CodeAwareCompressor(CodeCompressorConfig()).compress(src)` over a
sample of real `.py` files and re-tokenize before/after with tiktoken
(cl100k_base), re-parsing each output with `ast.parse`.
- Observed result: with the unpinned (1.x) resolution, every sampled
file returned `compression_ratio == 1.0` (0%, silent passthrough). With
the pinned (0.x) resolution and no code changes,
`is_tree_sitter_available()` is True and 31/60 files compressed validly
at a ~37% median (up to ~89%); all compressed outputs re-parsed clean.
- Not tested: the full pytest / ruff / mypy suite; per-language rates
for JS/TS/Go/Rust/Java/C/C++ (they share the same `_get_parser()` path,
so the fix applies, but I measured Python specifically); the ~28%
invalid-syntax rejections are a separate pre-existing robustness issue
tracked in #1233, not addressed here.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A (dependency-constraint change).
## Additional Notes
- This is the minimal fix to restore functionality. The proper
longer-term fix is to migrate `_get_parser()` and the AST walker to the
tree-sitter-language-pack 1.x API, after which the `<1.0` cap can be
lifted; happy to follow up with that if preferred.
- Unchecked checklist items, with rationale: no docs change needed
(constraint-only); no new tests added (a corpus-based
compress-and-reparse regression test would be valuable but belongs with
the robustness work in #1233); I did not run the full local unit-test
suite for a dependency pin; CHANGELOG appears to be release-please
managed, so I left it untouched.
- I am not a maintainer; this came out of an independent evaluation of
the `[code]` path. Pinning `<1.0` parks the project on the
now-superseded 0.x pack, which is the tradeoff for a one-line fix today.
Co-authored-by: mitralone <5514599+mitralone@users.noreply.github.com>
2026-06-21 20:06:59 +03:00
|
|
|
|
"tree-sitter-language-pack>=0.10.0,<1.0",
|
|
|
|
|
|
"tree-sitter>=0.25.2,<0.26",
|
2026-01-07 11:36:44 -08:00
|
|
|
|
]
|
2026-05-07 16:43:35 -07:00
|
|
|
|
# ML-based compression with Kompress (ModernBERT).
|
|
|
|
|
|
# (The legacy [llmlingua] extra was removed in 0.9.x — no live code path used it.
|
|
|
|
|
|
# Use [ml] for the supported ML compression dependencies.)
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
ml = [
|
fix(deps): remediate dependency CVEs and publish SBOM (#1509)
## Description
Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.
This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).
**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).
**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.
**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.
**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.
## Testing
- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof
### Test Output
```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found
# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME INSTALLED TYPE VULNERABILITY SEVERITY
sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted
nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted
# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised
# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out
# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found
```
## Real Behavior Proof
- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)
## Additional Notes
**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.
Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.
**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
|
|
|
|
"torch>=2.12.1",
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
|
"transformers>=4.30.0,<6.0",
|
fix(proxy): Strands MCP bundle + backend path fixes + Codex fail-closed protection
Three logically-related sets of proxy changes ship in this branch:
1. Strands integration on the Bedrock path (HeadroomBundle + 4 OpenAI
handler fixes + LiteLLM cache stats + dep pin)
2. /stats MCP aggregation (cross-process events log → proxy summary)
3. Codex compression-failure fail-closed (WS + HTTP /v1/responses)
== 1. Strands integration on the Bedrock path ==
* HeadroomBundle (headroom/integrations/strands/bundle.py): single-helper
MCP wiring for a Strands Agent — Headroom MCP server (headroom_compress
/ headroom_retrieve / headroom_stats) plus optional Serena MCP and
optional in-process compression hook. Constructor builds unstarted
MCPClient instances per server; Strands' Agent owns the subprocess
lifecycle. Default config: MCP enabled, Serena enabled, hook OFF
(proxy is the single source of truth for compression). User-side
integration is two lines in any Strands app.
* headroom/proxy/handlers/openai.py — backend path now:
- calls PrefixCacheTracker.update_from_response (was direct-OpenAI only)
- intercepts CCR headroom_retrieve tool_calls server-side, mirroring
the Anthropic handler pattern; NO silent fallback, re-raises on
CCR errors (per feedback_no_silent_fallbacks)
- works for both non-streaming and streaming paths
* headroom/proxy/handlers/streaming.py: _stream_openai_via_backend now
accepts prefix_tracker + optimized_messages, parses cache stats from
the SSE final-usage frame (cache_creation_input_tokens added to the
state machine), records CCR retrieve feedback via a new
_record_ccr_feedback_from_openai_sse helper. Streaming CCR intercept
is intentionally out of scope (mirrors Anthropic streaming behaviour).
* headroom/backends/litellm.py: send_openai_message response usage block
now carries cache_read_input_tokens / cache_creation_input_tokens
(Anthropic/Bedrock dialect) and prompt_tokens_details.cached_tokens
(OpenAI dialect). Backwards-compatible — cold-start callers see the
same 3-key shape; cache keys appear only when the underlying provider
returns them. Pinned by test_no_cache_fields_means_no_cache_keys.
* headroom/proxy/auth_mode.py: ("strands-agents/", "strands") added to
CLIENT_UA_MAP. Production callers should also set X-Client: strands
since the default openai-python UA carries no Strands signal.
* pyproject.toml: huggingface-hub>=1.5.0,<2.0 pinned in [ml] so a sibling
install (e.g. strands-agents) can't drag the version below the floor
transformers 5.x requires (otherwise Kompress silently goes
"unavailable").
== 2. /stats MCP aggregation ==
* headroom/proxy/cost.py: _aggregate_mcp_events() reads the cross-process
shared events file the Headroom MCP server already writes to and
surfaces summary.mcp with three new keys:
- compressions (count of headroom_compress invocations)
- tokens_removed (sum of input - output across those)
- retrievals (count of headroom_retrieve — the load-bearing
over-compression alarm; if it grows linearly
with turn count, lossy compressors are
dropping info the model actually needs)
Defensive on every axis — missing MCP SDK, missing file, malformed
events, read errors — never blocks /stats.
* examples/strands_bundle_demo.py: stats panel prints the new fields so
the demo shows the full proxy-HTTP + MCP-tool story in one view.
== 3. Codex compression-failure fail-closed protection ==
Reported by Camille (2026-05-21): Codex threads were locking with
"ran out of room in the model's context window" after Headroom's
compression timed out on an oversized response.create frame and
forwarded the original ~1.7 MB frame to the upstream, which then
rejected it. Codex's auto-compact heuristic gates on the upstream-
reported total_usage_tokens (which Headroom had been shrinking on
earlier turns), so its compaction never fired and the thread locked.
Validated against open Codex issues (CLI + Desktop share codex-rs/core):
* #16068 — confirms compaction gates on total_usage_tokens,
estimated_token_count is computed but only logged
* #19806 — confirms image token estimator unbounded, contributes to
the same ContextManager.get_total_token_usage → auto-compaction chain
* headroom/proxy/helpers.py: decide_compression_failure_action() with a
unit-tested decision matrix:
- asyncio.TimeoutError → refuse, always
- non-timeout failure + frame > 256 KiB (configurable) → refuse
- non-timeout failure + small frame → forward (legacy)
Operator escape hatches:
- HEADROOM_WS_FAIL_OPEN_ON_COMPRESSION_FAILURE=1 restores legacy
- HEADROOM_WS_COMPRESSION_FAIL_THRESHOLD_BYTES tunes the threshold
* headroom/proxy/handlers/openai.py (WS /v1/responses): consults the
helper after compression failure. On refuse: close client websocket
code 1009 with "headroom: compression <reason> — please compact
context and retry" reason; set termination_cause for the outer
lifecycle finally; return.
* headroom/proxy/handlers/openai.py (HTTP /v1/responses): same helper.
On refuse: raise HTTPException(413) with a structured error body so
FastAPI's HTTPException handler emits a clean 413. The existing
`except HTTPException: raise` guard in this handler already ensures
the 413 propagates without being swallowed by the 502 catch-all.
Anthropic /v1/messages NOT changed in this branch: no equivalent bug
report on Anthropic-protocol clients, Claude Code (Anthropic-owned)
handles context overflow via its own cache_control/ephemeral
primitives, and Cursor/Aider don't maintain the local-Y estimate the
Codex bug requires. Deferred until a real report lands; the patch is
a one-liner reusing the same helper.
== Tests + verification ==
* tests/test_backends/test_litellm_cache_stats.py — 3 tests pinning
cache-stat surfacing across Anthropic/OpenAI dialects + backwards-
compat for no-cache responses.
* tests/test_proxy/test_openai_backend_path.py — 5 tests (Bedrock cache
fields, OpenAI fallback shape, CCR intercept with provider="openai",
CCR re-raise on exception, streaming signature contract).
* tests/test_proxy/test_mcp_stats_aggregation.py — 5 tests pinning the
aggregator across compress+retrieve mixes, empty events, unknown event
types, missing token fields, and read failures.
* tests/test_proxy/test_compression_failure_action.py — 12 tests pinning
the fail-closed decision matrix (timeout always refuses, small
transient passes through, oversize refuses, env override variants,
custom threshold, invalid threshold falls back, 0/negative ignored).
* examples/strands_bedrock_demo.py — model_id bumped from deprecated
Claude 3 Haiku to Sonnet 4.5 (the deprecated model now errors on
account access).
* examples/strands_via_proxy_demo.py — proxy + Bedrock cache + streaming
smoke test.
* examples/strands_mcp_dispatch_test.py — pure MCP round-trip probe.
* examples/strands_bundle_demo.py — full Strands + HeadroomBundle E2E
demo (this is the shape a real Strands user copies into their app).
Full pytest: 5327 passed, 178 skipped. The previously-failing
test_core_operations.py::TestAddBatch::test_add_batch_basic passes now
that the huggingface-hub pin in pyproject.toml unblocks transformers
imports.
E2E verified live against AWS Bedrock (Sonnet 4.5):
* cache_write=10,438 on turn A → cache_read=10,438 on turn B
* streaming SSE final usage frame carries cache_read_input_tokens
* 78.7% reduction on a 50 KB JSON tool_result via SmartCrusher (
dispatched per-content-type by ContentRouter)
* Strands Agent + HeadroomBundle: model autonomously called
headroom_compress + headroom_retrieve via MCP; CompressionStore
round-trip succeeded; final answer correct.
2026-05-21 11:00:14 -07:00
|
|
|
|
# transformers >= 5.x requires huggingface-hub >= 1.5.0,<2.0; pinning
|
|
|
|
|
|
# the floor here prevents Kompress from silently falling back to
|
|
|
|
|
|
# "unavailable" when a sibling install (e.g. `pip install
|
|
|
|
|
|
# strands-agents`) drags huggingface-hub backwards.
|
|
|
|
|
|
"huggingface-hub>=1.5.0,<2.0",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
]
|
|
|
|
|
|
# Memory system (hierarchical memory with vector search)
|
|
|
|
|
|
memory = [
|
|
|
|
|
|
"hnswlib>=0.8.0",
|
|
|
|
|
|
"sqlite-vec>=0.1.6",
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
|
"sentence-transformers>=2.2.0,<6.0",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
]
|
2026-04-10 12:58:35 -05:00
|
|
|
|
# Qdrant + Neo4j memory backend helpers
|
|
|
|
|
|
memory-stack = [
|
fix(deps): remediate dependency CVEs and publish SBOM (#1509)
## Description
Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.
This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).
**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).
**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.
**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.
**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.
## Testing
- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof
### Test Output
```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found
# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME INSTALLED TYPE VULNERABILITY SEVERITY
sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted
nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted
# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised
# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out
# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found
```
## Real Behavior Proof
- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)
## Additional Notes
**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.
Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.
**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
|
|
|
|
"mem0ai>=2.0.0,<3.0",
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
|
"qdrant-client>=1.9.0,<2.0",
|
|
|
|
|
|
"neo4j>=5.20.0,<7.0",
|
2026-04-10 12:58:35 -05:00
|
|
|
|
]
|
feat(memory): add opt-in Apple-GPU (MPS) embedding runtime (#766)
## Description
On Apple-Silicon Macs — especially fanless models like the MacBook Air
(M5) — running the proxy with memory context injection can pin the CPU
while embedding. The embedding work runs an uncapped session on the CPU,
saturating multiple cores, which starves the proxy's asyncio loop and
leads to request timeouts.
This PR adds an **opt-in** runtime that offloads the memory embedder to
the Apple GPU (MPS). Setting `HEADROOM_EMBEDDER_RUNTIME=pytorch_mps`
routes embedding through the torch `sentence-transformers` backend on
MPS instead of the default ONNX CPU embedder, moving the work off the
CPU and keeping the proxy responsive.
The default behavior is unchanged — the feature is strictly opt-in,
env-var only, and falls through to the existing default embedder
selection (with a warning) whenever MPS or the torch dependencies are
unavailable.
Fixes: N/A — no tracking issue (surfaced while running codex auto-review
through
the proxy on a fanless MacBook Air (M5)).
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [x] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- **Runtime selection** (`headroom/proxy/memory_handler.py`): read
`HEADROOM_EMBEDDER_RUNTIME`; when set to `pytorch_mps` **and** MPS is
actually available, route the memory embedder to the torch
`sentence-transformers` backend (Apple GPU).
- If MPS is unavailable or torch/sentence-transformers is not installed,
log a warning and fall through to the existing default embedder
selection (ONNX when available, else the pre-existing local
sentence-transformers fallback) — no crash. The default (env var unset)
is unchanged. Env-var only
- **MPS serialization** (`headroom/memory/adapters/embedders.py`):
`LocalEmbedder` now funnels every `encode()` through a dedicated
single-worker `ThreadPoolExecutor` when the resolved device is MPS.
torch-MPS is not thread-safe, and the existing `run_in_executor(None,
...)` dispatch would otherwise let concurrent proxy requests call MPS
from multiple threads. CPU/CUDA keep the shared default executor
(behavior unchanged). `close()` also drops the cached model so re-use
after close re-initializes cleanly.
- **Packaging** (`pyproject.toml`): new `pytorch-mps` extra (`torch` +
`sentence-transformers`), **platform-gated to macOS** (`; sys_platform
== 'darwin'`) since MPS is Apple-Silicon-only. Deliberately left out of
`[all]` (its deps already arrive via `[ml]`/`[memory]`).
- **Tests** (`tests/test_memory/test_embedder_mps_serialization.py`):
regression coverage for the serialized executor, concurrency safety (no
SIGABRT), CPU-path default behavior, and close/re-use re-initialization.
- **Docs**: `wiki/{configuration,memory,macos-deployment}.md`,
`docs/content/docs/{configuration,installation,memory}.mdx`,
`README.md`, `CHANGELOG.md`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed (CPU-offload + concurrency profiling on
Apple Silicon)
## Test Output
```
$ pytest -v tests/test_memory/test_embedder_mps_serialization.py
collected 4 items
tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor PASSED [ 25%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_creates_single_worker_executor PASSED [ 50%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_concurrent_embeds_do_not_crash PASSED [ 75%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_reembed_after_close_recreates_executor PASSED [100%]
============================== 4 passed in 6.92s ===============================
$ ruff check headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py tests/test_memory/test_embedder_mps_serialization.py
All checks passed!
$ mypy headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py
Success: no issues found in 2 source files
$ pytest -q tests/test_memory/ tests/test_memory_handler_concurrent_init.py tests/test_memory_handler_native_ops.py
553 passed, 1 skipped in 13.51s
```
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
**Why MPS (and not CoreML or a thread cap):** measured on an
Apple-Silicon Mac, the default uncapped CPU embedding session saturates
the cores; the same model on MPS runs at a fraction of the CPU (≈8x
lower sustained CPU utilization in profiling) while producing
**byte-identical embeddings** (cosine distance ≈ 0 across runtimes), so
relevance/ranking is unchanged. A CoreML execution-provider path was
evaluated and rejected: the default optimized ONNX model uses fused ops
that fall back to CPU under CoreML (no offload), and a full-precision
re-export was impractical (very low throughput + multi-GB memory). MPS
via `sentence-transformers` was the only practical GPU offload.
**Why serialization is mandatory:** torch-MPS is not thread-safe —
concurrent encode calls from a multi-worker executor abort with
`-[IOGPUMetalCommandBuffer validate]: failed assertion 'commit an
already committed command buffer'` (reproduced deterministically; a
single-worker executor resolves it).
Under concurrent load the serialized single-GPU-stream throughput meets
or exceeds the parallel CPU path while using a fraction of the cores.
**Scope / boundary:** this targets the Python **memory** embedder, which
is live on the proxy request path (memory context injection). The
Rust-backed SmartCrusher compression path is unaffected and remains
non-configurable from Python by design.
**Safety:** default behavior is unchanged (ONNX, no torch). The feature
is opt-in, env-var only, macOS-gated at the packaging layer, and
degrades gracefully (warn + the existing default embedder selection)
when MPS or the dependencies are unavailable.
2026-06-12 02:59:20 +09:00
|
|
|
|
# Apple-Silicon GPU (MPS) offload for the memory embedder. Opt in at runtime with
|
|
|
|
|
|
# HEADROOM_EMBEDDER_RUNTIME=pytorch_mps. macOS-only; intentionally excluded from [all].
|
|
|
|
|
|
pytorch-mps = [
|
fix(deps): remediate dependency CVEs and publish SBOM (#1509)
## Description
Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.
This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).
**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).
**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.
**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.
**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.
## Testing
- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof
### Test Output
```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found
# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME INSTALLED TYPE VULNERABILITY SEVERITY
sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted
nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted
# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised
# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out
# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found
```
## Real Behavior Proof
- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)
## Additional Notes
**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.
Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.
**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
|
|
|
|
"torch>=2.12.1; sys_platform == 'darwin'",
|
feat(memory): add opt-in Apple-GPU (MPS) embedding runtime (#766)
## Description
On Apple-Silicon Macs — especially fanless models like the MacBook Air
(M5) — running the proxy with memory context injection can pin the CPU
while embedding. The embedding work runs an uncapped session on the CPU,
saturating multiple cores, which starves the proxy's asyncio loop and
leads to request timeouts.
This PR adds an **opt-in** runtime that offloads the memory embedder to
the Apple GPU (MPS). Setting `HEADROOM_EMBEDDER_RUNTIME=pytorch_mps`
routes embedding through the torch `sentence-transformers` backend on
MPS instead of the default ONNX CPU embedder, moving the work off the
CPU and keeping the proxy responsive.
The default behavior is unchanged — the feature is strictly opt-in,
env-var only, and falls through to the existing default embedder
selection (with a warning) whenever MPS or the torch dependencies are
unavailable.
Fixes: N/A — no tracking issue (surfaced while running codex auto-review
through
the proxy on a fanless MacBook Air (M5)).
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [x] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- **Runtime selection** (`headroom/proxy/memory_handler.py`): read
`HEADROOM_EMBEDDER_RUNTIME`; when set to `pytorch_mps` **and** MPS is
actually available, route the memory embedder to the torch
`sentence-transformers` backend (Apple GPU).
- If MPS is unavailable or torch/sentence-transformers is not installed,
log a warning and fall through to the existing default embedder
selection (ONNX when available, else the pre-existing local
sentence-transformers fallback) — no crash. The default (env var unset)
is unchanged. Env-var only
- **MPS serialization** (`headroom/memory/adapters/embedders.py`):
`LocalEmbedder` now funnels every `encode()` through a dedicated
single-worker `ThreadPoolExecutor` when the resolved device is MPS.
torch-MPS is not thread-safe, and the existing `run_in_executor(None,
...)` dispatch would otherwise let concurrent proxy requests call MPS
from multiple threads. CPU/CUDA keep the shared default executor
(behavior unchanged). `close()` also drops the cached model so re-use
after close re-initializes cleanly.
- **Packaging** (`pyproject.toml`): new `pytorch-mps` extra (`torch` +
`sentence-transformers`), **platform-gated to macOS** (`; sys_platform
== 'darwin'`) since MPS is Apple-Silicon-only. Deliberately left out of
`[all]` (its deps already arrive via `[ml]`/`[memory]`).
- **Tests** (`tests/test_memory/test_embedder_mps_serialization.py`):
regression coverage for the serialized executor, concurrency safety (no
SIGABRT), CPU-path default behavior, and close/re-use re-initialization.
- **Docs**: `wiki/{configuration,memory,macos-deployment}.md`,
`docs/content/docs/{configuration,installation,memory}.mdx`,
`README.md`, `CHANGELOG.md`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed (CPU-offload + concurrency profiling on
Apple Silicon)
## Test Output
```
$ pytest -v tests/test_memory/test_embedder_mps_serialization.py
collected 4 items
tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor PASSED [ 25%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_creates_single_worker_executor PASSED [ 50%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_concurrent_embeds_do_not_crash PASSED [ 75%]
tests/test_memory/test_embedder_mps_serialization.py::test_mps_reembed_after_close_recreates_executor PASSED [100%]
============================== 4 passed in 6.92s ===============================
$ ruff check headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py tests/test_memory/test_embedder_mps_serialization.py
All checks passed!
$ mypy headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py
Success: no issues found in 2 source files
$ pytest -q tests/test_memory/ tests/test_memory_handler_concurrent_init.py tests/test_memory_handler_native_ops.py
553 passed, 1 skipped in 13.51s
```
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
**Why MPS (and not CoreML or a thread cap):** measured on an
Apple-Silicon Mac, the default uncapped CPU embedding session saturates
the cores; the same model on MPS runs at a fraction of the CPU (≈8x
lower sustained CPU utilization in profiling) while producing
**byte-identical embeddings** (cosine distance ≈ 0 across runtimes), so
relevance/ranking is unchanged. A CoreML execution-provider path was
evaluated and rejected: the default optimized ONNX model uses fused ops
that fall back to CPU under CoreML (no offload), and a full-precision
re-export was impractical (very low throughput + multi-GB memory). MPS
via `sentence-transformers` was the only practical GPU offload.
**Why serialization is mandatory:** torch-MPS is not thread-safe —
concurrent encode calls from a multi-worker executor abort with
`-[IOGPUMetalCommandBuffer validate]: failed assertion 'commit an
already committed command buffer'` (reproduced deterministically; a
single-worker executor resolves it).
Under concurrent load the serialized single-GPU-stream throughput meets
or exceeds the parallel CPU path while using a fraction of the cores.
**Scope / boundary:** this targets the Python **memory** embedder, which
is live on the proxy request path (memory context injection). The
Rust-backed SmartCrusher compression path is unaffected and remains
non-configurable from Python by design.
**Safety:** default behavior is unchanged (ONNX, no torch). The feature
is opt-in, env-var only, macOS-gated at the packaging layer, and
degrades gracefully (warn + the existing default embedder selection)
when MPS or the dependencies are unavailable.
2026-06-12 02:59:20 +09:00
|
|
|
|
"sentence-transformers>=2.2.0; sys_platform == 'darwin'",
|
|
|
|
|
|
]
|
2026-04-26 23:36:22 -07:00
|
|
|
|
# Semantic relevance scoring with embeddings.
|
|
|
|
|
|
# Uses `fastembed` (BAAI/bge-small-en-v1.5 by default — 33M params,
|
|
|
|
|
|
# 384 dims, ~30 MB int8-quantized ONNX). Same library + model used by
|
|
|
|
|
|
# the Rust SmartCrusher (`fastembed` crate), giving byte-equal embeddings
|
|
|
|
|
|
# across the language boundary. Replaced sentence-transformers in
|
|
|
|
|
|
# Stage 3c.1 — fastembed is faster (~2-3x), smaller (no torch
|
|
|
|
|
|
# dependency), and outranks all-MiniLM-L6-v2 on MTEB by ~6 points.
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
relevance = [
|
2026-04-26 23:36:22 -07:00
|
|
|
|
"fastembed>=0.4.0",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
"numpy>=1.24.0",
|
|
|
|
|
|
]
|
2026-04-09 16:22:18 -07:00
|
|
|
|
# Image compression (ML-based routing + OCR)
|
fix: PR #372 — restore [image] extra on Python 3.13 via rapidocr 3.x adapter
Root cause: `headroom-ai[all]==0.20.16` fails to install on Python 3.13
because `rapidocr-onnxruntime` 1.4.0–1.4.4 wheels declare
`requires-python: <3.13,>=3.6`. After 1.4.x the rapidocr ecosystem
split: `rapidocr-onnxruntime` (bundled-ORT, capped at <3.13) vs
`rapidocr` 3.x (engine-agnostic core, supports 3.13+, returns
RapidOCROutput dataclass instead of v1's tuple).
Fix:
1. pyproject.toml — environment-marker hybrid in [image]:
- rapidocr-onnxruntime>=1.4.0,<2; python_version<'3.13'
- rapidocr>=3.0,<4; python_version>='3.13'
- onnxruntime>=1.7,<2; python_version>='3.13'
ORT remains the engine on every Python version; bundle and speed
unchanged, just split into two packages on 3.13+.
2. headroom/image/compressor.py — runtime adapter:
_resolve_rapidocr() tries v1 first, falls back to v3 when v1 is
missing, returns (None, None) when neither installed. Cached at
module scope. Detection at runtime (not Python-version-based) so
users can install either package on any Python version.
_ocr_extract branches on resolved api_version:
- v1: (list[(box, text, score)], elapsed) tuple — unchanged
- v3: RapidOCROutput dataclass with .txts / .scores / .boxes
attrs (each may be None when nothing detected)
Defensive None-handling, length-mismatch detection, structured
log events for both branches.
Smoke test (real install verified before commit):
pip install rapidocr onnxruntime pillow
→ result type: RapidOCROutput
→ fields: txts (None when empty), scores (None when empty), boxes
Confirms the v3 None-coercion is necessary.
Tests: 11 new unit tests in tests/test_image_ocr_api_compat.py covering:
- Resolver: v1 preferred, v3 fallback, both missing
- v1 path: tuple parses, low-confidence None, empty result None
- v3 path: dataclass parses, low-confidence None, None attrs handled,
mismatched lengths logged + None
- Backend missing: returns None gracefully
All 11 pass; `make ci-precheck` PASSED.
Closes #372.
2026-05-04 08:20:01 -07:00
|
|
|
|
#
|
|
|
|
|
|
# OCR backend uses ONNX Runtime regardless of Python version. The
|
|
|
|
|
|
# rapidocr ecosystem split into two flavors after 1.4.x:
|
|
|
|
|
|
# * rapidocr-onnxruntime 1.4.x — bundled-ORT package, capped at
|
|
|
|
|
|
# Python <3.13 by its requires-python metadata. Drop-in for our
|
|
|
|
|
|
# existing v1 tuple-shaped API call.
|
|
|
|
|
|
# * rapidocr 3.x — engine-agnostic core, supports Python 3.13+.
|
|
|
|
|
|
# Returns a RapidOCROutput dataclass (txts, scores, boxes, ...).
|
|
|
|
|
|
# Needs `onnxruntime` installed separately to use the ORT backend.
|
|
|
|
|
|
#
|
|
|
|
|
|
# `headroom/image/compressor.py` adapts both API shapes at runtime via
|
|
|
|
|
|
# a try/except cascade. See issue #372 for context.
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
image = [
|
|
|
|
|
|
"pillow>=10.0.0",
|
2026-04-08 11:33:28 -07:00
|
|
|
|
"sentencepiece>=0.1.99", # Required by SigLIP tokenizer (SiglipTokenizer)
|
fix: PR #372 — restore [image] extra on Python 3.13 via rapidocr 3.x adapter
Root cause: `headroom-ai[all]==0.20.16` fails to install on Python 3.13
because `rapidocr-onnxruntime` 1.4.0–1.4.4 wheels declare
`requires-python: <3.13,>=3.6`. After 1.4.x the rapidocr ecosystem
split: `rapidocr-onnxruntime` (bundled-ORT, capped at <3.13) vs
`rapidocr` 3.x (engine-agnostic core, supports 3.13+, returns
RapidOCROutput dataclass instead of v1's tuple).
Fix:
1. pyproject.toml — environment-marker hybrid in [image]:
- rapidocr-onnxruntime>=1.4.0,<2; python_version<'3.13'
- rapidocr>=3.0,<4; python_version>='3.13'
- onnxruntime>=1.7,<2; python_version>='3.13'
ORT remains the engine on every Python version; bundle and speed
unchanged, just split into two packages on 3.13+.
2. headroom/image/compressor.py — runtime adapter:
_resolve_rapidocr() tries v1 first, falls back to v3 when v1 is
missing, returns (None, None) when neither installed. Cached at
module scope. Detection at runtime (not Python-version-based) so
users can install either package on any Python version.
_ocr_extract branches on resolved api_version:
- v1: (list[(box, text, score)], elapsed) tuple — unchanged
- v3: RapidOCROutput dataclass with .txts / .scores / .boxes
attrs (each may be None when nothing detected)
Defensive None-handling, length-mismatch detection, structured
log events for both branches.
Smoke test (real install verified before commit):
pip install rapidocr onnxruntime pillow
→ result type: RapidOCROutput
→ fields: txts (None when empty), scores (None when empty), boxes
Confirms the v3 None-coercion is necessary.
Tests: 11 new unit tests in tests/test_image_ocr_api_compat.py covering:
- Resolver: v1 preferred, v3 fallback, both missing
- v1 path: tuple parses, low-confidence None, empty result None
- v3 path: dataclass parses, low-confidence None, None attrs handled,
mismatched lengths logged + None
- Backend missing: returns None gracefully
All 11 pass; `make ci-precheck` PASSED.
Closes #372.
2026-05-04 08:20:01 -07:00
|
|
|
|
# Python 3.6–3.12: keep the proven ORT-bundled package directly.
|
|
|
|
|
|
# ~15 MB ONNX models auto-downloaded on first use.
|
|
|
|
|
|
"rapidocr-onnxruntime>=1.4.0,<2; python_version<'3.13'",
|
|
|
|
|
|
# Python 3.13+: rapidocr-onnxruntime is unavailable (its wheels
|
|
|
|
|
|
# declare requires-python<3.13). Use the successor `rapidocr` 3.x
|
|
|
|
|
|
# core + `onnxruntime` engine; same ORT backend, just split into
|
|
|
|
|
|
# two packages. Total install size and inference speed unchanged.
|
|
|
|
|
|
"rapidocr>=3.0,<4; python_version>='3.13'",
|
|
|
|
|
|
"onnxruntime>=1.7,<2; python_version>='3.13'",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
]
|
|
|
|
|
|
# Report generation
|
|
|
|
|
|
reports = [
|
|
|
|
|
|
"jinja2>=3.0.0",
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
|
]
|
feat(transforms): tabular + spreadsheet (.xlsx/.xls) compression (#1128)
## Description
Adds a content-type-aware path for **tabular data** — CSV/TSV, markdown
tables, fixed-width text, and binary `.xlsx`/`.xls` spreadsheets — by
routing them through the existing, battle-tested `SmartCrusher` instead
of letting them fall through to `PLAIN_TEXT → Kompress`.
The pipeline already compressed tables losslessly when handed a JSON
array of records. This wires up the missing front door: detect tabular
text (and ingest binary spreadsheets), convert to JSON records, and
reuse `SmartCrusher.crush()`. No new compression algorithm.
Closes #
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- **Detection** (`content_detector.py`): new `ContentType.TABULAR` +
`_try_detect_tabular()` for CSV/TSV, markdown tables, and fixed-width
columns. Ordered after search/log (which also look "delimited") and
before code, with a prose-rejection guard so it never steals
`file:line:content` search output, `key: value` logs, or sentences with
incidental commas. Rust backend returns `plain_text` for unknown types
and the router already falls back to the Python detector, so **no Rust
change**.
- **Bridge** (`tabular_ingest.py`): stdlib parsers + `to_records()` + a
`TabularCompressor` that parses → JSON records → `SmartCrusher`
(lossless `csv-schema` first; lossy row-drop with reversible
`<<ccr:HASH>>` markers stays SmartCrusher's built-in fallback). Only
adopts a result when it actually saves bytes.
- **Spreadsheets** (`spreadsheet_ingest.py`): `.xlsx`/`.xls` → per-sheet
CSV text at the SDK boundary. Optional deps (`pip install
headroom-ai[spreadsheet]`) fail loudly with an install hint, never
silently degrade.
- **Routing** (`content_router.py`): `CompressionStrategy.TABULAR`,
`enable_tabular_compressor` flag, lazy getter, apply branch, strategy
maps, Kompress fallback eligibility.
- **SDK** (`compress.py`): `compress_spreadsheet(path, ...)` helper (one
message per sheet).
- **Packaging** (`pyproject.toml`): new `[spreadsheet]` extra;
`openpyxl` added to `[dev]` so the xlsx path is exercised in CI.
- **Docs/demo**: `examples/tabular_compression_demo.py` + README entry.
### Design note: lossless-only
Compact, all-unique tables with no query yield ~0 savings — this is
correct, not a bug. SmartCrusher returns
`skip:unique_entities_no_signal` and won't drop unique rows without a
duplicate/relevance signal. Real wins come from verbose/redundant tables
and query-driven selection. A pressure-driven lossy row sampler was
considered and intentionally not added.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_transforms_tabular.py -q
collected 20 items
tests/test_transforms_tabular.py .................... [100%]
============================== 20 passed in 7.15s ==============================
$ ruff check headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py
All checks passed!
$ mypy headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py
Success: no issues found in 2 source files
```
`tests/test_transforms_tabular.py` (20 tests): detection true positives
+ no-misroute negatives (search/log/JSON/prose), parser units (incl.
fixed-width), the CSV→SmartCrusher bridge, router routing + disable
flag, and `.xlsx` ingestion (skipif openpyxl missing) + error paths.
`spreadsheet_ingest` 100% / `tabular_ingest` 90% line coverage.
## Real Behavior Proof
- **Environment:** local checkout of `feat/tabular-compression`, Python
3.x, `pip install -e ".[dev]"`.
- **Exact command / steps:** `python
examples/tabular_compression_demo.py` (no API key required).
- **Observed result:**
```text
=== Raw tabular text (ContentRouter, char-level) ===
compact unique CSV strat=tabular chars 1306 -> 1072 ( 17.9% saved)
redundant CSV strat=tabular chars 2661 -> 1350 ( 49.3% saved)
verbose markdown strat=tabular chars 2019 -> 1580 ( 21.7% saved)
=== Full pipeline (real tokenizer) ===
redundant CSV tokens 768 -> 394 ( 48.7% saved)
=== Binary spreadsheet (.xlsx) ===
2-sheet workbook tokens 1092 -> 683 ( 37.5% saved)
```
- **Not tested:** legacy `.xls` binary path (needs optional `xlrd` +
binary fixture; `# pragma: no cover`); base64-embedded `.xlsx` inside
multimodal blocks (out of scope, noted as a follow-up).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Additional Notes
- CHANGELOG/version are intentionally untouched: this repo uses
**release-please**, which bumps the version and CHANGELOG via automated
`chore: release main` PRs, not per-feature PRs.
- The `.xls` path is `# pragma: no cover` (legacy, needs optional `xlrd`
+ a binary fixture).
- Follow-up (out of scope): base64-embedded `.xlsx` inside
tool-result/multimodal blocks; porting tabular parsers into the Rust
core for parity.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:30:20 -07:00
|
|
|
|
# Binary spreadsheet ingestion (.xlsx / .xls -> tabular text)
|
|
|
|
|
|
spreadsheet = [
|
|
|
|
|
|
"openpyxl>=3.1.0", # .xlsx
|
|
|
|
|
|
"xlrd>=2.0.1", # legacy .xls
|
|
|
|
|
|
]
|
2026-04-09 21:20:34 -05:00
|
|
|
|
# OpenTelemetry metrics export
|
|
|
|
|
|
otel = [
|
|
|
|
|
|
"opentelemetry-sdk>=1.24.0",
|
|
|
|
|
|
"opentelemetry-exporter-otlp-proto-http>=1.24.0",
|
|
|
|
|
|
]
|
2026-02-06 11:40:23 -06:00
|
|
|
|
# any-llm multi-provider backend (requires Python 3.11+)
|
|
|
|
|
|
anyllm = [
|
2026-04-04 13:45:37 -05:00
|
|
|
|
"any-llm-sdk>=1.0.0; python_version >= '3.11'",
|
2026-02-06 11:40:23 -06:00
|
|
|
|
]
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# LangChain integration
|
|
|
|
|
|
langchain = [
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
|
"langchain-core>=1.3.3,<4.0",
|
|
|
|
|
|
"langchain-openai>=1.1.14,<2.0",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
]
|
2026-01-16 16:00:13 -08:00
|
|
|
|
# Agno agent framework integration
|
|
|
|
|
|
agno = [
|
|
|
|
|
|
"agno>=1.0.0",
|
|
|
|
|
|
]
|
feat: Add AWS Strands Agents SDK integration
## Description
Add Headroom integration with AWS Strands Agents SDK, enabling automatic
context optimization and tool output compression for Strands-based agents.
Fixes #14
## Type of Change
- [x] New feature (non-breaking change that adds functionality)
- [x] Documentation update
## Changes Made
### Core Integration (`headroom/integrations/strands/`)
- **HeadroomHookProvider** - Implements Strands `HookProvider` interface for
automatic tool output compression via `AfterToolCallEvent`. Compresses
verbose tool outputs before they enter conversation context.
- **HeadroomStrandsModel** - Model wrapper that extends Strands `Model` base
class for message-level optimization. Implements all required abstract
methods: `stream()`, `get_config()`, `update_config()`, `structured_output()`.
- **Provider auto-detection** - Automatically detects appropriate Headroom
provider (Anthropic, OpenAI, Google) based on wrapped Strands model type.
- **`strands-agents` as optional dependency** - Install with
`pip install headroom-ai[strands]`
### Testing (`tests/integrations/test_strands/`)
- **Real integration tests (25 tests)** - Use actual AWS Bedrock API calls
with Claude 3 Haiku. Skip automatically when credentials unavailable.
- **Unit tests (57 tests)** - Mock-based tests for internal logic, edge cases,
and error handling. No credentials required.
### Demo (`examples/strands_bedrock_demo.py`)
- Interactive demo showcasing both integration patterns
- Visual before/after compression comparison with token savings
- 4 verbose tools (search, logs, database, metrics) demonstrating real savings
- Supports `--hook` and `--model` flags for individual demos
## Testing
All tests verified:
- [x] Unit tests pass (57 tests)
- [x] Integration tests pass (25 tests with real Bedrock API)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom/integrations/strands/`)
- [x] Formatting passes (`ruff format --check`)
- [x] Demo runs successfully with ~50% token savings
## Test Output
```
$ pytest tests/integrations/test_strands/ -v
=================== 82 passed in 90.09s ===================
$ ruff check headroom/integrations/strands/ --ignore E402
All checks passed!
$ mypy headroom/integrations/strands/ --ignore-missing-imports
Success: no issues found
```
## Demo Results
```
╭────────────────────────────────────────────────────────────╮
│ HeadroomHookProvider Results │
│────────────────────────────────────────────────────────────│
│ Tokens BEFORE compression: 51,961 │
│ Tokens AFTER compression: 25,658 │
│ Tokens SAVED: 26,303 (50.6%) │
╰────────────────────────────────────────────────────────────╯
```
2026-01-31 00:31:37 -08:00
|
|
|
|
# AWS Strands Agents SDK integration
|
|
|
|
|
|
strands = [
|
|
|
|
|
|
"strands-agents>=0.1.0",
|
|
|
|
|
|
]
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# MCP server for Claude Code integration
|
2026-02-02 11:05:24 -08:00
|
|
|
|
mcp = [
|
|
|
|
|
|
"mcp>=1.0.0",
|
|
|
|
|
|
"httpx>=0.24.0",
|
|
|
|
|
|
]
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# Voice filler detection
|
Add Click-based CLI with memory management commands
Refactor CLI from argparse to Click for better extensibility:
- New headroom/cli/ package with modular command structure
- Memory commands: list, show, stats, edit, delete, prune, purge, export, import
- Rich terminal output with tables, colors, and formatted stats
- Duration parsing for --older-than and --since flags (7d, 2w, 1m)
- Comprehensive tests using Click's CliRunner (55 tests, no mocks)
CLI structure:
headroom proxy - Start optimization proxy (migrated from argparse)
headroom memory ... - Memory management (new)
headroom evals ... - Evaluation commands (migrated, now under evals group)
Backwards compatibility maintained via headroom/cli.py shim.
2026-01-29 21:19:27 -08:00
|
|
|
|
voice = [
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
"onnxruntime>=1.16.0",
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
|
"transformers>=4.30.0,<6.0",
|
fix(deps): remediate dependency CVEs and publish SBOM (#1509)
## Description
Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.
This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).
**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).
**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.
**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.
**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.
## Testing
- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof
### Test Output
```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found
# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME INSTALLED TYPE VULNERABILITY SEVERITY
sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted
nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted
# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised
# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out
# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found
```
## Real Behavior Proof
- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)
## Additional Notes
**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.
Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.
**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
|
|
|
|
"torch>=2.12.1",
|
Add Click-based CLI with memory management commands
Refactor CLI from argparse to Click for better extensibility:
- New headroom/cli/ package with modular command structure
- Memory commands: list, show, stats, edit, delete, prune, purge, export, import
- Rich terminal output with tables, colors, and formatted stats
- Duration parsing for --older-than and --since flags (7d, 2w, 1m)
- Comprehensive tests using Click's CliRunner (55 tests, no mocks)
CLI structure:
headroom proxy - Start optimization proxy (migrated from argparse)
headroom memory ... - Memory management (new)
headroom evals ... - Evaluation commands (migrated, now under evals group)
Backwards compatibility maintained via headroom/cli.py shim.
2026-01-29 21:19:27 -08:00
|
|
|
|
]
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# Voice training (includes voice deps + training extras)
|
Add Click-based CLI with memory management commands
Refactor CLI from argparse to Click for better extensibility:
- New headroom/cli/ package with modular command structure
- Memory commands: list, show, stats, edit, delete, prune, purge, export, import
- Rich terminal output with tables, colors, and formatted stats
- Duration parsing for --older-than and --since flags (7d, 2w, 1m)
- Comprehensive tests using Click's CliRunner (55 tests, no mocks)
CLI structure:
headroom proxy - Start optimization proxy (migrated from argparse)
headroom memory ... - Memory management (new)
headroom evals ... - Evaluation commands (migrated, now under evals group)
Backwards compatibility maintained via headroom/cli.py shim.
2026-01-29 21:19:27 -08:00
|
|
|
|
voice-train = [
|
|
|
|
|
|
"headroom-ai[voice]",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
"datasets>=2.14.0",
|
|
|
|
|
|
"accelerate>=0.20.0",
|
Add Click-based CLI with memory management commands
Refactor CLI from argparse to Click for better extensibility:
- New headroom/cli/ package with modular command structure
- Memory commands: list, show, stats, edit, delete, prune, purge, export, import
- Rich terminal output with tables, colors, and formatted stats
- Duration parsing for --older-than and --since flags (7d, 2w, 1m)
- Comprehensive tests using Click's CliRunner (55 tests, no mocks)
CLI structure:
headroom proxy - Start optimization proxy (migrated from argparse)
headroom memory ... - Memory management (new)
headroom evals ... - Evaluation commands (migrated, now under evals group)
Backwards compatibility maintained via headroom/cli.py shim.
2026-01-29 21:19:27 -08:00
|
|
|
|
]
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# Evaluation framework
|
Add comprehensive evaluation framework for compression accuracy
- Add headroom.evals module with 12+ dataset loaders (HotpotQA, SQuAD,
Natural Questions, TriviaQA, MS MARCO, LongBench, NarrativeQA, BFCL,
ToolBench, CodeSearchNet, HumanEval, built-in tool outputs)
- Add before/after evaluation runner that compares LLM responses with
original vs compressed context
- Add metrics: F1 score, semantic similarity, exact match, ground truth
- Add CLI: python -m headroom.evals quick|benchmark|list|report
- Add [evals] extra to pyproject.toml for pip install headroom-ai[evals]
Fix ContentRouter to use LLMLingua for plain text compression:
- Route TEXT strategy through LLMLingua instead of heuristic TextCompressor
- Adjust LLMLingua compression rates for better accuracy (0.5 vs 0.25)
- HotpotQA now achieves 95% accuracy with 44% compression
Update documentation with evaluation framework section
Fix test isolation in test_toin.py (TOIN singleton persistence)
2026-01-22 09:17:28 -08:00
|
|
|
|
evals = [
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
"datasets>=2.14.0",
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
|
"sentence-transformers>=2.2.0,<6.0",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
"numpy>=1.24.0",
|
|
|
|
|
|
"scikit-learn>=1.3.0",
|
|
|
|
|
|
"anthropic>=0.18.0",
|
|
|
|
|
|
"openai>=1.0.0",
|
2026-01-22 23:58:54 -08:00
|
|
|
|
]
|
Add Click-based CLI with memory management commands
Refactor CLI from argparse to Click for better extensibility:
- New headroom/cli/ package with modular command structure
- Memory commands: list, show, stats, edit, delete, prune, purge, export, import
- Rich terminal output with tables, colors, and formatted stats
- Duration parsing for --older-than and --since flags (7d, 2w, 1m)
- Comprehensive tests using Click's CliRunner (55 tests, no mocks)
CLI structure:
headroom proxy - Start optimization proxy (migrated from argparse)
headroom memory ... - Memory management (new)
headroom evals ... - Evaluation commands (migrated, now under evals group)
Backwards compatibility maintained via headroom/cli.py shim.
2026-01-29 21:19:27 -08:00
|
|
|
|
# AWS Bedrock backend
|
|
|
|
|
|
bedrock = [
|
|
|
|
|
|
"boto3>=1.28.0",
|
|
|
|
|
|
]
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# HTML content extraction
|
2026-01-31 10:08:29 -08:00
|
|
|
|
html = [
|
|
|
|
|
|
"trafilatura>=1.6.0",
|
|
|
|
|
|
]
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# Comprehensive LLM benchmarks
|
2026-02-01 21:23:09 -08:00
|
|
|
|
benchmark = [
|
2026-05-11 13:35:24 -05:00
|
|
|
|
"lm-eval[api]>=0.4.0",
|
2026-02-01 21:23:09 -08:00
|
|
|
|
"openai>=1.0.0",
|
|
|
|
|
|
"anthropic>=0.18.0",
|
|
|
|
|
|
]
|
2026-01-07 11:36:44 -08:00
|
|
|
|
# Development dependencies
|
2026-01-06 23:16:58 -08:00
|
|
|
|
dev = [
|
|
|
|
|
|
"pytest>=7.0.0",
|
|
|
|
|
|
"pytest-cov>=4.0.0",
|
|
|
|
|
|
"pytest-asyncio>=0.21.0",
|
|
|
|
|
|
"ruff>=0.1.0",
|
|
|
|
|
|
"mypy>=1.0.0",
|
2026-01-21 21:49:29 -08:00
|
|
|
|
"pre-commit>=3.0.0",
|
2026-01-06 23:16:58 -08:00
|
|
|
|
"openai>=1.0.0",
|
2026-01-07 11:36:44 -08:00
|
|
|
|
"anthropic>=0.18.0",
|
2026-06-16 22:11:12 -04:00
|
|
|
|
"litellm>=1.86.2,<2.0; python_version < '3.14'", # see core deps note (GH #956)
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
"fastapi>=0.100.0",
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
|
"uvicorn>=0.23.0,<1.0",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
"httpx[http2]>=0.24.0",
|
2026-04-18 08:22:04 +07:00
|
|
|
|
"websockets>=13.0",
|
2026-04-09 21:20:34 -05:00
|
|
|
|
"opentelemetry-sdk>=1.24.0",
|
|
|
|
|
|
"opentelemetry-exporter-otlp-proto-http>=1.24.0",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
"ollama>=0.4.0",
|
|
|
|
|
|
"langchain-ollama>=0.2.0",
|
|
|
|
|
|
"hnswlib>=0.8.0",
|
|
|
|
|
|
"sqlite-vec>=0.1.6",
|
chore(deps): loosen over-pinned constraints and add upper bounds (#538)
## What
Loosen over-pinned Python dependency constraints and add missing upper
bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv
builder version.
## Why
Several dependencies had constraints that either blocked security
patches or allowed silent major-version jumps:
- `litellm==1.82.3` was an exact pin — every security patch release
requires a manual lockfile bump
- `transformers`, `sentence-transformers` had no upper bound and have
already crossed major version boundaries without a constraint gate
- `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x
in the wild
- `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is
already 1.0.11
- `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had
no upper bound on a range with active major-version churn
- `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch
releases behind the current 5.x LTS
- `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18`
## How
Constraint changes only — no code changes, no `uv lock --upgrade`. The
existing locked versions all satisfy the new bounds (we added caps, not
floors). `uv` re-resolved the lockfile to format revision 3 (adds
`upload-time` metadata fields) and cleaned up the defunct `llmlingua`
extra entries.
| Dependency | Before | After |
|---|---|---|
| `litellm` | `==1.82.3` | `>=1.82.3,<2.0` |
| `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` |
| `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` |
| `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` |
| `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` |
| `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` |
| `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` |
| `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` |
| `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` |
| neo4j Docker image | `5.15.0` | `5.26` |
| uv (Dockerfile ARG) | `0.11.16` | `0.11.18` |
## Breaking changes
None. All currently installed versions fall within the new ranges.
Installers that previously resolved `litellm` to an older exact pin may
now resolve newer patch releases — which is the desired behavior.
---------
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
|
|
|
|
"sentence-transformers>=2.2.0,<6.0",
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
"numpy>=1.24.0",
|
feat(transforms): tabular + spreadsheet (.xlsx/.xls) compression (#1128)
## Description
Adds a content-type-aware path for **tabular data** — CSV/TSV, markdown
tables, fixed-width text, and binary `.xlsx`/`.xls` spreadsheets — by
routing them through the existing, battle-tested `SmartCrusher` instead
of letting them fall through to `PLAIN_TEXT → Kompress`.
The pipeline already compressed tables losslessly when handed a JSON
array of records. This wires up the missing front door: detect tabular
text (and ingest binary spreadsheets), convert to JSON records, and
reuse `SmartCrusher.crush()`. No new compression algorithm.
Closes #
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- **Detection** (`content_detector.py`): new `ContentType.TABULAR` +
`_try_detect_tabular()` for CSV/TSV, markdown tables, and fixed-width
columns. Ordered after search/log (which also look "delimited") and
before code, with a prose-rejection guard so it never steals
`file:line:content` search output, `key: value` logs, or sentences with
incidental commas. Rust backend returns `plain_text` for unknown types
and the router already falls back to the Python detector, so **no Rust
change**.
- **Bridge** (`tabular_ingest.py`): stdlib parsers + `to_records()` + a
`TabularCompressor` that parses → JSON records → `SmartCrusher`
(lossless `csv-schema` first; lossy row-drop with reversible
`<<ccr:HASH>>` markers stays SmartCrusher's built-in fallback). Only
adopts a result when it actually saves bytes.
- **Spreadsheets** (`spreadsheet_ingest.py`): `.xlsx`/`.xls` → per-sheet
CSV text at the SDK boundary. Optional deps (`pip install
headroom-ai[spreadsheet]`) fail loudly with an install hint, never
silently degrade.
- **Routing** (`content_router.py`): `CompressionStrategy.TABULAR`,
`enable_tabular_compressor` flag, lazy getter, apply branch, strategy
maps, Kompress fallback eligibility.
- **SDK** (`compress.py`): `compress_spreadsheet(path, ...)` helper (one
message per sheet).
- **Packaging** (`pyproject.toml`): new `[spreadsheet]` extra;
`openpyxl` added to `[dev]` so the xlsx path is exercised in CI.
- **Docs/demo**: `examples/tabular_compression_demo.py` + README entry.
### Design note: lossless-only
Compact, all-unique tables with no query yield ~0 savings — this is
correct, not a bug. SmartCrusher returns
`skip:unique_entities_no_signal` and won't drop unique rows without a
duplicate/relevance signal. Real wins come from verbose/redundant tables
and query-driven selection. A pressure-driven lossy row sampler was
considered and intentionally not added.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_transforms_tabular.py -q
collected 20 items
tests/test_transforms_tabular.py .................... [100%]
============================== 20 passed in 7.15s ==============================
$ ruff check headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py
All checks passed!
$ mypy headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py
Success: no issues found in 2 source files
```
`tests/test_transforms_tabular.py` (20 tests): detection true positives
+ no-misroute negatives (search/log/JSON/prose), parser units (incl.
fixed-width), the CSV→SmartCrusher bridge, router routing + disable
flag, and `.xlsx` ingestion (skipif openpyxl missing) + error paths.
`spreadsheet_ingest` 100% / `tabular_ingest` 90% line coverage.
## Real Behavior Proof
- **Environment:** local checkout of `feat/tabular-compression`, Python
3.x, `pip install -e ".[dev]"`.
- **Exact command / steps:** `python
examples/tabular_compression_demo.py` (no API key required).
- **Observed result:**
```text
=== Raw tabular text (ContentRouter, char-level) ===
compact unique CSV strat=tabular chars 1306 -> 1072 ( 17.9% saved)
redundant CSV strat=tabular chars 2661 -> 1350 ( 49.3% saved)
verbose markdown strat=tabular chars 2019 -> 1580 ( 21.7% saved)
=== Full pipeline (real tokenizer) ===
redundant CSV tokens 768 -> 394 ( 48.7% saved)
=== Binary spreadsheet (.xlsx) ===
2-sheet workbook tokens 1092 -> 683 ( 37.5% saved)
```
- **Not tested:** legacy `.xls` binary path (needs optional `xlrd` +
binary fixture; `# pragma: no cover`); base64-embedded `.xlsx` inside
multimodal blocks (out of scope, noted as a follow-up).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Additional Notes
- CHANGELOG/version are intentionally untouched: this repo uses
**release-please**, which bumps the version and CHANGELOG via automated
`chore: release main` PRs, not per-feature PRs.
- The `.xls` path is `# pragma: no cover` (legacy, needs optional `xlrd`
+ a binary fixture).
- Follow-up (out of scope): base64-embedded `.xlsx` inside
tool-result/multimodal blocks; porting tabular parsers into the Rust
core for parity.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:30:20 -07:00
|
|
|
|
"openpyxl>=3.1.0", # exercises spreadsheet_ingest (.xlsx) in the test suite
|
2026-01-06 23:16:58 -08:00
|
|
|
|
]
|
Slim core dependencies: 2.5GB → 195MB install size
Move heavy deps to optional extras: sentence-transformers, torch,
numpy, pillow, datasets, accelerate out of core. Remove unused deps
entirely (semantic-router, protobuf, sentencepiece).
New extras: [ml] for Kompress, [image] for image compression,
[langchain] for LangChain integration. Guard memory/image imports
so core install works without numpy/torch.
Core (tiktoken, pydantic, litellm, click, rich) gives full
compression: SmartCrusher, ContentRouter, CCR, TOIN, CLI.
2026-03-13 15:35:49 -07:00
|
|
|
|
# All optional dependencies (everything you need)
|
fix(deps): remediate dependency CVEs and publish SBOM (#1509)
## Description
Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.
This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).
**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).
**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.
**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.
**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.
## Testing
- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof
### Test Output
```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found
# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME INSTALLED TYPE VULNERABILITY SEVERITY
sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted
nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted
# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised
# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out
# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found
```
## Real Behavior Proof
- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)
## Additional Notes
**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.
Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.
**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
|
|
|
|
#
|
|
|
|
|
|
# `benchmark` is deliberately EXCLUDED from `[all]`. It installs the
|
|
|
|
|
|
# EleutherAI lm-evaluation-harness (lm-eval), which headroom invokes as an
|
|
|
|
|
|
# external subprocess (`python -m lm_eval`) — it is never imported as a
|
|
|
|
|
|
# library, so it is not a true runtime dependency. lm-eval pulls two
|
|
|
|
|
|
# transitive deps with unpatchable High CVEs (sqlitedict CVE-2024-35515,
|
|
|
|
|
|
# nltk CVE-2026-54293 via rouge-score), neither of which has an upstream
|
|
|
|
|
|
# fix. Keeping `benchmark` out of `[all]` means `pip install
|
|
|
|
|
|
# headroom-ai[all]` is CVE-free; researchers who need the accuracy harness
|
|
|
|
|
|
# opt in explicitly with `pip install headroom-ai[benchmark]`.
|
2026-01-07 11:36:44 -08:00
|
|
|
|
all = [
|
fix(deps): remediate dependency CVEs and publish SBOM (#1509)
## Description
Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.
This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).
**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).
**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.
**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.
**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.
## Testing
- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof
### Test Output
```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found
# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME INSTALLED TYPE VULNERABILITY SEVERITY
sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted
nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted
# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised
# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out
# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found
```
## Real Behavior Proof
- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)
## Additional Notes
**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.
Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.
**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
|
|
|
|
"headroom-ai[proxy,code,ml,memory,relevance,image,reports,otel,evals,voice,html,mcp,spreadsheet]",
|
2026-01-07 11:36:44 -08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
[project.scripts]
|
|
|
|
|
|
headroom = "headroom.cli:main"
|
2026-01-06 23:16:58 -08:00
|
|
|
|
|
|
|
|
|
|
[project.urls]
|
docs: improve discoverability for AI agents and search crawlers
Several signals AI agents and search engines use to discover and
install a project were misaligned or missing:
* ``docs/app/layout.tsx`` set ``metadataBase`` to
``https://chopratejas.github.io/headroom/`` while the live docs run
on Vercel — every page's ``og:url`` and ``twitter:url`` resolved to
a URL that returns 404 for ``/llms.txt``. Now points at the live
Vercel host (overridable via ``NEXT_PUBLIC_SITE_URL`` for a future
custom domain). Adds explicit ``openGraph`` and ``twitter`` metadata
so social shares render a card with the project's pitch.
* No ``llms.txt`` at the GitHub repo root. AI agents crawling
``github.com/chopratejas/headroom/`` saw only the README. The new
``llms.txt`` follows the llmstxt.org convention: 1-line pitch,
canonical docs links, copy-paste install commands (pip / npm /
Docker / proxy / ``headroom wrap``), and entry points for the
library, proxy, MCP server, and SDK integrations. Points at the
Fumadocs-generated ``/llms.txt`` and ``/llms-full.txt`` for the
full picture.
* ``pyproject.toml`` ``Documentation`` URL pointed at the GitHub
README anchor. Updated to point at the docs site so PyPI visitors
land on searchable docs, and adds an ``AI / LLM Index`` URL
pointing at the Fumadocs ``/llms.txt``.
* No explicit AI-bot allow list. Added ``docs/app/robots.ts`` (Next
13+ App Router convention) with explicit allows for GPTBot,
ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot,
ChatGPT-User, Cohere-AI, CCBot, and Applebot-Extended. Wildcard
allow as the catch-all. Advertises the sitemap.
* No ``sitemap.xml`` route. Added ``docs/app/sitemap.ts`` that pulls
every Fumadocs page out of ``source`` (same source backing
``/llms.txt``, search, and OG images) so search and AI crawlers
can enumerate doc pages without scraping HTML.
* README didn't tell AI agents where to look. Added a 2-line
pointer near the top nav row: read ``/llms.txt`` here, or fetch
the live index / full docs blob.
Also tightened the GitHub repo description and added five topics
(``claude-code``, ``cursor``, ``tokens``, ``prompt-engineering``,
``typescript``) via ``gh repo edit`` — that's already live on the
repo, not part of this commit.
No Python or Rust code changes; ``make ci-precheck`` was run to
confirm the test slice still passes.
2026-05-13 17:36:06 -07:00
|
|
|
|
Homepage = "https://headroom-docs.vercel.app"
|
|
|
|
|
|
Documentation = "https://headroom-docs.vercel.app/docs"
|
2026-01-10 15:11:08 -08:00
|
|
|
|
Repository = "https://github.com/chopratejas/headroom"
|
|
|
|
|
|
Issues = "https://github.com/chopratejas/headroom/issues"
|
|
|
|
|
|
Changelog = "https://github.com/chopratejas/headroom/blob/main/CHANGELOG.md"
|
docs: improve discoverability for AI agents and search crawlers
Several signals AI agents and search engines use to discover and
install a project were misaligned or missing:
* ``docs/app/layout.tsx`` set ``metadataBase`` to
``https://chopratejas.github.io/headroom/`` while the live docs run
on Vercel — every page's ``og:url`` and ``twitter:url`` resolved to
a URL that returns 404 for ``/llms.txt``. Now points at the live
Vercel host (overridable via ``NEXT_PUBLIC_SITE_URL`` for a future
custom domain). Adds explicit ``openGraph`` and ``twitter`` metadata
so social shares render a card with the project's pitch.
* No ``llms.txt`` at the GitHub repo root. AI agents crawling
``github.com/chopratejas/headroom/`` saw only the README. The new
``llms.txt`` follows the llmstxt.org convention: 1-line pitch,
canonical docs links, copy-paste install commands (pip / npm /
Docker / proxy / ``headroom wrap``), and entry points for the
library, proxy, MCP server, and SDK integrations. Points at the
Fumadocs-generated ``/llms.txt`` and ``/llms-full.txt`` for the
full picture.
* ``pyproject.toml`` ``Documentation`` URL pointed at the GitHub
README anchor. Updated to point at the docs site so PyPI visitors
land on searchable docs, and adds an ``AI / LLM Index`` URL
pointing at the Fumadocs ``/llms.txt``.
* No explicit AI-bot allow list. Added ``docs/app/robots.ts`` (Next
13+ App Router convention) with explicit allows for GPTBot,
ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot,
ChatGPT-User, Cohere-AI, CCBot, and Applebot-Extended. Wildcard
allow as the catch-all. Advertises the sitemap.
* No ``sitemap.xml`` route. Added ``docs/app/sitemap.ts`` that pulls
every Fumadocs page out of ``source`` (same source backing
``/llms.txt``, search, and OG images) so search and AI crawlers
can enumerate doc pages without scraping HTML.
* README didn't tell AI agents where to look. Added a 2-line
pointer near the top nav row: read ``/llms.txt`` here, or fetch
the live index / full docs blob.
Also tightened the GitHub repo description and added five topics
(``claude-code``, ``cursor``, ``tokens``, ``prompt-engineering``,
``typescript``) via ``gh repo edit`` — that's already live on the
repo, not part of this commit.
No Python or Rust code changes; ``make ci-precheck`` was run to
confirm the test slice still passes.
2026-05-13 17:36:06 -07:00
|
|
|
|
# llms.txt convention (llmstxt.org) — point AI agents / LLM crawlers
|
|
|
|
|
|
# at the auto-generated docs index so they can resolve install paths
|
|
|
|
|
|
# and entry points without a follow-up fetch.
|
|
|
|
|
|
"AI / LLM Index" = "https://headroom-docs.vercel.app/llms.txt"
|
2026-01-06 23:16:58 -08:00
|
|
|
|
|
refactor: single-wheel maturin build backend (fixes #355)
Eliminates the dual-package architecture that was the root cause of #355.
`pip install headroom-ai` now produces ONE wheel containing both the Python
source (headroom/*.py) and the compiled Rust extension (headroom/_core.so).
No more separate `headroom-core-py` package, no more chicken-and-egg with
PyPI publication, no more wheelhouse / PIP_FIND_LINKS / composite-action
plumbing in CI.
This is the canonical pattern used by cryptography, polars, ruff,
pydantic-core, and other Rust-as-core Python packages. Honors the
"Rust as core engine" direction.
## What changed
- pyproject.toml: `[build-system]` swapped from hatchling to maturin.
`[tool.hatch.*]` deleted; `[tool.maturin]` added pointing at
`crates/headroom-py/Cargo.toml` for the cdylib. `python-source = "."`
picks up the root `headroom/` package directly (dashboard HTML
templates and other non-Python files included automatically).
- crates/headroom-py/pyproject.toml: deleted. The crate is no longer a
separate published package; its Cargo.toml stays as the cdylib build
target invoked via `[tool.maturin] manifest-path`.
- crates/headroom-py/python/: deleted (placeholder layout for the old
separate package).
## CI updates
- ci.yml: `test` / `test-extras` / `test-agno` jobs simplified — Rust
toolchain set up before `pip install -e .` (which now invokes maturin
via build-system). Removed the "build wheel + symlink .so" dance.
`build` job swapped from `python -m build` (hatch) to
`maturin build` + `maturin sdist`.
- release.yml: collapsed dual-package matrix into one. New `build-wheels`
matrix produces cross-platform wheels for cp310/11/12/13 ×
{linux x86_64, linux aarch64, macos x86_64, macos aarch64}. New
`collect-dist` aggregator merges artifacts. publish-pypi consumes the
merged dist.
- init-native-e2e.yml: dropped windows-latest from the matrix —
upstream `esaxx-rs` (/MT) and `ort-sys` (/MD) link with conflicting
MSVC C runtime libraries, so the Rust extension cannot build for
win_amd64 today. Tracked as a follow-up; not a blocker for Linux+macOS.
- headroom-e2e-setup: composite action now sets up Rust toolchain +
Swatinem/rust-cache before `pip install -e .[proxy]`.
- eval.yml, publish.yml, rust.yml: same pattern — rust toolchain before
install. rust.yml's wheels job builds from root pyproject.toml (no
more `-m crates/headroom-py/Cargo.toml`).
- e2e/init/Dockerfile, e2e/wrap/Dockerfile: install rust + maturin in
the build stage; copy `crates/` + workspace `Cargo.toml/lock` so the
install can build the extension. Dropped `HEADROOM_REQUIRE_RUST_CORE=false`
from wrap-e2e — the image now ships the full Rust core.
- Dockerfile (main): simplified — no more Layer 2/3 dance with
`headroom-core-py` install + symlink. Single `uv pip install` builds
+ installs everything.
- .devcontainer/Dockerfile: rust toolchain + libssl-dev + maturin
added so `uv sync` builds the extension inside the devcontainer.
## Lockfile + script
- uv.lock: regenerated. No `headroom-core-py` entries remain.
- scripts/build_rust_extension.sh: simplified from a symlink-into-tree
workaround to a thin wrapper around `pip install -e .`. The maturin
build-backend handles placement automatically.
## Local validation (all green on macOS aarch64)
1. Clean venv `pip install -e .` → `from headroom._core import …` works.
2. `maturin build --release` → 13.8 MB wheel, 336 files including
`headroom/_core.cpython-311-darwin.so` (32 MB cdylib) and
`headroom/dashboard/templates/dashboard.html`.
3. `pip install <wheel>` in fresh venv → import works.
4. Wheel contents verified via `unzip -l`.
5. `pytest tests/test_transforms/test_diff_compressor.py` — 29 passed.
6. `pytest tests/test_relevance.py` — 30 passed.
7. `cargo build --workspace` + `cargo test --workspace` — all green.
8. `make ci-precheck` — 176 Python tests + Rust + commitlint green.
## Migration notes
Users on `pip install headroom-ai` get the Rust core automatically
(linux + macos wheels). sdist installs require rust toolchain available
locally — pip will build via maturin.
Closes #355
Supersedes #357 (workarounds-based fix abandoned in favor of
architectural fix)
2026-05-03 13:16:41 -07:00
|
|
|
|
# Maturin builds a single wheel containing both the Python source under
|
|
|
|
|
|
# `headroom/` AND the compiled Rust extension `headroom/_core.so` (cdylib
|
|
|
|
|
|
# from `crates/headroom-py`). One `pip install headroom-ai` ships everything
|
|
|
|
|
|
# atomically — no separate `headroom-core-py` package, no chicken-and-egg,
|
|
|
|
|
|
# no PIP_FIND_LINKS plumbing. Phase A0's runtime fail-loud check still
|
|
|
|
|
|
# exists but only fires if someone forces an sdist install on a platform
|
|
|
|
|
|
# without a wheel and the rust toolchain isn't available to compile it.
|
fix(deps): remediate dependency CVEs and publish SBOM (#1509)
## Description
Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.
This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).
**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).
**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.
**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.
**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.
## Testing
- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof
### Test Output
```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found
# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME INSTALLED TYPE VULNERABILITY SEVERITY
sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted
nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted
# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised
# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out
# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found
```
## Real Behavior Proof
- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)
## Additional Notes
**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.
Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.
**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
|
|
|
|
# Constrain transitive dependencies that have CVEs requiring minimum versions.
|
|
|
|
|
|
# These packages don't appear as direct headroom deps but are pulled in
|
|
|
|
|
|
# transitively; the floor pins below ensure uv resolves to patched versions.
|
|
|
|
|
|
[tool.uv]
|
|
|
|
|
|
constraint-dependencies = [
|
|
|
|
|
|
# GHSA-5239-wwwm-4pmq (Low) — transitive via rich; fix at 2.20.0
|
|
|
|
|
|
"pygments>=2.20.0",
|
|
|
|
|
|
# GHSA-4xgf-cpjx-pc3j (Medium) — transitive via mcp; fix at 2.14.2
|
|
|
|
|
|
"pydantic-settings>=2.14.2",
|
|
|
|
|
|
# GHSA-mv93-w799-cj2w + 4 others (High) — transitive via lm-eval; fix at 3.1.50
|
|
|
|
|
|
"gitpython>=3.1.50",
|
|
|
|
|
|
# GHSA-f4xh-w4cj-qxq8 (High) — transitive via langchain-core; fix at 0.8.18
|
|
|
|
|
|
"langsmith>=0.9.0",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-05-03 13:52:16 -07:00
|
|
|
|
# Pin the project's package index to public PyPI. Without this, `uv lock`
|
|
|
|
|
|
# inherits the developer's user-level `~/.config/uv/uv.toml` index
|
|
|
|
|
|
# setting — including private/internal mirrors like
|
|
|
|
|
|
# `pypi.netflix.net/simple` — and bakes those URLs into uv.lock, which
|
|
|
|
|
|
# then breaks CI on every public runner that can't reach the mirror.
|
|
|
|
|
|
# Declaring the index in pyproject.toml makes the project authoritative
|
|
|
|
|
|
# regardless of who runs `uv lock`.
|
|
|
|
|
|
[[tool.uv.index]]
|
|
|
|
|
|
name = "pypi"
|
|
|
|
|
|
url = "https://pypi.org/simple/"
|
|
|
|
|
|
default = true
|
|
|
|
|
|
|
refactor: single-wheel maturin build backend (fixes #355)
Eliminates the dual-package architecture that was the root cause of #355.
`pip install headroom-ai` now produces ONE wheel containing both the Python
source (headroom/*.py) and the compiled Rust extension (headroom/_core.so).
No more separate `headroom-core-py` package, no more chicken-and-egg with
PyPI publication, no more wheelhouse / PIP_FIND_LINKS / composite-action
plumbing in CI.
This is the canonical pattern used by cryptography, polars, ruff,
pydantic-core, and other Rust-as-core Python packages. Honors the
"Rust as core engine" direction.
## What changed
- pyproject.toml: `[build-system]` swapped from hatchling to maturin.
`[tool.hatch.*]` deleted; `[tool.maturin]` added pointing at
`crates/headroom-py/Cargo.toml` for the cdylib. `python-source = "."`
picks up the root `headroom/` package directly (dashboard HTML
templates and other non-Python files included automatically).
- crates/headroom-py/pyproject.toml: deleted. The crate is no longer a
separate published package; its Cargo.toml stays as the cdylib build
target invoked via `[tool.maturin] manifest-path`.
- crates/headroom-py/python/: deleted (placeholder layout for the old
separate package).
## CI updates
- ci.yml: `test` / `test-extras` / `test-agno` jobs simplified — Rust
toolchain set up before `pip install -e .` (which now invokes maturin
via build-system). Removed the "build wheel + symlink .so" dance.
`build` job swapped from `python -m build` (hatch) to
`maturin build` + `maturin sdist`.
- release.yml: collapsed dual-package matrix into one. New `build-wheels`
matrix produces cross-platform wheels for cp310/11/12/13 ×
{linux x86_64, linux aarch64, macos x86_64, macos aarch64}. New
`collect-dist` aggregator merges artifacts. publish-pypi consumes the
merged dist.
- init-native-e2e.yml: dropped windows-latest from the matrix —
upstream `esaxx-rs` (/MT) and `ort-sys` (/MD) link with conflicting
MSVC C runtime libraries, so the Rust extension cannot build for
win_amd64 today. Tracked as a follow-up; not a blocker for Linux+macOS.
- headroom-e2e-setup: composite action now sets up Rust toolchain +
Swatinem/rust-cache before `pip install -e .[proxy]`.
- eval.yml, publish.yml, rust.yml: same pattern — rust toolchain before
install. rust.yml's wheels job builds from root pyproject.toml (no
more `-m crates/headroom-py/Cargo.toml`).
- e2e/init/Dockerfile, e2e/wrap/Dockerfile: install rust + maturin in
the build stage; copy `crates/` + workspace `Cargo.toml/lock` so the
install can build the extension. Dropped `HEADROOM_REQUIRE_RUST_CORE=false`
from wrap-e2e — the image now ships the full Rust core.
- Dockerfile (main): simplified — no more Layer 2/3 dance with
`headroom-core-py` install + symlink. Single `uv pip install` builds
+ installs everything.
- .devcontainer/Dockerfile: rust toolchain + libssl-dev + maturin
added so `uv sync` builds the extension inside the devcontainer.
## Lockfile + script
- uv.lock: regenerated. No `headroom-core-py` entries remain.
- scripts/build_rust_extension.sh: simplified from a symlink-into-tree
workaround to a thin wrapper around `pip install -e .`. The maturin
build-backend handles placement automatically.
## Local validation (all green on macOS aarch64)
1. Clean venv `pip install -e .` → `from headroom._core import …` works.
2. `maturin build --release` → 13.8 MB wheel, 336 files including
`headroom/_core.cpython-311-darwin.so` (32 MB cdylib) and
`headroom/dashboard/templates/dashboard.html`.
3. `pip install <wheel>` in fresh venv → import works.
4. Wheel contents verified via `unzip -l`.
5. `pytest tests/test_transforms/test_diff_compressor.py` — 29 passed.
6. `pytest tests/test_relevance.py` — 30 passed.
7. `cargo build --workspace` + `cargo test --workspace` — all green.
8. `make ci-precheck` — 176 Python tests + Rust + commitlint green.
## Migration notes
Users on `pip install headroom-ai` get the Rust core automatically
(linux + macos wheels). sdist installs require rust toolchain available
locally — pip will build via maturin.
Closes #355
Supersedes #357 (workarounds-based fix abandoned in favor of
architectural fix)
2026-05-03 13:16:41 -07:00
|
|
|
|
[tool.maturin]
|
|
|
|
|
|
# Where the Python package lives. With `python-source = "."` and the
|
|
|
|
|
|
# package directory `headroom/` at repo root, maturin includes every file
|
|
|
|
|
|
# under `headroom/` in the wheel — that picks up the dashboard HTML
|
2026-05-07 16:29:05 -07:00
|
|
|
|
# templates and bundled YAML configs. `LICENSE` and `NOTICE` are listed
|
|
|
|
|
|
# explicitly because maturin sdists do not get the package-directory
|
|
|
|
|
|
# treatment wheels do, and PEP 639 auto-discovery emits both files into
|
|
|
|
|
|
# `License-File:` metadata — PyPI rejects sdists whose declared license
|
|
|
|
|
|
# files are missing from the tarball with `400 License-File X does not
|
|
|
|
|
|
# exist in distribution file`.
|
|
|
|
|
|
include = [
|
|
|
|
|
|
{ path = "LICENSE", format = "sdist" },
|
|
|
|
|
|
{ path = "NOTICE", format = "sdist" },
|
|
|
|
|
|
]
|
refactor: single-wheel maturin build backend (fixes #355)
Eliminates the dual-package architecture that was the root cause of #355.
`pip install headroom-ai` now produces ONE wheel containing both the Python
source (headroom/*.py) and the compiled Rust extension (headroom/_core.so).
No more separate `headroom-core-py` package, no more chicken-and-egg with
PyPI publication, no more wheelhouse / PIP_FIND_LINKS / composite-action
plumbing in CI.
This is the canonical pattern used by cryptography, polars, ruff,
pydantic-core, and other Rust-as-core Python packages. Honors the
"Rust as core engine" direction.
## What changed
- pyproject.toml: `[build-system]` swapped from hatchling to maturin.
`[tool.hatch.*]` deleted; `[tool.maturin]` added pointing at
`crates/headroom-py/Cargo.toml` for the cdylib. `python-source = "."`
picks up the root `headroom/` package directly (dashboard HTML
templates and other non-Python files included automatically).
- crates/headroom-py/pyproject.toml: deleted. The crate is no longer a
separate published package; its Cargo.toml stays as the cdylib build
target invoked via `[tool.maturin] manifest-path`.
- crates/headroom-py/python/: deleted (placeholder layout for the old
separate package).
## CI updates
- ci.yml: `test` / `test-extras` / `test-agno` jobs simplified — Rust
toolchain set up before `pip install -e .` (which now invokes maturin
via build-system). Removed the "build wheel + symlink .so" dance.
`build` job swapped from `python -m build` (hatch) to
`maturin build` + `maturin sdist`.
- release.yml: collapsed dual-package matrix into one. New `build-wheels`
matrix produces cross-platform wheels for cp310/11/12/13 ×
{linux x86_64, linux aarch64, macos x86_64, macos aarch64}. New
`collect-dist` aggregator merges artifacts. publish-pypi consumes the
merged dist.
- init-native-e2e.yml: dropped windows-latest from the matrix —
upstream `esaxx-rs` (/MT) and `ort-sys` (/MD) link with conflicting
MSVC C runtime libraries, so the Rust extension cannot build for
win_amd64 today. Tracked as a follow-up; not a blocker for Linux+macOS.
- headroom-e2e-setup: composite action now sets up Rust toolchain +
Swatinem/rust-cache before `pip install -e .[proxy]`.
- eval.yml, publish.yml, rust.yml: same pattern — rust toolchain before
install. rust.yml's wheels job builds from root pyproject.toml (no
more `-m crates/headroom-py/Cargo.toml`).
- e2e/init/Dockerfile, e2e/wrap/Dockerfile: install rust + maturin in
the build stage; copy `crates/` + workspace `Cargo.toml/lock` so the
install can build the extension. Dropped `HEADROOM_REQUIRE_RUST_CORE=false`
from wrap-e2e — the image now ships the full Rust core.
- Dockerfile (main): simplified — no more Layer 2/3 dance with
`headroom-core-py` install + symlink. Single `uv pip install` builds
+ installs everything.
- .devcontainer/Dockerfile: rust toolchain + libssl-dev + maturin
added so `uv sync` builds the extension inside the devcontainer.
## Lockfile + script
- uv.lock: regenerated. No `headroom-core-py` entries remain.
- scripts/build_rust_extension.sh: simplified from a symlink-into-tree
workaround to a thin wrapper around `pip install -e .`. The maturin
build-backend handles placement automatically.
## Local validation (all green on macOS aarch64)
1. Clean venv `pip install -e .` → `from headroom._core import …` works.
2. `maturin build --release` → 13.8 MB wheel, 336 files including
`headroom/_core.cpython-311-darwin.so` (32 MB cdylib) and
`headroom/dashboard/templates/dashboard.html`.
3. `pip install <wheel>` in fresh venv → import works.
4. Wheel contents verified via `unzip -l`.
5. `pytest tests/test_transforms/test_diff_compressor.py` — 29 passed.
6. `pytest tests/test_relevance.py` — 30 passed.
7. `cargo build --workspace` + `cargo test --workspace` — all green.
8. `make ci-precheck` — 176 Python tests + Rust + commitlint green.
## Migration notes
Users on `pip install headroom-ai` get the Rust core automatically
(linux + macos wheels). sdist installs require rust toolchain available
locally — pip will build via maturin.
Closes #355
Supersedes #357 (workarounds-based fix abandoned in favor of
architectural fix)
2026-05-03 13:16:41 -07:00
|
|
|
|
python-source = "."
|
|
|
|
|
|
module-name = "headroom._core"
|
|
|
|
|
|
# The cdylib source lives under `crates/headroom-py`. Maturin invokes
|
|
|
|
|
|
# `cargo build` with this manifest to produce `_core.cdylib`, then injects
|
|
|
|
|
|
# the resulting `.so` into the wheel at `headroom/_core.so`.
|
|
|
|
|
|
manifest-path = "crates/headroom-py/Cargo.toml"
|
|
|
|
|
|
features = ["extension-module"]
|
|
|
|
|
|
# Forbid building without the cdylib feature — bare `cargo build` won't
|
|
|
|
|
|
# produce a usable Python extension. Maturin's default `bindings` is "pyo3"
|
|
|
|
|
|
# which is correct here (see `crates/headroom-py/src/`).
|
|
|
|
|
|
bindings = "pyo3"
|
2026-01-07 11:36:44 -08:00
|
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
|
[tool.ruff]
|
|
|
|
|
|
target-version = "py310"
|
|
|
|
|
|
line-length = 100
|
|
|
|
|
|
|
|
|
|
|
|
[tool.ruff.lint]
|
|
|
|
|
|
select = [
|
|
|
|
|
|
"E", # pycodestyle errors
|
|
|
|
|
|
"W", # pycodestyle warnings
|
|
|
|
|
|
"F", # pyflakes
|
|
|
|
|
|
"I", # isort
|
|
|
|
|
|
"B", # flake8-bugbear
|
|
|
|
|
|
"C4", # flake8-comprehensions
|
|
|
|
|
|
"UP", # pyupgrade
|
|
|
|
|
|
]
|
|
|
|
|
|
ignore = [
|
|
|
|
|
|
"E501", # line too long (handled by formatter)
|
|
|
|
|
|
"B008", # do not perform function calls in argument defaults
|
2026-01-07 11:36:44 -08:00
|
|
|
|
"B905", # zip without strict parameter
|
2026-01-06 23:16:58 -08:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
[tool.ruff.lint.isort]
|
|
|
|
|
|
known-first-party = ["headroom"]
|
|
|
|
|
|
|
2026-01-07 11:36:44 -08:00
|
|
|
|
[tool.ruff.format]
|
|
|
|
|
|
quote-style = "double"
|
|
|
|
|
|
indent-style = "space"
|
|
|
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
|
[tool.mypy]
|
|
|
|
|
|
python_version = "3.10"
|
|
|
|
|
|
warn_return_any = true
|
|
|
|
|
|
warn_unused_configs = true
|
|
|
|
|
|
disallow_untyped_defs = true
|
2026-01-07 11:36:44 -08:00
|
|
|
|
ignore_missing_imports = true
|
2026-01-06 23:16:58 -08:00
|
|
|
|
|
2026-01-10 15:53:38 -08:00
|
|
|
|
# Per-module overrides for modules with dynamic typing patterns
|
|
|
|
|
|
[[tool.mypy.overrides]]
|
|
|
|
|
|
module = [
|
|
|
|
|
|
"headroom.proxy.server",
|
2026-04-03 16:17:50 -07:00
|
|
|
|
"headroom.proxy.cost",
|
|
|
|
|
|
"headroom.proxy.prometheus_metrics",
|
|
|
|
|
|
"headroom.proxy.semantic_cache",
|
|
|
|
|
|
"headroom.proxy.rate_limiter",
|
|
|
|
|
|
"headroom.proxy.request_logger",
|
|
|
|
|
|
"headroom.proxy.helpers",
|
2026-01-10 15:53:38 -08:00
|
|
|
|
"headroom.integrations.langchain",
|
|
|
|
|
|
"headroom.integrations.mcp",
|
|
|
|
|
|
"headroom.ccr.mcp_server",
|
|
|
|
|
|
"headroom.relevance.embedding",
|
|
|
|
|
|
"headroom.reporting.generator",
|
|
|
|
|
|
]
|
|
|
|
|
|
disallow_untyped_defs = false
|
|
|
|
|
|
|
|
|
|
|
|
[[tool.mypy.overrides]]
|
|
|
|
|
|
module = [
|
|
|
|
|
|
"headroom.tokenizers.*",
|
|
|
|
|
|
"headroom.providers.litellm",
|
|
|
|
|
|
"headroom.providers.google",
|
|
|
|
|
|
]
|
|
|
|
|
|
disallow_untyped_defs = false
|
|
|
|
|
|
warn_return_any = false
|
|
|
|
|
|
|
2026-04-03 17:08:39 -07:00
|
|
|
|
# Handler mixins use self.* from HeadroomProxy via duck typing — mypy can't resolve these
|
|
|
|
|
|
[[tool.mypy.overrides]]
|
|
|
|
|
|
module = ["headroom.proxy.handlers.*"]
|
|
|
|
|
|
disallow_untyped_defs = false
|
|
|
|
|
|
ignore_errors = true
|
|
|
|
|
|
|
2026-01-10 15:53:38 -08:00
|
|
|
|
# Ignore third-party stubs with syntax errors
|
|
|
|
|
|
[[tool.mypy.overrides]]
|
|
|
|
|
|
module = ["mlx.*"]
|
|
|
|
|
|
ignore_errors = true
|
|
|
|
|
|
|
2026-01-06 23:16:58 -08:00
|
|
|
|
[tool.pytest.ini_options]
|
|
|
|
|
|
testpaths = ["tests"]
|
|
|
|
|
|
python_files = ["test_*.py"]
|
|
|
|
|
|
python_functions = ["test_*"]
|
|
|
|
|
|
addopts = "-v --tb=short"
|
2026-01-07 11:36:44 -08:00
|
|
|
|
asyncio_mode = "auto"
|
2026-06-10 19:30:00 -04:00
|
|
|
|
filterwarnings = [
|
|
|
|
|
|
# pyo3 Unsendable parsers emit an unraisable warning when GC drops them on a
|
|
|
|
|
|
# test-teardown thread; this is a test-harness artifact, not a production issue
|
|
|
|
|
|
# (production threads are long-lived and drop their parsers on themselves).
|
|
|
|
|
|
"ignore::pytest.PytestUnraisableExceptionWarning",
|
|
|
|
|
|
]
|
chore(proxy): add third-party extension point
Adds `headroom/proxy/extensions.py` — a generic entry-point hook under
the `headroom.proxy_extension` group. External packages register an
`install(app, config)` callable that runs once at proxy startup and is
free to add ASGI middleware, routes, mutate config, or raise to
fail-closed (e.g., license check failure aborts startup).
Why: Headroom OSS keeps a deliberately minimal surface, but several
high-value capabilities — PII redaction + tool-call vaulting, data-
residency routing, multi-tenant RBAC, compliance audit, vision/voice
privacy — only make sense as separately-distributed packages. A small,
stable plugin contract lets those live outside this repo without
requiring the OSS to know about them.
Contract:
[project.entry-points."headroom.proxy_extension"]
my_extension = "my_pkg.extension:install"
def install(app: FastAPI, config: ProxyConfig) -> None: ...
An extension that raises from install() is a deliberate fail-closed
signal and aborts startup. Entry-point load failures are logged and
skipped so one broken third-party package cannot take the proxy down.
Changes:
* New: headroom/proxy/extensions.py (~55 LOC)
* headroom/proxy/server.py: invoke install_all(app, config) in
create_app() immediately after CORS middleware registration
* pyproject.toml: register slow and real_llm pytest markers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:01:27 -07:00
|
|
|
|
markers = [
|
|
|
|
|
|
"slow: slow tests (model loads, large fixtures)",
|
|
|
|
|
|
"real_llm: tests that hit real LLM APIs; skipped unless explicitly enabled",
|
fix: Wave 3 — multi-turn live integration tests for A+B realignment
Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests
that validate the load-bearing claims of the Phase A+B megamerge against
real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or
more realignment PRs:
1. test_anthropic_cache_hit_across_two_turns — A2/A6/E
Identical cache_control'd system+messages on two turns must
eventually produce cache_read_input_tokens > 0. Guards the cache
hot zone invariant (I2): proxy must not mutate frozen prefix bytes.
Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's
eventually-consistent prompt-cache write latency without masking
a real "proxy broke cache stability" regression.
2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3
Turn 2 mutates only the LATEST user content (8KB+ JSON tail);
cache_read on turn 2 must still be > 0 AND the proxy must emit
compression headers — proving the live-zone block dispatcher
ran on the new tail without disturbing the cached prefix.
3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4
Wraps proxy._retry_request to snapshot the upstream-bound body
and assert cache_control on system blocks survives verbatim,
and user content is not flattened from list to string form.
4. test_openai_chat_completions_multi_turn_through_proxy — A8/B
Three-turn conversation through /v1/chat/completions; each
turn returns valid content, prior assistant turns survive in
the messages list (proxy doesn't drop them).
5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire)
Streams /v1/chat/completions; asserts each event is
'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n',
reassembled content non-empty, no malformed events.
6. test_gemini_multi_turn_through_proxy — Gemini reach
Two-turn conversation through native
/v1beta/models/{model}:generateContent. Proves Gemini handler
wiring stayed intact through the megamerge.
7. test_ccr_marker_round_trip_live — B7 (CCR)
Pre-populates compression_store with a fixture entry, embeds
a CCR marker on a tool_result, verifies (a) headroom_retrieve
tool is injected into the upstream tools array (PR-B7
always-on), and (b) /v1/retrieve returns the original bytes
by hash with all rows intact. Pre-populating the Python store
(vs. driving SmartCrusher's internal Rust store) matches the
established pattern in tests/test_proxy_ccr.py and exercises
the surface served by /v1/retrieve.
8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2
Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL,
seeds LocalBackend, captures upstream-bound body. Asserts:
(a) system prompt byte-identical to input; (b) memory text
lands on latest user message tail; (c) earlier messages
untouched. Guards the live-zone-only injection contract.
9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5
NOT a live API call. Sends three header shapes through the
proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer
sk-ant-api03-...), captures dispatcher headers via a wrap on
_retry_request, and asserts the canonical auth-mode classifier
maps each correctly. Codifies the Phase F contract.
Conventions:
* file-level pytestmark = pytest.mark.live → excluded by default
via 'pytest -m "not live"'. Adds a 'live' marker registration in
pyproject.toml's [tool.pytest.ini_options].markers.
* each test skipif's on the relevant API key — no silent fallbacks,
no real-API runs against fake keys.
* uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env)
rather than re-implementing env loading.
* model IDs and thresholds live in a top-of-file LIVE_CONFIG dict
(no hardcodes); Anthropic primary/fallback resolves at runtime per
key entitlement.
* assertions are direction-only (cache_read > 0, tokens_after <=
tokens_before) — never tied to upstream pricing/tokenizer drift.
* shared module-scoped TestClient fixture for performance; CCR and
memory tests build dedicated proxies for their config-specific paths.
Verification:
* pytest tests/test_realignment_live_multi_turn.py -v
→ 9 passed, 0 skipped, 0 failed in ~25s (with all keys set)
* pytest -m "not live" --tb=short -q
→ 4694 passed, 265 skipped, 9 deselected — same baseline as today
* make ci-precheck → green (rust + python + commitlint)
Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
|
|
|
|
"live: opt-in multi-turn tests that hit real upstream APIs; require provider keys",
|
chore(proxy): add third-party extension point
Adds `headroom/proxy/extensions.py` — a generic entry-point hook under
the `headroom.proxy_extension` group. External packages register an
`install(app, config)` callable that runs once at proxy startup and is
free to add ASGI middleware, routes, mutate config, or raise to
fail-closed (e.g., license check failure aborts startup).
Why: Headroom OSS keeps a deliberately minimal surface, but several
high-value capabilities — PII redaction + tool-call vaulting, data-
residency routing, multi-tenant RBAC, compliance audit, vision/voice
privacy — only make sense as separately-distributed packages. A small,
stable plugin contract lets those live outside this repo without
requiring the OSS to know about them.
Contract:
[project.entry-points."headroom.proxy_extension"]
my_extension = "my_pkg.extension:install"
def install(app: FastAPI, config: ProxyConfig) -> None: ...
An extension that raises from install() is a deliberate fail-closed
signal and aborts startup. Entry-point load failures are logged and
skipped so one broken third-party package cannot take the proxy down.
Changes:
* New: headroom/proxy/extensions.py (~55 LOC)
* headroom/proxy/server.py: invoke install_all(app, config) in
create_app() immediately after CORS middleware registration
* pyproject.toml: register slow and real_llm pytest markers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:01:27 -07:00
|
|
|
|
]
|
2026-01-07 11:36:44 -08:00
|
|
|
|
|
|
|
|
|
|
[tool.coverage.run]
|
|
|
|
|
|
source = ["headroom"]
|
|
|
|
|
|
branch = true
|
|
|
|
|
|
omit = [
|
|
|
|
|
|
"headroom/cli.py",
|
|
|
|
|
|
"*/tests/*",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
[tool.coverage.report]
|
|
|
|
|
|
exclude_lines = [
|
|
|
|
|
|
"pragma: no cover",
|
|
|
|
|
|
"def __repr__",
|
|
|
|
|
|
"raise NotImplementedError",
|
|
|
|
|
|
"if TYPE_CHECKING:",
|
|
|
|
|
|
"if __name__ == .__main__.:",
|
|
|
|
|
|
]
|