diff --git a/headroom/cli/mcp.py b/headroom/cli/mcp.py index 44d466e17..260ea82d9 100644 --- a/headroom/cli/mcp.py +++ b/headroom/cli/mcp.py @@ -76,6 +76,15 @@ def mcp() -> None: 3. Claude sees compressed summaries with hash markers 4. When Claude needs full details, it calls headroom_retrieve 5. The MCP server fetches original content from the proxy + + \b + Note on tool naming: MCP clients display tools as + `mcp____`. Our server is named "headroom" and our + tools are named headroom_retrieve / headroom_compress / etc., so + Claude Code shows them as `mcp__headroom__headroom_retrieve`. The + "headroom" doubling is normal MCP namespacing — not a bug. The + proxy's compression markers (and any docs/prompts) reference the + bare tool name `headroom_retrieve`. """ pass diff --git a/headroom/cli/proxy.py b/headroom/cli/proxy.py index 47333ec35..93ee7d1e8 100644 --- a/headroom/cli/proxy.py +++ b/headroom/cli/proxy.py @@ -62,7 +62,11 @@ from .main import main @click.option( "--mode", default=None, + metavar="[token|cache]", type=click.Choice( + # Canonical modes first; legacy aliases follow for backward compatibility. + # `metavar` above hides the alias clutter from --help; users see "[token|cache]" + # while internal callers passing "token_mode"/"cost_savings"/etc. still validate. [ "token", "cache", @@ -71,12 +75,15 @@ from .main import main "token_savings", "cost_savings", "token_headroom", - ] + ], + case_sensitive=False, ), help=( - "Optimization mode: token (prioritize compression) or cache " - "(freeze prior turns for prefix-cache stability). " - "Legacy aliases are accepted. Default: token. Env: HEADROOM_MODE" + "Optimization mode (default: token).\n" + " token — prioritize compression; prior turns may be rewritten for max savings.\n" + " cache — freeze prior turns to maximise provider prefix-cache hit rate.\n" + "Legacy aliases (token_mode, token_savings, token_headroom, cache_mode, " + "cost_savings) are still accepted. Env: HEADROOM_MODE." ), ) @click.option( @@ -184,11 +191,32 @@ from .main import main envvar="HEADROOM_BUDGET", help="Daily budget limit in USD (env: HEADROOM_BUDGET)", ) -# Code graph: indexes project + watches files for live reindex via codebase-memory-mcp +# Code-aware compression (AST-based, requires `pip install headroom-ai[code]`). +# Pair of flags so users can override the env-var default in either direction. +# We resolve HEADROOM_CODE_AWARE_ENABLED in the body (not via Click's envvar=), +# because Click's envvar handling for paired bool flags is brittle in older +# Click versions. +@click.option( + "--code-aware/--no-code-aware", + "code_aware_flag", + default=None, + help=( + "Enable/disable AST-based code compression. Requires the optional " + "tree-sitter dependency: pip install headroom-ai[code]. " + "Default: disabled. Env: HEADROOM_CODE_AWARE_ENABLED=1 to enable." + ), +) +# Code graph: indexes project + watches files for live reindex via codebase-memory-mcp. +# Only useful when the proxy is launched from a project root — it indexes the +# current working directory. @click.option( "--code-graph", is_flag=True, - help="Enable code graph intelligence (indexes project, watches files for live reindex via codebase-memory-mcp)", + help=( + "Enable code graph intelligence: indexes the current working directory " + "and watches files for live reindex via codebase-memory-mcp. Only useful " + "when the proxy is launched from a project root." + ), ) # Read lifecycle (ON by default: compresses stale/superseded Read outputs) @click.option( @@ -359,6 +387,7 @@ def proxy( log_file: str | None, log_messages: bool, budget: float | None, + code_aware_flag: bool | None, code_graph: bool, no_read_lifecycle: bool, memory: bool, @@ -514,6 +543,17 @@ def proxy( log_full_messages=log_messages or os.environ.get("HEADROOM_LOG_MESSAGES", "").lower() in ("true", "1", "yes", "on"), budget_limit_usd=budget, + # Code-aware compression resolution: + # 1. Explicit --code-aware / --no-code-aware always wins. + # 2. Otherwise read HEADROOM_CODE_AWARE_ENABLED (truthy = on). + # 3. Otherwise default off — matches the prior cli/proxy.py behavior so + # existing users see no change unless they opt in. + code_aware_enabled=( + bool(code_aware_flag) + if code_aware_flag is not None + else os.environ.get("HEADROOM_CODE_AWARE_ENABLED", "").strip().lower() + in ("true", "1", "yes", "on") + ), # Code graph: live file watcher for incremental reindexing code_graph_watcher=code_graph, # Read lifecycle: ON by default (use --no-read-lifecycle to disable) @@ -660,6 +700,13 @@ Memory (Multi-Provider): f"(available: {','.join(_ext_available)})" ) + # Code-aware status line — same logic the inner banner uses, surfaced here + # so the click-CLI banner is a complete picture (avoids the dual-banner + # confusion this branch retired). + from headroom.proxy.server import _get_code_aware_banner_status + + code_aware_line = f" Code-Aware: {_get_code_aware_banner_status(config)}" + click.echo(f""" ╔═══════════════════════════════════════════════════════════════════════╗ ║ HEADROOM PROXY ║ @@ -675,6 +722,7 @@ Starting proxy server... Rate Limit: {"ENABLED" if config.rate_limit_enabled else "DISABLED"} Memory: {memory_status} License: {license_status} +{code_aware_line} {extensions_line} {stateless_line}{telemetry_line} {backend_section} @@ -700,11 +748,15 @@ Press Ctrl+C to stop. """) try: - run_kwargs: dict[str, int] = {} + run_kwargs: dict[str, Any] = {} if workers != 1: run_kwargs["workers"] = workers if limit_concurrency != 1000: run_kwargs["limit_concurrency"] = limit_concurrency + # Suppress run_server's legacy banner — the click CLI already printed + # a richer one above. Direct `python -m headroom.proxy.server` keeps + # the legacy banner via run_server's default. + run_kwargs["print_banner"] = False run_server(config, **run_kwargs) except KeyboardInterrupt: click.echo("\nShutting down...") diff --git a/headroom/cli/wrap.py b/headroom/cli/wrap.py index af0112c74..4d22eeda6 100644 --- a/headroom/cli/wrap.py +++ b/headroom/cli/wrap.py @@ -1298,13 +1298,28 @@ def wrap() -> None: the target tool so all API calls route through Headroom automatically. \b - Supported tools: + Supported tools (one Click subcommand per tool): headroom wrap claude # Claude Code (Anthropic) - headroom wrap copilot -- --model claude-sonnet-4-20250514 headroom wrap codex # OpenAI Codex CLI + headroom wrap copilot -- --model claude-sonnet-4-20250514 headroom wrap aider # Aider headroom wrap cursor # Cursor (prints config instructions) headroom wrap openclaw # OpenClaw plugin bootstrap + + \b + `wrap` vs `proxy`: + - `headroom wrap ` — convenience: starts the proxy for you, + sets the right env vars, and launches the wrapped CLI. + - `headroom proxy` — just the proxy. Use this with any + OpenAI/Anthropic-compatible client by setting + ANTHROPIC_BASE_URL / OPENAI_BASE_URL yourself. Required for + tools without a dedicated `wrap` subcommand + (e.g. opencode, Cline, Continue). + + \b + Note: `headroom wrap opencode` does NOT exist. For opencode, run + `headroom proxy` and point opencode at it via OPENAI_BASE_URL. + `openclaw` is a separate tool — different from opencode. """ diff --git a/headroom/perf/analyzer.py b/headroom/perf/analyzer.py index e89851c80..b52e76ddf 100644 --- a/headroom/perf/analyzer.py +++ b/headroom/perf/analyzer.py @@ -13,6 +13,7 @@ from __future__ import annotations import logging import re from dataclasses import dataclass, field +from datetime import datetime, timedelta from headroom import paths as _paths @@ -213,6 +214,31 @@ class PerfReport: toin_records: list[ToinRecord] = field(default_factory=list) log_files_read: int = 0 total_lines_parsed: int = 0 + # Window covered by the report. `requested_hours` is what the caller + # asked for; `oldest_kept_ts` / `newest_kept_ts` are the actual + # timestamps of the oldest and newest records that survived the + # filter (may be narrower if the log doesn't go back that far). + # All optional so existing callers keep working. + requested_hours: float | None = None + oldest_kept_ts: str | None = None + newest_kept_ts: str | None = None + records_filtered_out: int = 0 + + +# Log timestamps are emitted by Python's `logging` formatter as +# `YYYY-MM-DD HH:MM:SS,fff`. We keep the parser permissive so the perf +# CLI never throws on a stray malformed line — unparsable records are +# just kept (better to over-report than to silently drop data). +_LOG_TS_FMT = "%Y-%m-%d %H:%M:%S,%f" + + +def _parse_log_ts(ts: str | None) -> datetime | None: + if not ts: + return None + try: + return datetime.strptime(ts, _LOG_TS_FMT) + except ValueError: + return None def parse_log_files(last_n_hours: float = 168.0) -> PerfReport: @@ -220,15 +246,40 @@ def parse_log_files(last_n_hours: float = 168.0) -> PerfReport: Args: last_n_hours: Only include records from the last N hours (default 7 days). + Records with un-parseable timestamps are kept (fail-open) — the + window in the report header reflects the actual timestamps that + survived the filter, so the user can see whether the log went + back far enough. Returns: PerfReport with all parsed records. """ report = PerfReport() + report.requested_hours = last_n_hours if not LOG_DIR.exists(): return report + cutoff = datetime.now() - timedelta(hours=last_n_hours) if last_n_hours > 0 else None + + def _within_window(ts_str: str | None) -> bool: + # Fail-open: records without a parseable timestamp are kept. The + # alternative (silent drop) makes `headroom perf` lie about coverage. + if cutoff is None: + return True + ts = _parse_log_ts(ts_str) + if ts is None: + return True + return ts >= cutoff + + def _track_window(ts_str: str | None) -> None: + if not ts_str: + return + if report.oldest_kept_ts is None or ts_str < report.oldest_kept_ts: + report.oldest_kept_ts = ts_str + if report.newest_kept_ts is None or ts_str > report.newest_kept_ts: + report.newest_kept_ts = ts_str + # Collect log files: proxy.log, proxy.log.1, proxy.log.2, ... log_files = sorted(LOG_DIR.glob("proxy.log*"), key=lambda p: p.stat().st_mtime) @@ -260,9 +311,14 @@ def parse_log_files(last_n_hours: float = 168.0) -> PerfReport: else: # Old comma-separated format transforms = transforms_str.split(",") + ts = m.group("ts") + if not _within_window(ts): + report.records_filtered_out += 1 + continue + _track_window(ts) report.perf_records.append( PerfRecord( - timestamp=m.group("ts"), + timestamp=ts, request_id=m.group("rid"), model=kv.get("model", ""), num_messages=int(kv.get("msgs", 0)), @@ -283,6 +339,10 @@ def parse_log_files(last_n_hours: float = 168.0) -> PerfReport: m2 = _ROUTER_RE.search(line) if m2: ts = line[:23] + if not _within_window(ts): + report.records_filtered_out += 1 + continue + _track_window(ts) detail = m2.group("detail") rec = RouterRecord( timestamp=ts, @@ -312,6 +372,10 @@ def parse_log_files(last_n_hours: float = 168.0) -> PerfReport: m3 = _TRANSFORM_RE.search(line) if m3: ts = line[:23] + if not _within_window(ts): + report.records_filtered_out += 1 + continue + _track_window(ts) report.transform_records.append( TransformRecord( timestamp=ts, @@ -327,6 +391,10 @@ def parse_log_files(last_n_hours: float = 168.0) -> PerfReport: m4 = _TOIN_RE.search(line) if m4: ts = line[:23] + if not _within_window(ts): + report.records_filtered_out += 1 + continue + _track_window(ts) report.toin_records.append( ToinRecord( timestamp=ts, @@ -357,6 +425,21 @@ def format_report(report: PerfReport) -> str: # Header lines.append("Headroom Performance Report") lines.append("=" * 60) + if report.requested_hours is not None: + if report.oldest_kept_ts and report.newest_kept_ts: + window_str = ( + f"Window: last {report.requested_hours:g}h " + f"(actual data: {report.oldest_kept_ts[:19]} → " + f"{report.newest_kept_ts[:19]})" + ) + else: + window_str = f"Window: last {report.requested_hours:g}h (no records found in window)" + lines.append(window_str) + if report.records_filtered_out > 0: + lines.append( + f"Records outside window: {report.records_filtered_out:,} " + "(filtered out — increase --hours to include them)" + ) lines.append("") records = report.perf_records @@ -501,7 +584,7 @@ def format_report(report: PerfReport) -> str: lines.append(" ! Excluded tools dominate — consider compressing stale Read outputs") lines.append("") - # TOIN status + # TOIN status — log-derived counters first, then live-store highlights. if report.toin_records: latest = report.toin_records[-1] lines.append("TOIN Learning") @@ -513,6 +596,17 @@ def format_report(report: PerfReport) -> str: lines.append(" ! 0% retrieval rate — TOIN learning but never used") lines.append("") + # TOIN highlights — read the live on-disk pattern store and surface + # strategy distribution + top high-impact patterns in human-readable + # form. Pattern keys are opaque hashes, so the actionable signal is + # *which strategies are winning* and *how many patterns have crossed + # the recommendation threshold*. Best-effort: if TOIN isn't installed + # or the store is empty, we skip the section silently. + toin_lines = _format_toin_highlights() + if toin_lines: + lines.extend(toin_lines) + lines.append("") + # Recommendations recommendations = _generate_recommendations(report) if recommendations: @@ -531,6 +625,88 @@ def format_report(report: PerfReport) -> str: return "\n".join(lines) +def _format_toin_highlights() -> list[str]: + """Render a human-readable TOIN highlights block from the live store. + + Returns an empty list when TOIN is unavailable or has no patterns. + Pattern keys (auth_mode, model_family, structure_hash) are opaque + hashes so we don't print them as rows — instead we group by the + learned ``optimal_strategy`` (a human-readable string like + ``"lossless:table(240->len=7026)"``) and surface the highest-impact + slices via ``avg_token_reduction``. + """ + try: + from headroom.telemetry.toin import get_toin + except ImportError: + return [] + + try: + pairs = get_toin().iter_patterns() + except Exception: # noqa: BLE001 — perf must never fail on TOIN errors + return [] + + if not pairs: + return [] + + # Strategy distribution: how many patterns settled on each strategy. + strategy_counts: dict[str, int] = {} + for _key, pattern in pairs: + strategy = pattern.optimal_strategy or "default" + strategy_counts[strategy] = strategy_counts.get(strategy, 0) + 1 + + # Top patterns by avg token reduction (the high-impact learnings). + by_impact = sorted( + pairs, + key=lambda kp: kp[1].avg_token_reduction, + reverse=True, + )[:5] + + # How many patterns have enough samples to drive a recommendation. + # Falls back to 0 if the threshold attr isn't reachable. + try: + from headroom.telemetry.toin import get_toin as _get + + threshold = _get()._config.min_samples_for_recommendation + except Exception: # noqa: BLE001 + threshold = 1 + qualified = sum(1 for _k, p in pairs if p.sample_size >= threshold) + + lines: list[str] = [] + lines.append("TOIN Highlights (live store)") + lines.append("-" * 40) + lines.append( + f" {qualified}/{len(pairs)} patterns have ≥{threshold} samples " + f"(eligible for `python -m headroom.cli.toin_publish`)" + ) + lines.append("") + lines.append(" Strategy distribution:") + for strategy, count in sorted(strategy_counts.items(), key=lambda kv: kv[1], reverse=True)[:8]: + lines.append(f" {count:>4} pattern(s) {strategy}") + + # Only surface patterns with non-trivial impact AND a non-default + # strategy — single-digit-token "wins" against the default strategy + # are noise, not insight. + impact_rows = [ + (kp[1].avg_token_reduction, kp[1].total_compressions, kp[1].optimal_strategy or "default") + for kp in by_impact + if kp[1].avg_token_reduction >= 50 and (kp[1].optimal_strategy or "default") != "default" + ] + if impact_rows: + lines.append("") + lines.append(" Top patterns by avg token reduction:") + for avg_red, n, strategy in impact_rows: + lines.append(f" {avg_red:>7.0f} tok avg ({n:>3} compression(s)) {strategy}") + + if qualified == 0 and len(pairs) > 0: + lines.append("") + lines.append( + f" ! No pattern has reached {threshold} samples — TOIN is still warming up. " + "Recommendations TOML will be empty until traffic grows." + ) + + return lines + + def _generate_recommendations(report: PerfReport) -> list[str]: """Generate actionable recommendations from the report data.""" recs: list[str] = [] diff --git a/headroom/providers/openclaw/wrap.py b/headroom/providers/openclaw/wrap.py index 2ccbeadf0..664128ea9 100644 --- a/headroom/providers/openclaw/wrap.py +++ b/headroom/providers/openclaw/wrap.py @@ -35,6 +35,15 @@ def decode_entry_json(raw_value: str | None) -> Any | None: return raw_value +# Keys we know newer openclaw plugin schemas reject when echoed back. +# We strip them defensively from `existing_entry` so a stale entry left +# over from an older Headroom or older OpenClaw install doesn't cause +# `openclaw config set` to fail with "Unrecognized key". The list is +# narrow on purpose — anything else is assumed user-managed and +# preserved verbatim. +_LEGACY_REJECTED_TOP_LEVEL_KEYS: frozenset[str] = frozenset({"mcpServers"}) + + def build_plugin_entry( *, existing_entry: Any, @@ -46,7 +55,8 @@ def build_plugin_entry( enabled: bool, ) -> dict[str, object]: """Merge managed Headroom plugin settings with any existing entry payload.""" - base_entry = existing_entry if isinstance(existing_entry, dict) else {} + raw_base = existing_entry if isinstance(existing_entry, dict) else {} + base_entry = {k: v for k, v in raw_base.items() if k not in _LEGACY_REJECTED_TOP_LEVEL_KEYS} existing_config = base_entry.get("config") next_config = dict(existing_config) if isinstance(existing_config, dict) else {} diff --git a/headroom/proxy/server.py b/headroom/proxy/server.py index 8e285077b..b7ffcfc0f 100644 --- a/headroom/proxy/server.py +++ b/headroom/proxy/server.py @@ -2598,14 +2598,15 @@ def _get_code_aware_banner_status(config: ProxyConfig) -> str: return "NOT INSTALLED (pip install headroom-ai[code])" else: if is_tree_sitter_available(): - return "DISABLED (remove --no-code-aware to enable)" - return "DISABLED" + return "DISABLED (--code-aware or HEADROOM_CODE_AWARE_ENABLED=1 to enable)" + return "DISABLED (install headroom-ai[code] to enable)" def run_server( config: ProxyConfig | None = None, workers: int = 1, limit_concurrency: int = 1000, + print_banner: bool = True, ): """Run the proxy server. @@ -2613,6 +2614,11 @@ def run_server( config: Proxy configuration workers: Number of worker processes (use N for multi-core scaling) limit_concurrency: Max concurrent connections before 503 response + print_banner: When False, skip the legacy ASCII banner. The + Click CLI (`headroom proxy`) prints its own startup banner + before calling this — printing a second banner here is the + "dual banner" UX issue. Direct `python -m headroom.proxy.server` + still gets the banner since it has no other startup output. """ if not FASTAPI_AVAILABLE: print("ERROR: FastAPI required. Install: pip install fastapi uvicorn httpx") @@ -2631,7 +2637,8 @@ def run_server( bedrock_region=config.bedrock_region, ) - print(f""" + if print_banner: + print(f""" ╔══════════════════════════════════════════════════════════════════════╗ ║ HEADROOM PROXY SERVER ║ ╠══════════════════════════════════════════════════════════════════════╣ diff --git a/pyproject.toml b/pyproject.toml index 90828428a..64af1b81b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,17 +75,13 @@ proxy = [ code = [ "tree-sitter-language-pack>=0.10.0", ] -# ML-based compression with Kompress (ModernBERT) +# 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.) ml = [ "torch>=2.0.0", "transformers>=4.30.0", ] -# Legacy ML compression (LLMLingua-2 — use [ml] instead for Kompress) -llmlingua = [ - "llmlingua>=0.2.0", - "torch>=2.0.0", - "transformers>=4.30.0", -] # Memory system (hierarchical memory with vector search) memory = [ "hnswlib>=0.8.0", diff --git a/tests/test_anthropic_pre_upstream_backpressure.py b/tests/test_anthropic_pre_upstream_backpressure.py index 237536e77..97aea531c 100644 --- a/tests/test_anthropic_pre_upstream_backpressure.py +++ b/tests/test_anthropic_pre_upstream_backpressure.py @@ -721,7 +721,7 @@ def _run_cli_capture(args: list[str], env: dict | None = None) -> ProxyConfig: captured: dict[str, ProxyConfig] = {} orig_run = server_mod.run_server - def _fake_run(config: ProxyConfig): # noqa: D401 - stub + def _fake_run(config: ProxyConfig, **_kwargs): # noqa: D401 - stub captured["config"] = config return 0 diff --git a/tests/test_cli_proxy_env.py b/tests/test_cli_proxy_env.py index c8c910493..b240c9c9e 100644 --- a/tests/test_cli_proxy_env.py +++ b/tests/test_cli_proxy_env.py @@ -30,7 +30,7 @@ class TestCLIProxyEnvVars: """HEADROOM_HOST env var should be passed to ProxyConfig.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -48,7 +48,7 @@ class TestCLIProxyEnvVars: """HEADROOM_PORT env var should be passed to ProxyConfig.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -66,7 +66,7 @@ class TestCLIProxyEnvVars: """HEADROOM_BUDGET env var should be passed to ProxyConfig.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -84,7 +84,7 @@ class TestCLIProxyEnvVars: """OPENAI_TARGET_API_URL env var should be passed to ProxyConfig.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -102,7 +102,7 @@ class TestCLIProxyEnvVars: """GEMINI_TARGET_API_URL env var should be passed to ProxyConfig.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -120,7 +120,7 @@ class TestCLIProxyEnvVars: """--openai-api-url CLI flag should take precedence.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -137,7 +137,7 @@ class TestCLIProxyEnvVars: """CLI flag should take precedence over env var.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -155,7 +155,7 @@ class TestCLIProxyEnvVars: """Without env var or flag, openai_api_url should be None.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config # Ensure the env var is not set @@ -178,7 +178,7 @@ class TestCLIProxyEnvVars: """Both OPENAI and GEMINI target URLs can be set via env.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -200,7 +200,7 @@ class TestCLIProxyEnvVars: """Fast-fail CLI flags should map into ProxyConfig.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -243,7 +243,12 @@ class TestCLIProxyEnvVars: assert result.exit_code == 0, result.output assert captured["config"].max_connections == 200 assert captured["config"].max_keepalive_connections == 50 - assert captured["kwargs"] == {"workers": 4, "limit_concurrency": 250} + # Click CLI also passes `print_banner=False` to suppress the legacy + # run_server banner (cli/proxy.py prints its own). Assert the + # production-scaling keys we care about, not the full kwargs dict. + assert captured["kwargs"]["workers"] == 4 + assert captured["kwargs"]["limit_concurrency"] == 250 + assert captured["kwargs"].get("print_banner") is False def test_production_scaling_cli_flags_override_env_vars(self, runner): captured = {} @@ -278,7 +283,11 @@ class TestCLIProxyEnvVars: assert result.exit_code == 0, result.output assert captured["config"].max_connections == 150 assert captured["config"].max_keepalive_connections == 25 - assert captured["kwargs"] == {"workers": 3, "limit_concurrency": 125} + # Click CLI also passes `print_banner=False`. Assert production + # scaling keys explicitly rather than the full kwargs dict. + assert captured["kwargs"]["workers"] == 3 + assert captured["kwargs"]["limit_concurrency"] == 125 + assert captured["kwargs"].get("print_banner") is False class TestCLIProxyBackend: @@ -288,7 +297,7 @@ class TestCLIProxyBackend: """--backend litellm-hosted_vllm should be accepted (not rejected).""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -305,7 +314,7 @@ class TestCLIProxyBackend: """--backend litellm-vertex should be accepted.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -322,7 +331,7 @@ class TestCLIProxyBackend: """Full vLLM setup: litellm backend + OPENAI_TARGET_API_URL.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -350,7 +359,7 @@ class TestCLIAnyllmProviderEnv: """HEADROOM_ANYLLM_PROVIDER env var should override the default.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): @@ -368,7 +377,7 @@ class TestCLIAnyllmProviderEnv: """--anyllm-provider flag should still work.""" captured_config = {} - def mock_run_server(config): + def mock_run_server(config, **kwargs): captured_config["config"] = config with patch("headroom.proxy.server.run_server", mock_run_server): diff --git a/tests/test_provider_openclaw_wrap.py b/tests/test_provider_openclaw_wrap.py index 5025aa6e0..be24c82ca 100644 --- a/tests/test_provider_openclaw_wrap.py +++ b/tests/test_provider_openclaw_wrap.py @@ -117,3 +117,34 @@ def test_build_unwrap_entry_disables_plugin_and_removes_managed_keys_only() -> N def test_build_unwrap_entry_handles_non_mapping_input() -> None: # Arrange / Act / Assert assert build_unwrap_entry("not-a-dict") == {"enabled": False, "config": {}} + + +def test_build_plugin_entry_strips_mcpServers_from_existing_entry() -> None: + """Newer OpenClaw schemas reject `mcpServers` at the plugin-entry root. + + `headroom init -g` was failing with `Config invalid: Unrecognized + key: "mcpServers"` because the prior plugin entry in the user's + config still had that legacy field, and we were spreading it back in + via `**existing_entry`. Pin the strip so we don't regress. + """ + existing_entry = { + "enabled": True, + "name": "headroom", + "mcpServers": {"some": "stale-block"}, # legacy, must be removed + "config": {"keep": "value"}, + } + + entry = build_plugin_entry( + existing_entry=existing_entry, + proxy_port=8787, + startup_timeout_ms=1500, + python_path=None, + no_auto_start=False, + gateway_provider_ids=None, + enabled=True, + ) + + assert "mcpServers" not in entry + assert entry["enabled"] is True + assert entry["name"] == "headroom" + assert entry["config"]["keep"] == "value" diff --git a/wiki/ARCHITECTURE.md b/wiki/ARCHITECTURE.md index 93d815aba..497e6b63f 100644 --- a/wiki/ARCHITECTURE.md +++ b/wiki/ARCHITECTURE.md @@ -239,32 +239,18 @@ analysis = { --- -#### Transform 4: LLMLingua Compressor (Optional) +#### Transform 4: ML Compressor (Optional, Kompress) **When to use:** Maximum compression needed and latency is acceptable. -```python -# Opt-in ML-based compression using Microsoft's LLMLingua-2 -# BERT-based token classifier trained via GPT-4 distillation +The proxy ships an opt-in ML compression path backed by **Kompress** +(ModernBERT-based token classifier). Install with `pip install +'headroom-ai[ml]'`; see `wiki/transforms.md` for current configuration. -# Before: Long tool output text -"The function processUserData takes a user object and validates all fields..." - -# After: Compressed while preserving semantic meaning -"function processUserData validates user fields..." -``` - -**Key characteristics:** -- Uses `microsoft/llmlingua-2-xlm-roberta-large-meetingbank` model -- Auto-detects content type (code, JSON, text) for optimal compression rates -- Stores original in CCR for retrieval if needed -- Adds 50-200ms latency per request -- Requires ~1GB RAM when loaded - -**Proxy integration (opt-in):** -```bash -headroom proxy --llmlingua --llmlingua-device cuda -``` +**Note:** The earlier LLMLingua-2 integration (`--llmlingua` flag, the +`headroom-ai[llmlingua]` extra, and the `LLMLinguaCompressor` class) was +retired and replaced by Kompress. `pip install 'headroom-ai[llmlingua]'` +no longer resolves; use `[ml]` instead. --- @@ -418,8 +404,8 @@ def apply(self, messages, ...): # - Compresses to 17 points (preserving spike) # - Factors out constant "host" field - # Transform 3: LLMLingua (if enabled via --llmlingua) - # - ML-based compression on remaining long text + # Transform 3: Kompress ML compressor (opt-in via headroom-ai[ml]) + # - ModernBERT-based compression on remaining long text # - Auto-detects content type for optimal rate # - Stores original in CCR for retrieval @@ -1082,7 +1068,7 @@ headroom/ │ ├── rolling_window.py # Token limit enforcement (position-based) │ ├── intelligent_context.py # Semantic context management (score-based) │ ├── scoring.py # Message importance scoring -│ └── llmlingua_compressor.py # ML-based compression (opt-in) +│ └── (legacy llmlingua_compressor.py removed — see [ml] extra for Kompress) │ ├── cache/ # CCR Architecture - Caching & Storage │ ├── compression_store.py # Phase 1: Store original content diff --git a/wiki/LIMITATIONS.md b/wiki/LIMITATIONS.md index 4dfe1f683..0323d16f3 100644 --- a/wiki/LIMITATIONS.md +++ b/wiki/LIMITATIONS.md @@ -73,24 +73,23 @@ SmartCrusher doesn't use fixed K values. It uses information-theoretic sizing: These are kept even if they exceed the K budget. -## Text Compression (LLMLingua) +## ML Text Compression (Kompress, opt-in) -- **Requires**: `headroom-ai[llmlingua]` — downloads ~2GB model, needs ~1GB RAM -- **First call**: 10-30s model load latency (cached globally after) -- **Sequence length**: Content chunked at 512 tokens (model limit) -- **Content < 100 tokens**: Skipped -- **Latency**: Adds overhead that doesn't break even on fast models (GPT-4o Mini, Sonnet). Use for **cost savings**, not speed +- **Requires**: `headroom-ai[ml]` — downloads model weights and needs GPU/CPU RAM for inference +- **First call**: model-load latency (cached globally after) +- **Latency**: Adds overhead that doesn't break even on fast models. Use for **cost savings**, not speed - **Thread safety**: Single global model instance with lock — sequential access under concurrency +> The earlier LLMLingua-2 integration (`headroom-ai[llmlingua]`) was retired and is no longer installable. + ## Error Handling All compressors follow the same principle: **fail gracefully, return original content unchanged**. - Invalid JSON → passthrough (no error raised) -- AST parse failure in CodeCompressor → falls back to original or LLMLingua +- AST parse failure in CodeCompressor → falls back to original - Compression makes output larger → original returned -- Missing optional dependencies (tree-sitter, LLMLingua) → passthrough with warning log -- **One exception**: LLMLingua out-of-memory during model loading raises `RuntimeError` +- Missing optional dependencies (tree-sitter, ML stack) → passthrough with warning log Errors are logged at WARNING level and never propagated to callers. diff --git a/wiki/compression.md b/wiki/compression.md index 61ad7bb38..a76a173c3 100644 --- a/wiki/compression.md +++ b/wiki/compression.md @@ -8,7 +8,7 @@ Universal Compression combines several techniques: 1. **ML-based Detection** - Automatically detects content type (JSON, code, logs, text) using Magika 2. **Structure Preservation** - Keeps keys, signatures, and templates intact via structure masks -3. **Intelligent Compression** - Compresses content while preserving meaning with LLMLingua +3. **Intelligent Compression** - Compresses content while preserving meaning with the optional ML compressor (Kompress) 4. **Reversible via CCR** - Stores originals for retrieval when LLM needs full context ## Quick Start @@ -51,9 +51,9 @@ result = compressor.compress(content) │ │ │ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ - │ Magika │ │ Handler │ │ LLMLingua │ - │ (ML) │ │ (JSON, │ │ (optional) │ - │ │ │ Code...) │ │ │ + │ Magika │ │ Handler │ │ Kompress │ + │ (ML) │ │ (JSON, │ │ (ML, opt- │ + │ │ │ Code...) │ │ in [ml]) │ └─────────────┘ └─────────────┘ └─────────────┘ ``` @@ -82,7 +82,9 @@ config = UniversalCompressorConfig( use_magika=True, # Use ML-based detection (requires magika) # Compression - use_llmlingua=True, # Use LLMLingua for compression + # (Note: the legacy `use_llmlingua` flag was retired with the + # LLMLingua-2 integration. The optional ML compressor is now Kompress, + # installed via `headroom-ai[ml]` and configured separately.) compression_ratio_target=0.3, # Keep 30% of content (70% reduction) min_content_length=100, # Skip content shorter than this diff --git a/wiki/configuration.md b/wiki/configuration.md index 5e0787383..d5808f9d3 100644 --- a/wiki/configuration.md +++ b/wiki/configuration.md @@ -59,9 +59,9 @@ headroom proxy --no-ccr-responses # Disable proactive expansion headroom proxy --no-ccr-expansion -# Enable LLMLingua ML compression -headroom proxy --llmlingua -headroom proxy --llmlingua --llmlingua-device cuda --llmlingua-rate 0.4 +# (The earlier --llmlingua flag was retired in 0.9.x and replaced by +# Kompress (ModernBERT). See `wiki/transforms.md` for the current +# opt-in path via the `[ml]` extra.) ``` ### All Options diff --git a/wiki/llmlingua.md b/wiki/llmlingua.md deleted file mode 100644 index be9cee960..000000000 --- a/wiki/llmlingua.md +++ /dev/null @@ -1,188 +0,0 @@ -# LLMLingua-2 Integration - -For maximum compression, Headroom integrates with **LLMLingua-2**, Microsoft's BERT-based token classifier trained via GPT-4 distillation. It achieves **up to 20x compression** while preserving semantic meaning. - -## When to Use LLMLingua-2 - -| Approach | Best For | Compression | Speed | -|----------|----------|-------------|-------| -| **SmartCrusher** | JSON tool outputs | 70-90% | ~1ms | -| **Text Utilities** | Search/logs | 50-90% | ~1ms | -| **LLMLingua-2** | Any text, max compression | 80-95% | ~50-200ms | - -LLMLingua-2 is ideal when you need maximum compression and can tolerate slightly higher latency (e.g., compressing large tool outputs before storage, offline processing). - -## Installation - -```bash -# Adds ~2GB of model weights -pip install "headroom-ai[llmlingua]" -``` - -## Basic Usage - -```python -from headroom.transforms import LLMLinguaCompressor - -# Create compressor (model loaded lazily on first use) -compressor = LLMLinguaCompressor() - -# Compress any text -long_output = "The function processUserData takes a user object and validates..." -result = compressor.compress(long_output) - -print(f"Before: {result.original_tokens} tokens") -print(f"After: {result.compressed_tokens} tokens") -print(f"Saved: {result.savings_percentage:.1f}%") -print(result.compressed) -``` - -## Content-Aware Compression - -LLMLingua-2 automatically adjusts compression based on content type: - -```python -from headroom.transforms import LLMLinguaCompressor, LLMLinguaConfig - -# Conservative for code (keep 40% of tokens) -config = LLMLinguaConfig( - code_compression_rate=0.4, # More conservative - json_compression_rate=0.35, # Moderate - text_compression_rate=0.25, # Aggressive -) - -compressor = LLMLinguaCompressor(config) - -# Auto-detects content type -code_result = compressor.compress("def calculate(x): return x * 2") -text_result = compressor.compress("This is a verbose explanation...") -``` - -## Memory Management - -The model uses ~1GB RAM. Unload it when done: - -```python -from headroom.transforms import ( - LLMLinguaCompressor, - unload_llmlingua_model, - is_llmlingua_model_loaded, -) - -compressor = LLMLinguaCompressor() -result = compressor.compress(content) # Model loaded here - -# Check if loaded -print(is_llmlingua_model_loaded()) # True - -# Free memory when done -unload_llmlingua_model() # Frees ~1GB -print(is_llmlingua_model_loaded()) # False - -# Next compression will reload automatically -``` - -## Device Configuration - -```python -from headroom.transforms import LLMLinguaConfig, LLMLinguaCompressor - -# Force CPU (slower but works everywhere) -config = LLMLinguaConfig(device="cpu") - -# Force GPU (faster but needs CUDA) -config = LLMLinguaConfig(device="cuda") - -# Auto-detect (default): uses CUDA > MPS > CPU -config = LLMLinguaConfig(device="auto") - -compressor = LLMLinguaCompressor(config) -``` - -## Use in Pipeline - -```python -from headroom.transforms import TransformPipeline, LLMLinguaCompressor, SmartCrusher - -# Combine with other transforms -pipeline = TransformPipeline([ - SmartCrusher(), # First: compress JSON - LLMLinguaCompressor(), # Then: ML compression on remaining text -]) - -result = pipeline.apply(messages, tokenizer) -``` - -## Proxy Integration - -Enable LLMLingua in the proxy server for automatic ML compression: - -```bash -# Enable LLMLingua in proxy (requires: pip install headroom-ai[llmlingua,proxy]) -headroom proxy --llmlingua - -# With custom settings -headroom proxy --llmlingua --llmlingua-device cuda --llmlingua-rate 0.4 - -# The proxy shows LLMLingua status at startup: -# LLMLingua: ENABLED (device=cuda, rate=0.4) -# -# If llmlingua is installed but not enabled, you'll see a helpful hint: -# LLMLingua: available (enable with --llmlingua for ML compression) -``` - -## Configuration Reference - -| Option | Default | Description | -|--------|---------|-------------| -| `device` | `"auto"` | Device to run model on: auto, cpu, cuda, mps | -| `code_compression_rate` | `0.4` | Keep 40% of tokens for code | -| `json_compression_rate` | `0.35` | Keep 35% of tokens for JSON | -| `text_compression_rate` | `0.25` | Keep 25% of tokens for text | -| `force_tokens` | `[]` | Tokens to always preserve | -| `drop_consecutive` | `True` | Drop consecutive whitespace | - -## Performance Characteristics - -| Metric | Value | -|--------|-------| -| Model size | ~500MB | -| Memory usage | ~1GB RAM | -| Cold start | 10-30s (first load) | -| Inference | 50-200ms per request | -| Compression | 80-95% | - -## Why Opt-In? - -LLMLingua adds significant dependencies and overhead: - -| Aspect | Default Proxy | With LLMLingua | -|--------|--------------|----------------| -| Dependencies | ~50MB | ~2GB | -| Cold start | <1s | 10-30s | -| Per-request | ~1-5ms | ~50-200ms | -| Compression | 70-90% | 80-95% | - -The default proxy is lightweight and fast. Enable LLMLingua when you need maximum compression and can accept the tradeoffs. - -## Troubleshooting - -### "Model not found" - -```bash -# Ensure llmlingua extra is installed -pip install "headroom-ai[llmlingua]" -``` - -### "CUDA out of memory" - -```python -# Force CPU mode -config = LLMLinguaConfig(device="cpu") -``` - -### "Slow compression" - -- Use GPU if available: `device="cuda"` -- Batch multiple compressions -- Consider using SmartCrusher for JSON (faster, similar results) diff --git a/wiki/macos-deployment.md b/wiki/macos-deployment.md index 3c9a5cecb..5e7e34350 100644 --- a/wiki/macos-deployment.md +++ b/wiki/macos-deployment.md @@ -202,21 +202,14 @@ Configure additional options in the plist `EnvironmentVariables` section: ANTHROPIC_API_KEY sk-ant-... - - HEADROOM_COMPRESSION_PROVIDER - llmlingua - - - HEADROOM_LLMLINGUA_DEVICE - mps ``` -**Note:** LLMLingua requires additional installation: - -```bash -pip install headroom-ai[llmlingua] -``` +**Note:** The earlier LLMLingua-2 launch-agent variables +(`HEADROOM_COMPRESSION_PROVIDER=llmlingua`, `HEADROOM_LLMLINGUA_DEVICE`, +the `headroom-ai[llmlingua]` extra) were retired with the +`--llmlingua` flag. For ML compression today, install the `[ml]` +extra and follow `wiki/transforms.md`. ### Crash Recovery @@ -683,4 +676,4 @@ A: The LaunchAgent setup is Anthropic-specific. For other providers, see [proxy. **Q: Does this work with Apple Silicon (M1/M2/M3)?** -A: Yes, fully compatible. For LLMLingua compression, use `--llmlingua-device mps` for Apple Silicon acceleration. +A: Yes, fully compatible. ML compression (Kompress, opt-in via `headroom-ai[ml]`) auto-detects MPS on Apple Silicon. diff --git a/wiki/metrics.md b/wiki/metrics.md index 5e9142fd6..7408558c1 100644 --- a/wiki/metrics.md +++ b/wiki/metrics.md @@ -303,8 +303,7 @@ curl http://localhost:8787/health { "status": "healthy", "version": "0.1.0", - "uptime_seconds": 3600, - "llmlingua_enabled": false + "uptime_seconds": 3600 } ``` diff --git a/wiki/proxy.md b/wiki/proxy.md index c52e02ff9..ae95a5d78 100644 --- a/wiki/proxy.md +++ b/wiki/proxy.md @@ -117,26 +117,12 @@ headroom proxy --no-intelligent-context headroom proxy --no-intelligent-scoring ``` -### LLMLingua Options (ML Compression) +### ML Compression — RETIRED `--llmlingua` flag -| Option | Default | Description | -|--------|---------|-------------| -| `--llmlingua` | `false` | Enable LLMLingua-2 ML-based compression | -| `--llmlingua-device` | `auto` | Device for model: `auto`, `cuda`, `cpu`, `mps` | -| `--llmlingua-rate` | `0.3` | Target compression rate (0.3 = keep 30% of tokens) | - -**Note:** LLMLingua requires additional dependencies: `pip install headroom-ai[llmlingua]` - -```bash -# Enable LLMLingua with GPU acceleration -headroom proxy --llmlingua --llmlingua-device cuda - -# More aggressive compression (keep only 20%) -headroom proxy --llmlingua --llmlingua-rate 0.2 - -# Conservative compression for code (keep 50%) -headroom proxy --llmlingua --llmlingua-rate 0.5 -``` +The `--llmlingua` / `--llmlingua-device` / `--llmlingua-rate` flags and +the `headroom-ai[llmlingua]` extra were retired and replaced by Kompress +(ModernBERT). For the current opt-in path, install `headroom-ai[ml]` +and see [transforms.md](transforms.md) and [ARCHITECTURE.md](ARCHITECTURE.md). ## API Endpoints @@ -341,41 +327,14 @@ client = OpenAI( ## Features -### LLMLingua ML Compression (Opt-In) +### ML Compression (Opt-In, Kompress) -When enabled, the proxy uses Microsoft's LLMLingua-2 model for ML-based token compression: - -```bash -headroom proxy --llmlingua -``` - -**How it works:** -- LLMLinguaCompressor is added to the transform pipeline (before RollingWindow) -- Automatically detects content type (JSON, code, text) and adjusts compression -- Stores original content in CCR for retrieval if needed - -**Startup feedback:** - -``` -# When enabled and available: -LLMLingua: ENABLED (device=cuda, rate=0.3) - -# When installed but not enabled (helpful hint): -LLMLingua: available (enable with --llmlingua for ML compression) - -# When enabled but not installed: -WARNING: LLMLingua requested but not installed. Install with: pip install headroom-ai[llmlingua] -``` - -**Why opt-in?** -| Concern | Default Proxy | With LLMLingua | -|---------|---------------|----------------| -| Dependencies | ~50MB | +2GB (torch, transformers) | -| Cold start | <1s | 10-30s (model load) | -| Memory | ~100MB | +1GB (model in RAM) | -| Overhead | <5ms | 50-200ms per request | - -Enable LLMLingua when maximum compression justifies the resource cost. +> The earlier LLMLingua-2 integration documented in this section +> (`--llmlingua`, `--llmlingua-device`, `--llmlingua-rate`, +> `headroom-ai[llmlingua]`, `LLMLinguaCompressor`) was retired and +> replaced by **Kompress** (ModernBERT). Install with `pip install +> 'headroom-ai[ml]'`. See [transforms.md](transforms.md) and +> [ARCHITECTURE.md](ARCHITECTURE.md) for current configuration. ### Semantic Caching diff --git a/wiki/transforms.md b/wiki/transforms.md index 307a54354..4c9d93493 100644 --- a/wiki/transforms.md +++ b/wiki/transforms.md @@ -325,73 +325,16 @@ intelligent_config = IntelligentContextConfig( --- -## LLMLinguaCompressor (Optional) +## LLMLinguaCompressor — RETIRED -ML-based compression using Microsoft's LLMLingua-2 model. - -### When to Use - -| Transform | Best For | Speed | Compression | -|-----------|----------|-------|-------------| -| SmartCrusher | JSON arrays | ~1ms | 70-90% | -| Text Utilities | Search/logs | ~1ms | 50-90% | -| **LLMLinguaCompressor** | Any text, max compression | 50-200ms | 80-95% | - -### Installation - -```bash -pip install "headroom-ai[llmlingua]" # Adds ~2GB -``` - -### Configuration - -```python -from headroom.transforms import LLMLinguaCompressor, LLMLinguaConfig - -config = LLMLinguaConfig( - device="auto", # auto, cuda, cpu, mps - target_compression_rate=0.3, # Keep 30% of tokens - min_tokens_for_compression=100, # Skip small content - code_compression_rate=0.4, # Conservative for code - json_compression_rate=0.35, # Moderate for JSON - text_compression_rate=0.25, # Aggressive for text - enable_ccr=True, # Store original for retrieval -) - -compressor = LLMLinguaCompressor(config) -``` - -### Content-Aware Rates - -LLMLinguaCompressor auto-detects content type: - -| Content Type | Default Rate | Behavior | -|--------------|--------------|----------| -| Code | 0.4 | Conservative - preserves syntax | -| JSON | 0.35 | Moderate - keeps structure | -| Text | 0.3 | Aggressive - maximum compression | - -### Memory Management - -```python -from headroom.transforms import ( - is_llmlingua_model_loaded, - unload_llmlingua_model, -) - -# Check if model is loaded -print(is_llmlingua_model_loaded()) # True/False - -# Free ~1GB RAM when done -unload_llmlingua_model() -``` - -### Proxy Integration - -```bash -# Enable in proxy -headroom proxy --llmlingua --llmlingua-device cuda --llmlingua-rate 0.3 -``` +The earlier LLMLingua-2 integration (`LLMLinguaCompressor`, +`LLMLinguaConfig`, `is_llmlingua_model_loaded`, `unload_llmlingua_model`, +the `headroom-ai[llmlingua]` extra, and the `--llmlingua` proxy flag) +was retired in 0.9.x and replaced by **Kompress** (ModernBERT). +`pip install 'headroom-ai[llmlingua]'` no longer resolves; use the +`[ml]` extra instead. The Kompress transform shipped with the proxy +runs as Transform 4 in the live-zone pipeline (see +[ARCHITECTURE.md](ARCHITECTURE.md)). --- @@ -405,14 +348,14 @@ AST-based compression for source code using tree-sitter. |-----------|----------|-------|-------------| | SmartCrusher | JSON arrays | ~1ms | 70-90% | | **CodeAwareCompressor** | Source code | ~10-50ms | 40-70% | -| LLMLinguaCompressor | Any text | 50-200ms | 80-95% | +| Kompress (ML) | Any text | 50-200ms | 80-95% | ### Key Benefits - **Syntax validity guaranteed** — Output always parses correctly - **Preserves critical structure** — Imports, signatures, types, error handlers - **Multi-language support** — Python, JavaScript, TypeScript, Go, Rust, Java, C, C++ -- **Lightweight** — ~50MB vs ~1GB for LLMLingua +- **Lightweight** — ~50MB vs ~1GB for the ML compressor ### Installation @@ -436,7 +379,6 @@ config = CodeCompressorConfig( max_body_lines=5, # Lines to keep per function body min_tokens_for_compression=100, # Skip small content language_hint=None, # Auto-detect if None - fallback_to_llmlingua=True, # Use LLMLingua for unknown langs ) compressor = CodeAwareCompressor(config) @@ -553,9 +495,10 @@ print(result.routing_log) # List of routing decisions | SEARCH | Grep/find output | SearchCompressor | | LOG | Log files | LogCompressor | | TEXT | Plain text | TextCompressor | -| LLMLINGUA | Any (max compression) | LLMLinguaCompressor | | PASSTHROUGH | Small content | None | +(The earlier `LLMLINGUA` strategy was retired with the LLMLingua integration; ML compression is now provided by Kompress.) + ### Content Detection The router automatically detects content types by analyzing the content itself: @@ -572,7 +515,7 @@ No manual hints required - the router inspects content directly. ContentRouter records all compressions to TOIN (Tool Output Intelligence Network) for cross-user learning: -- **All strategies tracked**: Code, search, logs, text, and LLMLingua compressions are recorded +- **All strategies tracked**: Code, search, logs, text, and ML compressions are recorded - **Retrieval feedback**: When users retrieve original content via CCR, TOIN learns which compressions need expansion - **Pattern learning**: TOIN builds signatures for each content type to improve future compressions @@ -597,21 +540,9 @@ result = pipeline.transform(messages) print(f"Saved {result.tokens_saved} tokens") ``` -### With LLMLingua (Optional) +### With ML compression (Optional, Kompress) -```python -from headroom.transforms import ( - TransformPipeline, SmartCrusher, CacheAligner, - RollingWindow, LLMLinguaCompressor -) - -pipeline = TransformPipeline([ - CacheAligner(), # 1. Stabilize prefix - SmartCrusher(), # 2. Compress JSON arrays - LLMLinguaCompressor(), # 3. ML compression on remaining text - RollingWindow(), # 4. Final size constraint (always last) -]) -``` +The earlier hand-assembled `TransformPipeline([..., LLMLinguaCompressor(), ...])` recipe is no longer supported. ML compression now ships as part of the live-zone pipeline when the `[ml]` extra is installed; see [ARCHITECTURE.md](ARCHITECTURE.md) for the current placement. ### Recommended Order @@ -619,13 +550,13 @@ pipeline = TransformPipeline([ |-------|-----------|---------| | 1 | CacheAligner | Stabilize prefix for caching | | 2 | SmartCrusher | Compress JSON tool outputs | -| 3 | LLMLinguaCompressor | ML compression (optional) | +| 3 | Kompress (ML) | ML compression on remaining text (optional, `[ml]` extra) | | 4 | RollingWindow | Enforce token limits (always last) | **Why this order?** - CacheAligner first to maximize prefix stability - SmartCrusher handles JSON arrays efficiently -- LLMLingua compresses remaining long text +- Kompress compresses remaining long text - RollingWindow truncates only if still over limit ---