Remove LLMLingua: Kompress is the sole text compressor

LLMLingua was the original ML text compressor (BERT-based). Kompress
(ModernBERT, trained on 330K structured tool outputs) replaced it with
better compression quality and simpler architecture.

Removed across 35 files:
- Deleted headroom/transforms/llmlingua_compressor.py
- Deleted tests/test_transforms/test_llmlingua_compressor.py
- Deleted tests/test_proxy_llmlingua.py
- Removed all enable_llmlingua config, _get_llmlingua methods,
  LLMLingua fallback paths, LLMLINGUA strategy enum values
- Removed CLI flags, model configs, compression handler references
- Simplified ContentRouter: Kompress is primary and only text compressor
This commit is contained in:
chopratejas 2026-03-26 11:11:00 -07:00
parent 7a79aa0792
commit 3290a3d582
35 changed files with 126 additions and 2461 deletions

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
# Swift SDK (separate repo)
swift/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

View file

@ -44,13 +44,13 @@ try:
except ImportError:
HEADROOM_AVAILABLE = False
# LLMLingua imports (SOTA baseline)
# Kompress imports (ML baseline)
try:
from headroom.transforms.llmlingua_compressor import LLMLinguaCompressor, LLMLinguaConfig
from headroom.transforms.kompress_compressor import KompressCompressor, is_kompress_available
LLMLINGUA_AVAILABLE = True
KOMPRESS_AVAILABLE = is_kompress_available()
except ImportError:
LLMLINGUA_AVAILABLE = False
KOMPRESS_AVAILABLE = False
@dataclass
@ -427,27 +427,21 @@ Provide a structured summary that retains all critical details."""
return summary, cost, latency
def llmlingua_compress(data: list[dict]) -> tuple[str, dict]:
def kompress_compress(data: list[dict]) -> tuple[str, dict]:
"""
Use LLMLingua-2 (Microsoft SOTA) for ML-based compression.
Use Kompress (ModernBERT) for ML-based compression.
Returns (compressed_text, metadata).
"""
if not LLMLINGUA_AVAILABLE:
raise RuntimeError(
"LLMLingua not available. Install with: pip install headroom-ai[llmlingua]"
)
if not KOMPRESS_AVAILABLE:
raise RuntimeError("Kompress not available. Install with: pip install headroom-ai[ml]")
config = LLMLinguaConfig(
target_compression_rate=0.3, # Keep ~30% of tokens
min_tokens_for_compression=50,
)
compressor = LLMLinguaCompressor(config)
compressor = KompressCompressor()
# Convert data to string for LLMLingua (it works on text, not structured data)
# Convert data to string for Kompress (it works on text, not structured data)
data_str = json.dumps(data, indent=2)
start = time.time()
result = compressor.compress(data_str, content_type="json")
result = compressor.compress(data_str)
latency = (time.time() - start) * 1000
metadata = {
@ -590,7 +584,7 @@ class BenchmarkConfig:
max_truncate_items: int = 20
max_headroom_items: int = 20
run_summarization: bool = True # Can disable to save cost
run_llmlingua: bool = True # Run LLMLingua-2 (SOTA baseline)
run_kompress: bool = True # Run Kompress (ML baseline)
def run_scenario_benchmark(
@ -701,12 +695,12 @@ def run_scenario_benchmark(
except Exception as e:
print(f" Summarization failed: {e}")
# --- LLMLINGUA-2 (SOTA) ---
if config.run_llmlingua:
print("\n[3/4] Running LLMLingua-2 (Microsoft SOTA)...")
if LLMLINGUA_AVAILABLE:
# --- KOMPRESS (ML baseline) ---
if config.run_kompress:
print("\n[3/4] Running Kompress (ModernBERT ML baseline)...")
if KOMPRESS_AVAILABLE:
try:
ll_compressed, ll_metadata = llmlingua_compress(scenario.data)
ll_compressed, ll_metadata = kompress_compress(scenario.data)
ll_tokens = count_tokens(ll_compressed)
ll_answers = []
@ -729,7 +723,7 @@ def run_scenario_benchmark(
results.append(
ApproachResult(
approach="llmlingua-2",
approach="kompress",
scenario=scenario.name,
tokens_original=original_tokens,
tokens_after=ll_tokens,
@ -747,9 +741,9 @@ def run_scenario_benchmark(
print(f" Accuracy: {ll_accuracy:.1%}")
print(f" Compression latency: {ll_metadata['latency_ms']:.1f}ms")
except Exception as e:
print(f" LLMLingua-2 failed: {e}")
print(f" Kompress failed: {e}")
else:
print(" LLMLingua-2 not available. Install with: pip install headroom-ai[llmlingua]")
print(" Kompress not available. Install with: pip install headroom-ai[ml]")
# --- HEADROOM ---
print("\n[4/4] Running Headroom...")

View file

@ -196,7 +196,6 @@ class CCRToolInjector:
_detected_hashes: list[str] = field(default_factory=list)
# Multiple marker patterns to match different compressors:
# - SmartCrusher: [100 items compressed to 10. Retrieve more: hash=abc123]
# - LLMLingua: [1000 items compressed to 300. Retrieve more: hash=abc123]
# - Kompress: [100 lines compressed to 10. Retrieve more: hash=abc123]
# - LogCompressor: [200 lines compressed to 20. Retrieve more: hash=abc123]
# - SearchCompressor: [50 matches compressed to 5. Retrieve more: hash=abc123]

View file

@ -21,20 +21,6 @@ from .main import main
@click.option("--no-rate-limit", is_flag=True, help="Disable rate limiting")
@click.option("--log-file", default=None, help="Path to JSONL log file")
@click.option("--budget", type=float, default=None, help="Daily budget limit in USD")
# LLMLingua ML-based compression (ON by default if installed)
@click.option("--no-llmlingua", is_flag=True, help="Disable LLMLingua-2 ML-based compression")
@click.option(
"--llmlingua-device",
type=click.Choice(["auto", "cuda", "cpu", "mps"]),
default="auto",
help="Device for LLMLingua model (default: auto)",
)
@click.option(
"--llmlingua-rate",
type=float,
default=0.3,
help="LLMLingua compression rate 0.0-1.0 (default: 0.3 = keep 30%)",
)
# Code-aware compression (ON by default if installed)
@click.option("--no-code-aware", is_flag=True, help="Disable AST-based code compression")
# Read lifecycle (ON by default: compresses stale/superseded Read outputs)
@ -149,9 +135,6 @@ def proxy(
no_rate_limit: bool,
log_file: str | None,
budget: float | None,
no_llmlingua: bool,
llmlingua_device: str,
llmlingua_rate: float,
no_code_aware: bool,
no_read_lifecycle: bool,
no_intelligent_context: bool,
@ -225,10 +208,6 @@ def proxy(
rate_limit_enabled=not no_rate_limit,
log_file=log_file,
budget_limit_usd=budget,
# LLMLingua: ON by default (use --no-llmlingua to disable)
llmlingua_enabled=not no_llmlingua,
llmlingua_device=llmlingua_device,
llmlingua_target_rate=llmlingua_rate,
# Code-aware: ON by default (use --no-code-aware to disable)
code_aware_enabled=not no_code_aware,
# Read lifecycle: ON by default (use --no-read-lifecycle to disable)

View file

@ -203,7 +203,7 @@ def _get_pipeline() -> Any:
# Default pipeline: CacheAligner → ContentRouter → IntelligentContext
# CacheAligner: stabilizes prefix for provider KV cache hits
# ContentRouter: routes to the right compressor per content type
# (SmartCrusher for JSON, CodeCompressor for code, LLMLingua for text)
# (SmartCrusher for JSON, CodeCompressor for code, Kompress for text)
# IntelligentContext: enforces token limits with score-based dropping
_pipeline = TransformPipeline()
logger.debug("Headroom compression pipeline initialized")

View file

@ -3,7 +3,7 @@
This module provides intelligent, automatic compression that:
1. Detects content type using ML (Magika)
2. Preserves structure (keys, signatures, templates)
3. Compresses content with LLMLingua
3. Compresses content with Kompress
4. Enables retrieval via CCR
Quick Start:

View file

@ -4,7 +4,7 @@ Each handler knows how to extract structural information from a specific
content type and create a StructureMask marking what should be preserved.
Handlers don't compress - they only identify structure. The actual
compression is done by LLMLingua on the non-structural parts.
compression is done by Kompress on the non-structural parts.
"""
from headroom.compression.handlers.base import (

View file

@ -181,7 +181,7 @@ class BaseStructureHandler(ABC):
Subclasses may override for more sophisticated tokenization.
For mask purposes, character-level is often sufficient and
aligns well with LLMLingua's token-level compression.
aligns well with token-level compression.
Args:
content: Content to tokenize.

View file

@ -1,11 +1,11 @@
"""Structure mask system for compression.
A StructureMask identifies which parts of content are "structural" (should be
preserved) vs "compressible" (can be compressed by LLMLingua).
preserved) vs "compressible" (can be compressed by Kompress).
This separates the concerns of:
1. Structure detection (handlers) - What tokens are navigational?
2. Content compression (LLMLingua) - What tokens can be removed?
2. Content compression (Kompress) - What tokens can be removed?
The mask is content-agnostic - it's just a boolean array aligned to tokens.
"""
@ -21,7 +21,7 @@ class StructureMask:
"""A mask identifying structural vs compressible tokens.
The mask is aligned to a token sequence. True means "preserve this token"
(it's structural/navigational), False means "compressible" (LLMLingua can
(it's structural/navigational), False means "compressible" (Kompress can
potentially remove it).
Attributes:
@ -207,7 +207,7 @@ def apply_mask_to_text(
Args:
text: Original text.
mask: Structure mask aligned to tokens.
compress_fn: Function to compress text (e.g., LLMLingua).
compress_fn: Function to compress text (e.g., Kompress).
tokenizer_decode: Optional function to decode tokens to text.
If not provided, assumes tokens are strings and joins them.
@ -245,7 +245,7 @@ class EntropyScore:
be preserved because:
1. They're information-dense (can't be reconstructed)
2. They're often identifiers (semantically important)
3. LLMLingua may mangle them
3. Token-level compressors may mangle them
This is a self-signal - no external classifier needed.
"""

View file

@ -3,7 +3,7 @@
This is the main entry point for compression. It:
1. Detects content type using Magika (ML)
2. Extracts structure using appropriate handler
3. Compresses non-structural content with LLMLingua
3. Compresses non-structural content with Kompress
4. Optionally stores original in CCR for retrieval
Usage:
@ -51,7 +51,7 @@ class UniversalCompressorConfig:
Attributes:
use_magika: Use ML-based detection (requires magika package).
use_llmlingua: Use LLMLingua for content compression.
use_kompress: Use Kompress for content compression.
use_entropy_preservation: Preserve high-entropy tokens (UUIDs, etc.).
entropy_threshold: Threshold for entropy-based preservation.
min_content_length: Minimum content length to compress.
@ -60,7 +60,7 @@ class UniversalCompressorConfig:
"""
use_magika: bool = True
use_llmlingua: bool = True
use_kompress: bool = True
use_entropy_preservation: bool = True
entropy_threshold: float = 0.85
min_content_length: int = 100
@ -139,7 +139,7 @@ class UniversalCompressor:
config: Compression configuration.
handlers: Custom handlers for content types.
compress_fn: Custom compression function. If None, uses
LLMLingua when available, else simple truncation.
Kompress when available, else simple truncation.
"""
self.config = config or UniversalCompressorConfig()
@ -165,18 +165,18 @@ class UniversalCompressor:
def _get_default_compress_fn(self) -> Callable[[str], str]:
"""Get default compression function.
Returns LLMLingua wrapper if available, else simple truncation.
Returns Kompress wrapper if available, else simple truncation.
"""
if self.config.use_llmlingua:
if self.config.use_kompress:
try:
return self._llmlingua_compress
return self._kompress_compress
except ImportError:
logger.info("LLMLingua not available, using simple compression")
logger.info("Kompress not available, using simple compression")
return self._simple_compress
def _llmlingua_compress(self, text: str) -> str:
"""Compress using LLMLingua.
def _kompress_compress(self, text: str) -> str:
"""Compress using Kompress.
Args:
text: Text to compress.
@ -185,16 +185,15 @@ class UniversalCompressor:
Compressed text.
"""
try:
from headroom.transforms.llmlingua_compressor import compress_with_llmlingua
from headroom.transforms.kompress_compressor import KompressCompressor
return compress_with_llmlingua(
text,
compression_rate=self.config.compression_ratio_target,
)
compressor = KompressCompressor()
result = compressor.compress(text)
return result.compressed
except ImportError:
return self._simple_compress(text)
except Exception as e:
logger.warning("LLMLingua compression failed: %s", e)
logger.warning("Kompress compression failed: %s", e)
return self._simple_compress(text)
def _simple_compress(self, text: str) -> str:

View file

@ -619,7 +619,7 @@ class HeadroomConfig:
prefix_freeze: PrefixFreezeConfig = field(default_factory=PrefixFreezeConfig)
# Content Router - intelligent content-type based compression
# Routes content to appropriate compressor (LLMLingua for text, SmartCrusher for JSON,
# Routes content to appropriate compressor (Kompress for text, SmartCrusher for JSON,
# CodeCompressor for code, LogCompressor for logs, etc.)
content_router_enabled: bool = True

View file

@ -353,7 +353,7 @@ def generate_factual_test_cases() -> list[BatchTestCase]:
Context:
The Headroom SDK is a context optimization layer for LLM applications. It was created
by Anthropic in 2024. The main features include SmartCrusher for JSON compression,
LLMLingua for text compression, and CCR (Compress-Cache-Retrieve) for reversible
Kompress for text compression, and CCR (Compress-Cache-Retrieve) for reversible
compression. The SDK supports Python 3.9+ and can save up to 70% of tokens on
large JSON arrays.
@ -363,7 +363,7 @@ Question: What percentage of tokens can the SDK save on large JSON arrays?""",
),
ground_truth="70%",
ground_truth_keywords=["70", "percent", "%"],
context_facts=["70%", "SmartCrusher", "LLMLingua", "CCR", "Python 3.9"],
context_facts=["70%", "SmartCrusher", "Kompress", "CCR", "Python 3.9"],
),
BatchTestCase(
id="factual_002",

View file

@ -4,7 +4,6 @@ This module evaluates whether HTMLExtractor preserves the information
that LLMs need to answer questions about web content. We compare:
1. LLM answers from original HTML
2. LLM answers from HTMLExtractor output
3. LLM answers from LLMLingua baseline (current fallback)
Uses LLM-as-judge to score answer quality on a 1-5 scale.
"""
@ -81,7 +80,7 @@ class HTMLEvalResult:
# Answers from different methods
answer_from_original: str
answer_from_extracted: str
answer_from_baseline: str | None = None # LLMLingua baseline
answer_from_baseline: str | None = None # Baseline comparison
# Judge scores (1-5 scale)
extracted_score: float = 0.0
@ -211,7 +210,7 @@ class HTMLExtractionEvaluator:
Args:
answer_model: Model for generating answers from content.
judge_model: Model for judging answer quality.
compare_baseline: Whether to also test LLMLingua baseline.
compare_baseline: Whether to also test Kompress baseline.
provider: API provider ("openai", "anthropic", "litellm").
"""
self.answer_model = answer_model
@ -221,7 +220,7 @@ class HTMLExtractionEvaluator:
# Lazy-loaded components
self._extractor: HTMLExtractor | None = None
self._llmlingua: Any = None
self._kompress: Any = None
self._judge_fn: Callable[[str, str, str], tuple[float, str]] | None = None
self._answer_fn: Any = None
@ -235,16 +234,16 @@ class HTMLExtractionEvaluator:
return self._extractor
@property
def llmlingua(self) -> Any:
"""Lazy-load LLMLingua compressor for baseline."""
if self._llmlingua is None and self.compare_baseline:
def kompress(self) -> Any:
"""Lazy-load Kompress compressor for baseline."""
if self._kompress is None and self.compare_baseline:
try:
from headroom.transforms.llmlingua_compressor import LLMLinguaCompressor
from headroom.transforms.kompress_compressor import KompressCompressor
self._llmlingua = LLMLinguaCompressor()
self._kompress = KompressCompressor()
except ImportError:
logger.warning("LLMLingua not available for baseline comparison")
return self._llmlingua
logger.warning("Kompress not available for baseline comparison")
return self._kompress
@property
def judge_fn(self) -> Callable[[str, str, str], tuple[float, str]]:
@ -429,14 +428,14 @@ Answer concisely and factually based only on the content provided."""
answer_from_extracted,
)
# Optionally compare with LLMLingua baseline
# Optionally compare with Kompress baseline
baseline_answer = None
baseline_score = None
baseline_reasoning = None
if self.compare_baseline and self.llmlingua:
if self.compare_baseline and self.kompress:
try:
baseline_result = self.llmlingua.compress(case.html)
baseline_result = self.kompress.compress(case.html)
baseline_content = baseline_result.compressed
baseline_answer = self._get_answer(baseline_content, case.question)
baseline_score, baseline_reasoning = self.judge_fn(

View file

@ -12,7 +12,6 @@ Usage:
# Or use environment variables to override at runtime:
# HEADROOM_SENTENCE_TRANSFORMER=intfloat/e5-small-v2
# HEADROOM_SIGLIP=google/siglip-base-patch16-224
# HEADROOM_LLMLINGUA=microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank
"""
from __future__ import annotations
@ -31,7 +30,6 @@ class MLModelConfig:
Environment variables can override any default:
- HEADROOM_SENTENCE_TRANSFORMER
- HEADROOM_SIGLIP
- HEADROOM_LLMLINGUA
- HEADROOM_SPACY
- HEADROOM_TECHNIQUE_ROUTER
@ -47,10 +45,6 @@ class MLModelConfig:
Default: google/siglip-base-patch16-224 (~400MB)
Alternative: google/siglip-so400m-patch14-384 (larger, more accurate)
llmlingua: Model for ML-based prompt compression.
Default: microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank (~350MB)
Alternative: microsoft/llmlingua-2-xlm-roberta-large-meetingbank (~1GB, slightly more accurate)
spacy: Model for named entity recognition.
Default: en_core_web_sm (~40MB)
Alternative: en_core_web_md (~120MB, more accurate)
@ -70,13 +64,6 @@ class MLModelConfig:
default_factory=lambda: os.environ.get("HEADROOM_SIGLIP", "google/siglip-base-patch16-224")
)
# Prompt Compression (LLMLingua-2)
llmlingua: str = field(
default_factory=lambda: os.environ.get(
"HEADROOM_LLMLINGUA", "microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank"
)
)
# Named Entity Recognition (spaCy)
spacy: str = field(default_factory=lambda: os.environ.get("HEADROOM_SPACY", "en_core_web_sm"))
@ -99,9 +86,6 @@ class MLModelConfig:
"google/siglip-base-patch16-224": 400,
"google/siglip-so400m-patch14-384": 900,
"google/siglip-large-patch16-384": 1200,
# LLMLingua
"microsoft/llmlingua-2-xlm-roberta-large-meetingbank": 1000,
"microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank": 350,
# spaCy
"en_core_web_sm": 40,
"en_core_web_md": 120,
@ -131,7 +115,6 @@ class MLModelConfig:
return (
self.get_memory_estimate(self.sentence_transformer)
+ self.get_memory_estimate(self.siglip)
+ self.get_memory_estimate(self.llmlingua)
+ self.get_memory_estimate(self.spacy)
+ self.get_memory_estimate(self.technique_router)
)
@ -160,8 +143,3 @@ def get_default_spacy_model() -> str:
def get_default_siglip_model() -> str:
"""Get the default SIGLIP model name."""
return ML_MODEL_DEFAULTS.siglip
def get_default_llmlingua_model() -> str:
"""Get the default LLMLingua model name."""
return ML_MODEL_DEFAULTS.llmlingua

View file

@ -252,34 +252,6 @@ class MLModelRegistry:
result: tuple[Any, Any] = instance._models[key]
return result
# =========================================================================
# LLMLingua (uses existing singleton pattern)
# =========================================================================
@classmethod
def get_llmlingua(cls, device: str | None = None, model_name: str | None = None) -> Any:
"""Get the LLMLingua compressor.
Note: LLMLingua already has its own singleton in llmlingua_compressor.py.
This method delegates to that implementation.
Args:
device: Device to use. Auto-detected if None.
model_name: Model name (default: microsoft/llmlingua-2-xlm-roberta-large-meetingbank).
Returns:
PromptCompressor instance.
"""
from headroom.transforms.llmlingua_compressor import _get_llmlingua_compressor
if device is None:
device = cls._detect_device()
if model_name is None:
model_name = ML_MODEL_DEFAULTS.llmlingua
return _get_llmlingua_compressor(model_name=model_name, device=device)
# =========================================================================
# Utility Methods
# =========================================================================

View file

@ -562,7 +562,7 @@ def _generate_recommendations(report: PerfReport) -> list[str]:
if len(slow) > len(report.perf_records) * 0.2:
recs.append(
f"{len(slow)} requests took >500ms for optimization — "
"consider disabling LLMLingua or reducing transform pipeline"
"consider reducing transform pipeline"
)
if report.router_records:

View file

@ -93,7 +93,6 @@ from headroom.telemetry import get_telemetry_collector
from headroom.telemetry.toin import get_toin
from headroom.tokenizers import get_tokenizer
from headroom.transforms import (
_LLMLINGUA_AVAILABLE,
CacheAligner,
CodeAwareCompressor,
CodeCompressorConfig,
@ -127,10 +126,6 @@ def _get_image_compressor():
return _image_compressor if _image_compressor else None
# Conditionally import LLMLingua if available
if _LLMLINGUA_AVAILABLE:
from headroom.transforms import LLMLinguaCompressor, LLMLinguaConfig
# Try to import LiteLLM for pricing
try:
import litellm
@ -648,11 +643,6 @@ class ProxyConfig:
ccr_proactive_expansion: bool = True # Proactively expand based on query relevance
ccr_max_proactive_expansions: int = 2 # Max contexts to proactively expand per turn
# LLMLingua ML-based compression (ON by default if installed)
llmlingua_enabled: bool = True # Enable LLMLingua-2 for ML-based compression
llmlingua_device: str = "auto" # Device: 'auto', 'cuda', 'cpu', 'mps'
llmlingua_target_rate: float = 0.3 # Target compression rate (0.3 = keep 30%)
# Code-aware compression (ON by default if installed)
code_aware_enabled: bool = True # Enable AST-based code compression
@ -1641,9 +1631,8 @@ class HeadroomProxy:
if config.smart_routing:
# Smart routing: ContentRouter handles all content types intelligently
# It lazy-loads compressors (including LLMLingua) only when needed
# It lazy-loads compressors only when needed
router_config = ContentRouterConfig(
enable_llmlingua=config.llmlingua_enabled,
enable_code_aware=config.code_aware_enabled,
tool_profiles=config.tool_profiles,
read_lifecycle=ReadLifecycleConfig(enabled=config.read_lifecycle),
@ -1656,7 +1645,6 @@ class HeadroomProxy:
ContentRouter(router_config),
context_manager,
]
self._llmlingua_status = "lazy" if config.llmlingua_enabled else "disabled"
self._code_aware_status = "lazy" if config.code_aware_enabled else "disabled"
else:
# Legacy mode: sequential pipeline
@ -1675,8 +1663,6 @@ class HeadroomProxy:
),
context_manager,
]
# Add LLMLingua if enabled and available
self._llmlingua_status = self._setup_llmlingua(config, transforms)
# Add CodeAware if enabled and available
self._code_aware_status = self._setup_code_aware(config, transforms)
@ -1893,38 +1879,6 @@ class HeadroomProxy:
self._compression_caches[session_id] = CompressionCache()
return self._compression_caches[session_id]
def _setup_llmlingua(self, config: ProxyConfig, transforms: list) -> str:
"""Set up LLMLingua compression if enabled.
Args:
config: Proxy configuration
transforms: Transform list to append to
Returns:
Status string for logging: 'enabled', 'disabled', 'available', 'unavailable'
"""
if config.llmlingua_enabled:
if _LLMLINGUA_AVAILABLE:
llmlingua_config = LLMLinguaConfig(
device=config.llmlingua_device,
target_compression_rate=config.llmlingua_target_rate,
enable_ccr=config.ccr_inject_tool, # Link to CCR
)
# Insert before RollingWindow (which should be last)
# LLMLingua works best on individual tool outputs before windowing
transforms.insert(-1, LLMLinguaCompressor(llmlingua_config))
return "enabled"
else:
logger.warning(
"LLMLingua requested but not installed. "
"Install with: pip install headroom-ai[llmlingua]"
)
return "unavailable"
else:
if _LLMLINGUA_AVAILABLE:
return "available" # Available but not enabled - hint to user
return "disabled"
def _setup_code_aware(self, config: ProxyConfig, transforms: list) -> str:
"""Set up code-aware compression if enabled.
@ -2013,8 +1967,6 @@ class HeadroomProxy:
# Update internal status from eager loading results
if eager_status.get("kompress") == "enabled":
self._kompress_status = "enabled"
if eager_status.get("llmlingua") == "enabled":
self._llmlingua_status = "enabled"
if eager_status.get("code_aware") == "enabled":
self._code_aware_status = "enabled"
@ -2024,18 +1976,6 @@ class HeadroomProxy:
elif self.config.optimize:
logger.info("Kompress: not installed (pip install headroom-ai[ml] for ML compression)")
if self._llmlingua_status == "enabled":
logger.info(
f"LLMLingua: ENABLED (device={self.config.llmlingua_device}, "
f"rate={self.config.llmlingua_target_rate})"
)
elif self._kompress_status == "enabled":
logger.info("LLMLingua: skipped (Kompress is active)")
elif self._llmlingua_status == "lazy":
logger.info("LLMLingua: LAZY (will load when prose content detected)")
elif self._llmlingua_status == "disabled":
logger.info("LLMLingua: DISABLED")
if self._code_aware_status == "enabled":
logger.info("Code-Aware: ENABLED (AST-based compression)")
if "tree_sitter" in eager_status:
@ -7969,21 +7909,6 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
return app
def _get_llmlingua_banner_status(config: ProxyConfig) -> str:
"""Get LLMLingua status line for banner."""
if config.llmlingua_enabled:
if _LLMLINGUA_AVAILABLE:
return (
f"ENABLED (device={config.llmlingua_device}, rate={config.llmlingua_target_rate})"
)
else:
return "NOT INSTALLED (pip install headroom-ai[llmlingua])"
else:
if _LLMLINGUA_AVAILABLE:
return "DISABLED (remove --no-llmlingua to enable)"
return "DISABLED"
def _get_code_aware_banner_status(config: ProxyConfig) -> str:
"""Get code-aware compression status line for banner."""
if config.code_aware_enabled:
@ -8016,7 +7941,6 @@ def run_server(
config = config or ProxyConfig()
app = create_app(config)
llmlingua_status = _get_llmlingua_banner_status(config)
code_aware_status = _get_code_aware_banner_status(config)
# Format connection pool info
@ -8055,7 +7979,6 @@ def run_server(
Rate Limiting: {"ENABLED " if config.rate_limit_enabled else "DISABLED"} ({config.rate_limit_requests_per_minute} req/min, {config.rate_limit_tokens_per_minute:,} tok/min)
Retry: {"ENABLED " if config.retry_enabled else "DISABLED"} (max {config.retry_max_attempts} attempts)
Cost Tracking: {"ENABLED " if config.cost_tracking_enabled else "DISABLED"} (budget: {"$" + str(config.budget_limit_usd) + "/" + config.budget_period if config.budget_limit_usd else "unlimited"})
LLMLingua: {llmlingua_status:<52}
Code-Aware: {code_aware_status:<52}
HTTP/2: {http2_status:<52}
Conn Pool: {pool_info:<52}
@ -8269,30 +8192,6 @@ if __name__ == "__main__":
help="Disable smart routing (use legacy sequential pipeline)",
)
# LLMLingua ML-based compression
parser.add_argument(
"--llmlingua",
action="store_true",
help="Enable LLMLingua-2 ML-based compression (requires: pip install headroom-ai[llmlingua])",
)
parser.add_argument(
"--no-llmlingua",
action="store_true",
help="Disable LLMLingua compression",
)
parser.add_argument(
"--llmlingua-device",
choices=["auto", "cuda", "cpu", "mps"],
default="auto",
help="Device for LLMLingua model (default: auto)",
)
parser.add_argument(
"--llmlingua-rate",
type=float,
default=0.3,
help="LLMLingua target compression rate, 0.0-1.0 (default: 0.3 = keep 30%%)",
)
# Code-aware compression
parser.add_argument(
"--code-aware",
@ -8310,7 +8209,6 @@ if __name__ == "__main__":
# Environment variable defaults (HEADROOM_* prefix)
# CLI args override env vars, env vars override ProxyConfig defaults
env_smart_routing = _get_env_bool("HEADROOM_SMART_ROUTING", True)
env_llmlingua = _get_env_bool("HEADROOM_LLMLINGUA_ENABLED", True)
env_code_aware = _get_env_bool("HEADROOM_CODE_AWARE_ENABLED", True)
env_optimize = _get_env_bool("HEADROOM_OPTIMIZE", True)
env_cache = _get_env_bool("HEADROOM_CACHE_ENABLED", True)
@ -8319,11 +8217,6 @@ if __name__ == "__main__":
# Determine settings: CLI flags override env vars
# --no-X explicitly disables, --X explicitly enables, neither uses env var
smart_routing = env_smart_routing if not args.no_smart_routing else False
llmlingua_enabled = (
env_llmlingua
if not (args.llmlingua or args.no_llmlingua)
else (args.llmlingua or not args.no_llmlingua)
)
code_aware_enabled = (
env_code_aware
if not (args.code_aware or args.no_code_aware)
@ -8364,9 +8257,6 @@ if __name__ == "__main__":
else os.environ.get("HEADROOM_LOG_FILE"),
log_full_messages=args.log_messages or _get_env_bool("HEADROOM_LOG_MESSAGES", False),
smart_routing=smart_routing,
llmlingua_enabled=llmlingua_enabled,
llmlingua_device=_get_env_str("HEADROOM_LLMLINGUA_DEVICE", args.llmlingua_device),
llmlingua_target_rate=_get_env_float("HEADROOM_LLMLINGUA_RATE", args.llmlingua_rate),
code_aware_enabled=code_aware_enabled,
# Connection pool settings
max_connections=_get_env_int("HEADROOM_MAX_CONNECTIONS", args.max_connections),

View file

@ -25,21 +25,6 @@ from .search_compressor import (
from .smart_crusher import SmartCrusher, SmartCrusherConfig
from .tool_crusher import ToolCrusher
# ML-based compression (optional dependency)
try:
from .llmlingua_compressor import ( # noqa: F401
LLMLinguaCompressor,
LLMLinguaConfig,
LLMLinguaResult,
compress_with_llmlingua,
is_llmlingua_model_loaded,
unload_llmlingua_model,
)
_LLMLINGUA_AVAILABLE = True
except ImportError:
_LLMLINGUA_AVAILABLE = False
# HTML content extraction (optional dependency - requires trafilatura)
try:
from .html_extractor import ( # noqa: F401
@ -122,25 +107,10 @@ __all__ = [
"MessageScorer",
"MessageScore",
"EmbeddingProvider",
# ML-based compression (optional)
"_LLMLINGUA_AVAILABLE",
# HTML extraction (optional)
"_HTML_EXTRACTOR_AVAILABLE",
]
# Conditionally add LLMLingua exports
if _LLMLINGUA_AVAILABLE:
__all__.extend(
[
"LLMLinguaCompressor",
"LLMLinguaConfig",
"LLMLinguaResult",
"compress_with_llmlingua",
"is_llmlingua_model_loaded",
"unload_llmlingua_model",
]
)
# Conditionally add HTML extractor exports
if _HTML_EXTRACTOR_AVAILABLE:
__all__.extend(

View file

@ -1,8 +1,8 @@
"""Code-aware compressor using AST parsing for syntax-preserving compression.
This module provides AST-based compression for source code that guarantees
valid syntax output. Unlike token-level compression (LLMLingua), this
preserves structural elements while compressing function bodies.
valid syntax output. Unlike token-level compression, this preserves
structural elements while compressing function bodies.
Key Features:
- Syntax validity guaranteed (output always parses)
@ -329,7 +329,7 @@ class CodeCompressorConfig:
compress_comments: Remove non-docstring comments.
min_tokens_for_compression: Minimum tokens to trigger compression.
language_hint: Explicit language (None = auto-detect).
fallback_to_llmlingua: Use LLMLingua for unknown languages.
fallback_to_kompress: Use Kompress for unknown languages.
enable_ccr: Store originals for retrieval.
ccr_ttl: TTL for CCR entries in seconds.
"""
@ -351,7 +351,7 @@ class CodeCompressorConfig:
# Language handling
language_hint: str | None = None
fallback_to_llmlingua: bool = True
fallback_to_kompress: bool = True
# Semantic analysis (symbol importance scoring)
semantic_analysis: bool = True
@ -615,7 +615,7 @@ class CodeAwareCompressor(Transform):
- Syntax validity guaranteed
- Preserves imports, signatures, types
- Better compression ratios for code (5-8x vs 3-5x)
- Lower latency (~20-50ms vs 50-200ms for LLMLingua)
- Lower latency (~20-50ms vs 50-200ms for token-level compressors)
- Smaller memory footprint (~50MB vs ~1GB)
- Thread-safe (no mutable instance state during compression)
@ -947,9 +947,9 @@ class CodeAwareCompressor(Transform):
else:
detected_lang, confidence = detect_language(code)
# If language unknown and fallback enabled, try LLMLingua
# If language unknown and fallback enabled, try Kompress
if detected_lang == CodeLanguage.UNKNOWN:
if self.config.fallback_to_llmlingua:
if self.config.fallback_to_kompress:
return self._fallback_compress(code, original_tokens)
else:
return CodeCompressionResult(
@ -966,7 +966,7 @@ class CodeAwareCompressor(Transform):
# Check if tree-sitter is available
if not _check_tree_sitter_available():
logger.warning("tree-sitter not available. Install with: pip install headroom-ai[code]")
if self.config.fallback_to_llmlingua:
if self.config.fallback_to_kompress:
return self._fallback_compress(code, original_tokens)
return CodeCompressionResult(
compressed=code,
@ -1062,7 +1062,7 @@ class CodeAwareCompressor(Transform):
except Exception as e:
logger.warning("AST compression failed: %s, falling back", e)
if self.config.fallback_to_llmlingua:
if self.config.fallback_to_kompress:
return self._fallback_compress(code, original_tokens)
return CodeCompressionResult(
compressed=code,
@ -1629,13 +1629,13 @@ class CodeAwareCompressor(Transform):
return False
def _fallback_compress(self, code: str, original_tokens: int) -> CodeCompressionResult:
"""Fall back to LLMLingua compression."""
"""Fall back to Kompress compression."""
try:
from .llmlingua_compressor import LLMLinguaCompressor, _check_llmlingua_available
from .kompress_compressor import KompressCompressor, is_kompress_available
if _check_llmlingua_available():
compressor = LLMLinguaCompressor()
result = compressor.compress(code, content_type="code")
if is_kompress_available():
compressor = KompressCompressor()
result = compressor.compress(code)
return CodeCompressionResult(
compressed=result.compressed,
original=code,
@ -1644,7 +1644,7 @@ class CodeAwareCompressor(Transform):
compression_ratio=result.compression_ratio,
language=CodeLanguage.UNKNOWN,
language_confidence=0.0,
# LLMLingua does NOT guarantee syntax validity
# Kompress does NOT guarantee syntax validity
syntax_valid=False,
)
except ImportError:

View file

@ -224,7 +224,7 @@ def _try_detect_html(content: str) -> DetectionResult | None:
"""Try to detect HTML content.
HTML needs content extraction (removing scripts, styles, nav, etc.),
not token-level compression like LLMLingua.
not token-level compression like Kompress.
"""
# Check first 3000 chars for HTML indicators
sample = content[:3000]

View file

@ -9,7 +9,7 @@ Supported Compressors:
- SmartCrusher: JSON arrays
- SearchCompressor: grep/ripgrep results
- LogCompressor: Build/test output
- LLMLinguaCompressor: Plain text (ML-based)
- KompressCompressor: Plain text (ML-based)
- Kompress: Plain text (ML-based, requires [ml] extra)
Routing Strategy:
@ -251,7 +251,6 @@ class CompressionStrategy(Enum):
SEARCH = "search"
LOG = "log"
KOMPRESS = "kompress"
LLMLINGUA = "llmlingua"
TEXT = "text"
DIFF = "diff"
HTML = "html"
@ -360,12 +359,11 @@ class ContentRouterConfig:
Attributes:
enable_code_aware: Enable AST-based code compression.
enable_llmlingua: Enable ML-based text compression.
enable_smart_crusher: Enable JSON array compression.
enable_search_compressor: Enable search result compression.
enable_log_compressor: Enable build/test log compression.
enable_image_optimizer: Enable image token optimization.
prefer_code_aware_for_code: Use CodeAware over LLMLingua for code.
prefer_code_aware_for_code: Use CodeAware over Kompress for code.
mixed_content_threshold: Min distinct types to consider "mixed".
min_section_tokens: Minimum tokens for a section to compress.
fallback_strategy: Strategy when no compressor matches.
@ -376,8 +374,7 @@ class ContentRouterConfig:
# Enable/disable specific compressors
enable_code_aware: bool = True
enable_kompress: bool = True # Kompress: ModernBERT token compressor (preferred over LLMLingua)
enable_llmlingua: bool = True
enable_kompress: bool = True # Kompress: ModernBERT token compressor
enable_smart_crusher: bool = True
enable_search_compressor: bool = True
enable_log_compressor: bool = True
@ -647,7 +644,6 @@ class ContentRouter(Transform):
self._diff_compressor: Any = None
self._html_extractor: Any = None
self._kompress: Any = None
self._llmlingua: Any = None
self._image_optimizer: Any = None
# TOIN integration for cross-strategy learning
@ -813,7 +809,7 @@ class ContentRouter(Transform):
strategy == CompressionStrategy.CODE_AWARE
and not self.config.prefer_code_aware_for_code
):
strategy = CompressionStrategy.LLMLINGUA
strategy = CompressionStrategy.KOMPRESS
return strategy
@ -960,9 +956,11 @@ class ContentRouter(Transform):
result = compressor.compress(content, language=language, context=context)
compressed, compressed_tokens = result.compressed, result.compressed_tokens
if compressed is None:
# Fallback to LLMLingua
compressed, compressed_tokens = self._try_llmlingua(content, context, question)
strategy = CompressionStrategy.LLMLINGUA # Update for TOIN
# Fallback to Kompress
compressed, compressed_tokens = self._try_ml_compressor(
content, context, question
)
strategy = CompressionStrategy.KOMPRESS # Update for TOIN
elif strategy == CompressionStrategy.SMART_CRUSHER:
# SmartCrusher handles its own TOIN recording
@ -1013,12 +1011,9 @@ class ContentRouter(Transform):
elif strategy == CompressionStrategy.KOMPRESS:
compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
elif strategy == CompressionStrategy.LLMLINGUA:
compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
elif strategy == CompressionStrategy.TEXT:
# Prefer ML compressor (Kompress > LLMLingua) for text
# Passes through unchanged if neither Kompress nor LLMLingua available
# Prefer Kompress ML compressor for text
# Passes through unchanged if Kompress not available
compressed, compressed_tokens = self._try_ml_compressor(content, context, question)
except Exception as e:
@ -1043,7 +1038,7 @@ class ContentRouter(Transform):
def _try_ml_compressor(
self, content: str, context: str, question: str | None = None
) -> tuple[str, int]:
"""ML-based compression: Kompress (primary), LLMLingua (fallback only).
"""ML-based compression using Kompress.
Kompress (ModernBERT, trained on 330K structured tool outputs)
auto-downloads from HuggingFace on first use. No heuristic fallback.
@ -1090,19 +1085,6 @@ class ContentRouter(Transform):
except Exception as e:
logger.warning("Kompress failed: %s", e)
# Fallback: LLMLingua (only if Kompress not installed)
if compressed is None and self.config.enable_llmlingua:
compressor = self._get_llmlingua()
if compressor:
try:
result = compressor.compress(
text_to_compress, context=context, question=question
)
compressed = result.compressed
compressed_tokens = result.compressed_tokens
except Exception as e:
logger.warning("LLMLingua failed: %s", e)
if compressed is None:
return content, len(content.split())
@ -1113,9 +1095,6 @@ class ContentRouter(Transform):
return compressed, compressed_tokens or len(compressed.split())
# Backwards compatibility
_try_llmlingua = _try_ml_compressor
def _strategy_from_detection_type(self, content_type: ContentType) -> CompressionStrategy:
"""Get strategy from ContentType enum."""
mapping = {
@ -1140,7 +1119,6 @@ class ContentRouter(Transform):
CompressionStrategy.HTML: ContentType.HTML,
CompressionStrategy.TEXT: ContentType.PLAIN_TEXT,
CompressionStrategy.KOMPRESS: ContentType.PLAIN_TEXT,
CompressionStrategy.LLMLINGUA: ContentType.PLAIN_TEXT,
CompressionStrategy.PASSTHROUGH: ContentType.PLAIN_TEXT,
}
return mapping.get(strategy, ContentType.PLAIN_TEXT)
@ -1233,7 +1211,7 @@ class ContentRouter(Transform):
"""
status: dict[str, str] = {}
# 1. ML text compressor: Kompress or LLMLingua fallback
# 1. ML text compressor: Kompress
if self.config.enable_kompress:
compressor = self._get_kompress()
if compressor:
@ -1241,20 +1219,6 @@ class ContentRouter(Transform):
status["kompress"] = "enabled"
else:
status["kompress"] = "unavailable"
if "kompress" not in status or status["kompress"] != "enabled":
if self.config.enable_llmlingua:
compressor = self._get_llmlingua()
if compressor:
try:
from .llmlingua_compressor import _get_llmlingua_compressor
device = compressor._resolve_device()
_get_llmlingua_compressor(compressor.config.model_name, device)
logger.info("LLMLingua model pre-loaded at startup")
status["llmlingua"] = "enabled"
except Exception as e:
logger.warning("Failed to pre-load LLMLingua model: %s", e)
status["llmlingua"] = f"failed: {e}"
# 2. Magika content detector (avoids 100-200ms on first content detection)
try:
@ -1326,21 +1290,6 @@ class ContentRouter(Transform):
logger.debug("Kompress dependencies not available")
return self._kompress
def _get_llmlingua(self) -> Any:
"""Get LLMLinguaCompressor (lazy load). Fallback if Kompress unavailable."""
if self._llmlingua is None:
try:
from .llmlingua_compressor import (
LLMLinguaCompressor,
_check_llmlingua_available,
)
if _check_llmlingua_available():
self._llmlingua = LLMLinguaCompressor()
except ImportError:
logger.debug("LLMLinguaCompressor not available")
return self._llmlingua
def _get_image_optimizer(self) -> Any:
"""Get ImageCompressor (lazy load).
@ -1558,7 +1507,7 @@ class ContentRouter(Transform):
"non_string": 0,
"content_blocks": 0,
}
compressed_details: list[str] = [] # e.g. ["code_aware:0.72", "llmlingua:0.65"]
compressed_details: list[str] = [] # e.g. ["code_aware:0.72", "kompress:0.65"]
# Check for analysis intent in the most recent user message
analysis_intent = False

View file

@ -501,7 +501,6 @@ class IntelligentContextManager(Transform):
# Configure for aggressive compression in COMPRESS_FIRST context
router_config = ContentRouterConfig(
enable_code_aware=True,
enable_llmlingua=True,
enable_smart_crusher=True,
enable_search_compressor=True,
enable_log_compressor=True,

View file

@ -1,7 +1,7 @@
"""Kompress: ModernBERT token compressor for structured tool outputs.
Drop-in replacement for LLMLingua-2. Auto-downloads the model from
HuggingFace (chopratejas/kompress-base) on first use.
Auto-downloads the model from HuggingFace (chopratejas/kompress-base)
on first use.
Requires the [ml] extra: pip install headroom-ai[ml]
@ -222,7 +222,6 @@ class KompressCompressor(Transform):
"""Kompress: ModernBERT token compressor for structured tool outputs.
Auto-downloads chopratejas/kompress-base from HuggingFace on first use.
Drop-in replacement for LLMLinguaCompressor with identical interface.
"""
name: str = "kompress_compressor"
@ -242,9 +241,9 @@ class KompressCompressor(Transform):
Args:
content: Text to compress.
context: Optional surrounding context (unused by model, kept for interface compat).
context: Optional surrounding context (unused by model).
content_type: Ignored model decides importance per content type.
question: Ignored kept for LLMLingua interface compat.
question: Ignored reserved for future QA-aware compression.
target_ratio: If None (default), model decides how much to keep using
score threshold. If set (e.g. 0.3), forces that keep ratio.
The proxy never sets this only user-facing API does.

View file

@ -1,652 +0,0 @@
"""LLMLingua-2 compressor for ML-based prompt compression.
This module provides integration with LLMLingua-2, a BERT-based token classifier
trained via GPT-4 distillation. It achieves superior compression (up to 20x)
while maintaining high fidelity on tool outputs and structured content.
Key Features:
- Token-level classification (keep/remove) using fine-tuned BERT
- 3-6x faster than LLMLingua-1 with better results
- Especially effective on tool outputs, code, and structured data
- Reversible compression via CCR integration
Reference:
LLMLingua-2: Data Distillation for Efficient and Faithful Task-Agnostic Prompt Compression
https://arxiv.org/abs/2403.12968
Installation:
pip install headroom-ai[llmlingua]
Usage:
>>> from headroom.transforms import LLMLinguaCompressor
>>> compressor = LLMLinguaCompressor()
>>> result = compressor.compress(long_tool_output)
>>> print(result.compressed) # Significantly reduced output
"""
from __future__ import annotations
import logging
import threading
from dataclasses import dataclass, field
from typing import Any
from ..config import TransformResult
from ..models.config import ML_MODEL_DEFAULTS
from ..tokenizer import Tokenizer
from .base import Transform
logger = logging.getLogger(__name__)
# Lazy import for optional dependency
_llmlingua_available: bool | None = None
_llmlingua_instance: Any = None
_llmlingua_lock = threading.Lock() # Thread safety for model access
def _check_llmlingua_available() -> bool:
"""Check if llmlingua package is available."""
global _llmlingua_available
if _llmlingua_available is None:
try:
import llmlingua # noqa: F401
_llmlingua_available = True
except ImportError:
_llmlingua_available = False
return _llmlingua_available
def _get_llmlingua_compressor(model_name: str, device: str) -> Any:
"""Get or create the LLMLingua compressor instance.
Uses lazy initialization and caches the instance to avoid repeated model loading.
Thread-safe: uses lock to prevent race conditions during model initialization.
Args:
model_name: HuggingFace model name for the compressor.
device: Device to run the model on ('cuda', 'cpu', or 'auto').
Returns:
PromptCompressor instance from llmlingua.
Raises:
ImportError: If llmlingua is not installed.
RuntimeError: If model loading fails.
"""
global _llmlingua_instance
if not _check_llmlingua_available():
raise ImportError(
"llmlingua is not installed. Install with: pip install headroom-ai[llmlingua]\n"
"Note: This requires ~2GB of disk space and ~1GB RAM for the model."
)
with _llmlingua_lock:
# Double-check after acquiring lock
if _llmlingua_instance is None or _llmlingua_instance._model_name != model_name:
try:
from llmlingua import PromptCompressor
logger.info(
"Loading LLMLingua-2 model: %s on device: %s "
"(this may take 10-30s on first run)",
model_name,
device,
)
_llmlingua_instance = PromptCompressor(
model_name=model_name,
device_map=device,
use_llmlingua2=True, # Use LLMLingua-2 (BERT classifier)
)
# Store model name for later comparison
_llmlingua_instance._model_name = model_name
logger.info("LLMLingua-2 model loaded successfully")
except Exception as e:
error_msg = str(e).lower()
if "out of memory" in error_msg or "oom" in error_msg:
raise RuntimeError(
f"Out of memory loading LLMLingua model. Try:\n"
f" 1. Use device='cpu' instead of 'cuda'\n"
f" 2. Close other GPU applications\n"
f" 3. Use a smaller model\n"
f"Original error: {e}"
) from e
elif "not found" in error_msg or "404" in error_msg:
raise RuntimeError(
f"Model '{model_name}' not found on HuggingFace. Try:\n"
f" 1. Check the model name is correct\n"
f" 2. Use default: 'microsoft/llmlingua-2-xlm-roberta-large-meetingbank'\n"
f"Original error: {e}"
) from e
else:
raise RuntimeError(
f"Failed to load LLMLingua model: {e}\n"
f"Ensure you have sufficient disk space and memory."
) from e
return _llmlingua_instance
def unload_llmlingua_model() -> bool:
"""Unload the LLMLingua model to free memory.
Use this when you're done with compression and want to reclaim GPU/CPU memory.
The model will be reloaded automatically on the next compression call.
Returns:
True if a model was unloaded, False if no model was loaded.
Example:
>>> from headroom.transforms import LLMLinguaCompressor, unload_llmlingua_model
>>> compressor = LLMLinguaCompressor()
>>> result = compressor.compress(content) # Model loaded here
>>> # ... do other work ...
>>> unload_llmlingua_model() # Free ~1GB of memory
"""
global _llmlingua_instance
with _llmlingua_lock:
if _llmlingua_instance is not None:
model_name = getattr(_llmlingua_instance, "_model_name", "unknown")
logger.info("Unloading LLMLingua model: %s", model_name)
# Clear the instance
_llmlingua_instance = None
# Attempt to free GPU memory if torch is available
try:
import torch
if torch.cuda.is_available():
torch.cuda.empty_cache()
logger.debug("Cleared CUDA cache")
except ImportError:
pass
return True
return False
def is_llmlingua_model_loaded() -> bool:
"""Check if an LLMLingua model is currently loaded.
Returns:
True if a model is loaded in memory, False otherwise.
"""
return _llmlingua_instance is not None
@dataclass
class LLMLinguaConfig:
"""Configuration for LLMLingua-2 compression.
Attributes:
model_name: HuggingFace model for the compressor. Default is the
LLMLingua-2 xlm-roberta-large model fine-tuned for compression.
device: Device to run on ('cuda', 'cpu', 'auto'). Auto will use CUDA if available.
target_compression_rate: Target compression ratio (e.g., 0.3 = keep 30% of tokens).
force_tokens: Tokens to always preserve (e.g., important keywords).
drop_consecutive: Whether to drop consecutive punctuation/whitespace.
min_tokens_for_compression: Minimum token count to trigger compression.
Content below this threshold is passed through unchanged.
enable_ccr: Whether to store originals in CCR for retrieval.
ccr_ttl: TTL for CCR entries in seconds.
GOTCHA: Lower target_compression_rate = more aggressive compression.
A rate of 0.2 means keeping only 20% of tokens.
"""
# Model configuration
model_name: str = field(default_factory=lambda: ML_MODEL_DEFAULTS.llmlingua)
device: str = "auto"
# Compression parameters
target_compression_rate: float = 0.3
force_tokens: list[str] = field(default_factory=list)
drop_consecutive: bool = True
# Thresholds
min_tokens_for_compression: int = 100
# CCR integration
enable_ccr: bool = True
ccr_ttl: int = 300 # 5 minutes
# Content type specific settings
code_compression_rate: float = 0.5 # Conservative for code
json_compression_rate: float = 0.4 # Somewhat conservative for JSON
text_compression_rate: float = 0.5 # Balanced for plain text (higher = more accurate)
@dataclass
class LLMLinguaResult:
"""Result of LLMLingua-2 compression.
Attributes:
compressed: Compressed content.
original: Original content before compression.
original_tokens: Token count of original content.
compressed_tokens: Token count after compression.
compression_ratio: Actual compression ratio achieved.
cache_key: CCR cache key if stored.
model_used: Model that performed the compression.
tokens_saved: Number of tokens saved.
"""
compressed: str
original: str
original_tokens: int
compressed_tokens: int
compression_ratio: float
cache_key: str | None = None
model_used: str | None = None
@property
def tokens_saved(self) -> int:
"""Number of tokens saved by compression."""
return max(0, self.original_tokens - self.compressed_tokens)
@property
def savings_percentage(self) -> float:
"""Percentage of tokens saved."""
if self.original_tokens == 0:
return 0.0
return (self.tokens_saved / self.original_tokens) * 100
class LLMLinguaCompressor(Transform):
"""LLMLingua-2 based prompt compressor.
Uses a BERT-based token classifier trained via GPT-4 distillation to
identify and remove non-essential tokens while preserving semantic meaning.
Key advantages over statistical compression:
- Learned token importance from LLM feedback
- Better handling of context-dependent importance
- More aggressive compression with less information loss
- Especially effective on structured outputs (JSON, code, logs)
Example:
>>> compressor = LLMLinguaCompressor()
>>> result = compressor.compress(long_tool_output)
>>> print(f"Saved {result.tokens_saved} tokens ({result.savings_percentage:.1f}%)")
>>> # Use as a Transform in pipeline
>>> from headroom.transforms import TransformPipeline
>>> pipeline = TransformPipeline([LLMLinguaCompressor()])
>>> result = pipeline.apply(messages, tokenizer)
"""
name: str = "llmlingua_compressor"
def __init__(self, config: LLMLinguaConfig | None = None):
"""Initialize LLMLingua compressor.
Args:
config: Compression configuration. If None, uses defaults.
Note:
The underlying model is loaded lazily on first use to avoid
startup overhead when the compressor isn't used.
"""
self.config = config or LLMLinguaConfig()
self._compressor: Any = None # Lazy loaded
def compress(
self,
content: str,
context: str = "",
content_type: str | None = None,
question: str | None = None,
) -> LLMLinguaResult:
"""Compress content using LLMLingua-2.
Args:
content: Content to compress.
context: Optional context for relevance-aware compression.
content_type: Type of content ('code', 'json', 'text').
If None, auto-detected.
question: Optional question for QA-aware compression. When provided,
LLMLingua preserves tokens relevant to answering this question.
This significantly improves accuracy for QA tasks.
Returns:
LLMLinguaResult with compressed content and metadata.
Raises:
ImportError: If llmlingua is not installed.
"""
# Check availability
if not _check_llmlingua_available():
logger.warning(
"LLMLingua not available. Install with: pip install headroom-ai[llmlingua]"
)
return LLMLinguaResult(
compressed=content,
original=content,
original_tokens=len(content.split()), # Rough estimate
compressed_tokens=len(content.split()),
compression_ratio=1.0,
)
# Estimate token count (rough)
estimated_tokens = len(content.split())
# Skip compression for small content
if estimated_tokens < self.config.min_tokens_for_compression:
return LLMLinguaResult(
compressed=content,
original=content,
original_tokens=estimated_tokens,
compressed_tokens=estimated_tokens,
compression_ratio=1.0,
)
# Get compression rate based on content type
compression_rate = self._get_compression_rate(content, content_type)
# Get or initialize compressor
device = self._resolve_device()
compressor = _get_llmlingua_compressor(self.config.model_name, device)
# Prepare force tokens
force_tokens = list(self.config.force_tokens)
# Add context words as force tokens if provided
if context:
context_words = [w for w in context.split() if len(w) > 3]
force_tokens.extend(context_words[:10]) # Limit to avoid overhead
# Perform compression
try:
# Build compress_prompt kwargs
compress_kwargs: dict[str, Any] = {
"context": [content], # LLMLingua expects a list of context strings
"rate": compression_rate,
"force_tokens": force_tokens if force_tokens else [],
"drop_consecutive": self.config.drop_consecutive,
}
# Add question for QA-aware token selection (LLMLingua-2 feature)
# This enables relevance-aware compression where tokens relevant
# to answering the question are preserved with higher probability
if question:
compress_kwargs["question"] = question
result = compressor.compress_prompt(**compress_kwargs)
compressed = result.get("compressed_prompt", content)
original_tokens = result.get("origin_tokens", estimated_tokens)
compressed_tokens = result.get("compressed_tokens", len(compressed.split()))
except Exception as e:
logger.warning("LLMLingua compression failed: %s", e)
return LLMLinguaResult(
compressed=content,
original=content,
original_tokens=estimated_tokens,
compressed_tokens=estimated_tokens,
compression_ratio=1.0,
)
# Calculate actual ratio
ratio = compressed_tokens / max(original_tokens, 1)
# Store in CCR if enabled
cache_key = None
if self.config.enable_ccr and ratio < 0.8:
cache_key = self._store_in_ccr(content, compressed, original_tokens)
if cache_key:
# Use standard CCR marker format for CCRToolInjector detection
compressed += f"\n[{original_tokens} items compressed to {compressed_tokens}. Retrieve more: hash={cache_key}]"
return LLMLinguaResult(
compressed=compressed,
original=content,
original_tokens=original_tokens,
compressed_tokens=compressed_tokens,
compression_ratio=ratio,
cache_key=cache_key,
model_used=self.config.model_name,
)
def apply(
self,
messages: list[dict[str, Any]],
tokenizer: Tokenizer,
**kwargs: Any,
) -> TransformResult:
"""Apply LLMLingua compression to messages.
This method implements the Transform interface for use in pipelines.
It compresses tool outputs and long assistant/user messages.
Args:
messages: List of message dicts to transform.
tokenizer: Tokenizer for accurate token counting.
**kwargs: Additional arguments (e.g., 'context' for relevance).
Returns:
TransformResult with compressed messages and metadata.
"""
tokens_before = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages)
context = kwargs.get("context", "")
transformed_messages = []
transforms_applied = []
warnings: list[str] = []
for message in messages:
role = message.get("role", "")
content = message.get("content", "")
# Skip non-string content (multimodal messages with images)
if not isinstance(content, str):
transformed_messages.append(message)
continue
# Compress tool results (highest value compression)
if role == "tool" and content:
result = self.compress(content, context=context, content_type="json")
if result.compression_ratio < 0.9:
transformed_messages.append({**message, "content": result.compressed})
transforms_applied.append(f"llmlingua:tool:{result.compression_ratio:.2f}")
else:
transformed_messages.append(message)
# Compress long assistant messages (tool outputs often embedded)
elif role == "assistant" and len(content) > 500:
result = self.compress(content, context=context)
if result.compression_ratio < 0.9:
transformed_messages.append({**message, "content": result.compressed})
transforms_applied.append(f"llmlingua:assistant:{result.compression_ratio:.2f}")
else:
transformed_messages.append(message)
# Pass through other messages
else:
transformed_messages.append(message)
tokens_after = sum(
tokenizer.count_text(str(m.get("content", ""))) for m in transformed_messages
)
# Add warning if llmlingua not available
if not _check_llmlingua_available():
warnings.append(
"LLMLingua not installed. Install with: pip install headroom-ai[llmlingua]"
)
return TransformResult(
messages=transformed_messages,
tokens_before=tokens_before,
tokens_after=tokens_after,
transforms_applied=transforms_applied if transforms_applied else ["llmlingua:noop"],
warnings=warnings,
)
def should_apply(
self,
messages: list[dict[str, Any]],
tokenizer: Tokenizer,
**kwargs: Any,
) -> bool:
"""Check if LLMLingua compression should be applied.
Returns True if:
- LLMLingua is available, AND
- Total token count exceeds minimum threshold
Args:
messages: Messages to check.
tokenizer: Tokenizer for counting.
**kwargs: Additional arguments.
Returns:
True if compression should be applied.
"""
if not _check_llmlingua_available():
return False
total_tokens = sum(tokenizer.count_text(str(m.get("content", ""))) for m in messages)
return total_tokens >= self.config.min_tokens_for_compression
def _get_compression_rate(
self,
content: str,
content_type: str | None,
) -> float:
"""Get appropriate compression rate based on content type.
Args:
content: Content to analyze.
content_type: Explicit content type or None for auto-detection.
Returns:
Target compression rate for this content.
"""
if content_type == "code":
return self.config.code_compression_rate
elif content_type == "json":
return self.config.json_compression_rate
elif content_type == "text":
return self.config.text_compression_rate
# Auto-detect content type
if self._looks_like_json(content):
return self.config.json_compression_rate
elif self._looks_like_code(content):
return self.config.code_compression_rate
else:
return self.config.text_compression_rate
def _looks_like_json(self, content: str) -> bool:
"""Check if content appears to be JSON."""
stripped = content.strip()
return (stripped.startswith("{") and stripped.endswith("}")) or (
stripped.startswith("[") and stripped.endswith("]")
)
def _looks_like_code(self, content: str) -> bool:
"""Check if content appears to be code."""
code_indicators = [
"def ",
"class ",
"function ",
"import ",
"from ",
"const ",
"let ",
"var ",
"public ",
"private ",
"async ",
"await ",
"return ",
"if (",
"for (",
"while (",
]
return any(indicator in content for indicator in code_indicators)
def _resolve_device(self) -> str:
"""Resolve 'auto' device to actual device."""
if self.config.device != "auto":
return self.config.device
try:
import torch
if torch.cuda.is_available():
return "cuda"
elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
return "mps"
except ImportError:
pass
return "cpu"
def _store_in_ccr(
self,
original: str,
compressed: str,
original_tokens: int,
) -> str | None:
"""Store original content in CCR for later retrieval.
Args:
original: Original content before compression.
compressed: Compressed content.
original_tokens: Token count of original.
Returns:
Cache key if stored successfully, None otherwise.
"""
try:
from ..cache.compression_store import get_compression_store
store = get_compression_store()
return store.store(
original,
compressed,
original_tokens=original_tokens,
compressed_tokens=len(compressed.split()),
compression_strategy="llmlingua2",
)
except ImportError:
return None
except Exception as e:
logger.debug("CCR storage failed: %s", e)
return None
def compress_with_llmlingua(
content: str,
compression_rate: float = 0.3,
context: str = "",
model_name: str | None = None,
) -> str:
"""Convenience function for one-off compression.
Args:
content: Content to compress.
compression_rate: Target compression rate (0.0-1.0).
context: Optional context for relevance-aware compression.
model_name: Optional model name override.
Returns:
Compressed content string.
Example:
>>> compressed = compress_with_llmlingua(long_output, compression_rate=0.2)
"""
config = LLMLinguaConfig(target_compression_rate=compression_rate)
if model_name:
config.model_name = model_name
compressor = LLMLinguaCompressor(config)
result = compressor.compress(content, context=context)
return result.compressed

View file

@ -40,7 +40,7 @@ class TransformPipeline:
Transform order:
1. Cache Aligner - normalize prefix for cache hits
2. Content Router - intelligent content-aware compression (routes to appropriate
compressor: LLMLingua for text, SmartCrusher for JSON, CodeCompressor for code, etc.)
compressor: Kompress for text, SmartCrusher for JSON, CodeCompressor for code, etc.)
3. SmartCrusher/ToolCrusher - fallback if ContentRouter disabled
4. IntelligentContextManager/RollingWindow - enforce token limits
"""

View file

@ -1,7 +1,7 @@
"""Protect workflow/custom XML tags from text compression.
LLM workflows use XML-style tags (<system-reminder>, <tool_call>, <thinking>)
as structural markers. Text compressors (Kompress, LLMLingua) treat these as
as structural markers. Text compressors (Kompress) treat these as
droppable noise and silently remove them, breaking downstream tools.
This module detects custom tags (anything NOT standard HTML), replaces entire

View file

@ -413,7 +413,7 @@ class TestAlternativeMarkerFormats:
- TextCompressor: [N lines compressed to M. Retrieve more: hash=xxx]
- LogCompressor: [N lines compressed to M. Retrieve more: hash=xxx]
- SearchCompressor: [N matches compressed to M. Retrieve more: hash=xxx]
- LLMLingua: [N items compressed to M. Retrieve more: hash=xxx]
- Kompress: [N items compressed to M. Retrieve more: hash=xxx]
The CCRToolInjector should detect all these formats.
"""

View file

@ -755,10 +755,10 @@ class TestJSONAPIResponseEval:
@pytest.fixture
def compressor(self):
"""Create compressor with simple compression (no LLMLingua for tests)."""
"""Create compressor with simple compression (no Kompress for tests)."""
config = UniversalCompressorConfig(
use_magika=False, # Use fallback for consistent tests
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
)
return UniversalCompressor(config=config)
@ -885,7 +885,7 @@ class TestCodeFileEval:
"""Create compressor."""
config = UniversalCompressorConfig(
use_magika=False,
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
)
return UniversalCompressor(config=config)
@ -969,7 +969,7 @@ class TestLogOutputEval:
"""Create compressor."""
config = UniversalCompressorConfig(
use_magika=False,
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
)
return UniversalCompressor(config=config)
@ -1031,7 +1031,7 @@ class TestMultiToolAgentScenario:
"""Create compressor."""
config = UniversalCompressorConfig(
use_magika=False,
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
)
return UniversalCompressor(config=config)
@ -1129,7 +1129,7 @@ class TestCompressionQualityMetrics:
"""Create compressor."""
config = UniversalCompressorConfig(
use_magika=False,
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
compression_ratio_target=0.3, # Target 70% reduction
)

View file

@ -301,7 +301,7 @@ class TestJSONDiscoverability:
"""Create compressor."""
config = UniversalCompressorConfig(
use_magika=False,
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
)
return UniversalCompressor(config=config)
@ -418,7 +418,7 @@ class TestCodeUnderstanding:
"""Create compressor."""
config = UniversalCompressorConfig(
use_magika=False,
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
)
return UniversalCompressor(config=config)
@ -528,7 +528,7 @@ class TestMultiContentAgent:
"""Create compressor."""
config = UniversalCompressorConfig(
use_magika=False,
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
)
return UniversalCompressor(config=config)
@ -591,7 +591,7 @@ class TestCompressionEfficacy:
"""Create compressor."""
config = UniversalCompressorConfig(
use_magika=False,
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
)
return UniversalCompressor(config=config)

View file

@ -22,7 +22,7 @@ class TestUniversalCompressorConfig:
config = UniversalCompressorConfig()
assert config.use_magika is True
assert config.use_llmlingua is True
assert config.use_kompress is True
assert config.use_entropy_preservation is True
assert config.entropy_threshold == 0.85
assert config.min_content_length == 100
@ -99,7 +99,7 @@ class TestUniversalCompressor:
"""Create compressor with fallback detector (no Magika required)."""
config = UniversalCompressorConfig(
use_magika=False, # Use fallback detector
use_llmlingua=False, # Use simple compression
use_kompress=False, # Use simple compression
ccr_enabled=False, # Skip CCR
)
return UniversalCompressor(config=config)
@ -215,7 +215,7 @@ class TestUniversalCompressorBatch:
"""Create compressor with fallback detector."""
config = UniversalCompressorConfig(
use_magika=False,
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
)
return UniversalCompressor(config=config)
@ -262,7 +262,7 @@ class TestStructurePreservation:
"""Create compressor."""
config = UniversalCompressorConfig(
use_magika=False,
use_llmlingua=False,
use_kompress=False,
ccr_enabled=False,
)
return UniversalCompressor(config=config)

View file

@ -262,7 +262,7 @@ class TestHTMLExtractionWithLLM:
@pytest.mark.skipif(not os.environ.get("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set")
class TestHTMLvsBaseline:
"""Tests comparing HTMLExtractor vs LLMLingua baseline."""
"""Tests comparing HTMLExtractor vs Kompress baseline."""
@pytest.fixture
def evaluator_with_baseline(self):
@ -274,9 +274,9 @@ class TestHTMLvsBaseline:
provider="openai",
)
@pytest.mark.skipif(True, reason="LLMLingua requires GPU, skip in CI")
@pytest.mark.skipif(True, reason="Kompress requires GPU, skip in CI")
def test_extraction_beats_baseline(self, evaluator_with_baseline):
"""Test that HTMLExtractor outperforms LLMLingua on HTML."""
"""Test that HTMLExtractor outperforms Kompress on HTML."""
cases = get_sample_eval_cases()[:2] # Just test 2 for speed
results = evaluator_with_baseline.evaluate(cases)
@ -286,9 +286,9 @@ class TestHTMLvsBaseline:
print(f"Avg extraction score: {results.avg_extraction_score}/5")
print(f"Avg baseline score: {results.avg_baseline_score}/5")
# HTMLExtractor should beat LLMLingua on HTML content
# HTMLExtractor should beat Kompress on HTML content
assert results.avg_extraction_score >= results.avg_baseline_score, (
"HTMLExtractor should perform at least as well as LLMLingua on HTML"
"HTMLExtractor should perform at least as well as Kompress on HTML"
)

View file

@ -1,468 +0,0 @@
"""Tests for LLMLingua opt-in mechanism in the proxy server.
These tests verify:
- ProxyConfig llmlingua settings
- LLMLingua transform integration in pipeline
- Status detection and logging hints
- CLI flag parsing
- DevEx: helpful messages when llmlingua unavailable
"""
from unittest.mock import MagicMock, patch
import pytest
# Skip if fastapi not available
pytest.importorskip("fastapi")
from fastapi.testclient import TestClient
from headroom.proxy.server import (
HeadroomProxy,
ProxyConfig,
_get_llmlingua_banner_status,
create_app,
)
from headroom.transforms import _LLMLINGUA_AVAILABLE
# =============================================================================
# Test Fixtures
# =============================================================================
@pytest.fixture
def base_config():
"""Base config with optimization disabled for simpler tests."""
return ProxyConfig(
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
llmlingua_enabled=False, # Explicitly disable for base config
smart_routing=False, # Use legacy mode for these tests
)
@pytest.fixture
def llmlingua_config():
"""Config with LLMLingua enabled (legacy mode for explicit status testing)."""
return ProxyConfig(
optimize=True,
cache_enabled=False,
rate_limit_enabled=False,
cost_tracking_enabled=False,
llmlingua_enabled=True,
llmlingua_device="cpu",
llmlingua_target_rate=0.4,
smart_routing=False, # Use legacy mode for explicit status testing
)
@pytest.fixture
def client(base_config):
"""Create test client with base config."""
app = create_app(base_config)
with TestClient(app) as client:
yield client
# =============================================================================
# TestProxyConfigLLMLingua
# =============================================================================
class TestProxyConfigLLMLingua:
"""Tests for LLMLingua settings in ProxyConfig."""
def test_default_llmlingua_enabled(self):
"""LLMLingua is enabled by default (with smart routing)."""
config = ProxyConfig()
# LLMLingua is now enabled by default with smart routing
assert config.llmlingua_enabled is True
assert config.llmlingua_device == "auto"
assert config.llmlingua_target_rate == 0.3
def test_llmlingua_can_be_enabled(self):
"""LLMLingua can be enabled via config."""
config = ProxyConfig(
llmlingua_enabled=True,
llmlingua_device="cuda",
llmlingua_target_rate=0.5,
)
assert config.llmlingua_enabled is True
assert config.llmlingua_device == "cuda"
assert config.llmlingua_target_rate == 0.5
def test_llmlingua_device_options(self):
"""LLMLingua device accepts valid options."""
for device in ["auto", "cuda", "cpu", "mps"]:
config = ProxyConfig(llmlingua_device=device)
assert config.llmlingua_device == device
def test_llmlingua_target_rate_range(self):
"""LLMLingua target rate accepts 0.0-1.0 range."""
# Low rate (aggressive compression)
config_low = ProxyConfig(llmlingua_target_rate=0.1)
assert config_low.llmlingua_target_rate == 0.1
# High rate (conservative compression)
config_high = ProxyConfig(llmlingua_target_rate=0.8)
assert config_high.llmlingua_target_rate == 0.8
# =============================================================================
# TestLLMLinguaSetup
# =============================================================================
class TestLLMLinguaSetup:
"""Tests for LLMLingua setup in HeadroomProxy."""
def test_setup_returns_disabled_when_not_enabled(self, base_config):
"""Setup returns 'disabled' when llmlingua not enabled and not available."""
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
proxy = HeadroomProxy(base_config)
assert proxy._llmlingua_status == "disabled"
def test_setup_returns_available_when_installed_but_not_enabled(self, base_config):
"""Setup returns 'available' when llmlingua installed but not enabled."""
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
proxy = HeadroomProxy(base_config)
assert proxy._llmlingua_status == "available"
def test_setup_returns_enabled_when_enabled_and_available(self, llmlingua_config):
"""Setup returns 'enabled' when llmlingua enabled and available."""
mock_compressor = MagicMock()
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
with patch("headroom.proxy.server.LLMLinguaCompressor", mock_compressor):
with patch("headroom.proxy.server.LLMLinguaConfig"):
proxy = HeadroomProxy(llmlingua_config)
assert proxy._llmlingua_status == "enabled"
def test_setup_returns_unavailable_when_enabled_but_not_installed(self):
"""Setup returns 'unavailable' when enabled but llmlingua not installed."""
config = ProxyConfig(
llmlingua_enabled=True,
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
smart_routing=False, # Use legacy mode for explicit status testing
)
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
proxy = HeadroomProxy(config)
assert proxy._llmlingua_status == "unavailable"
def test_llmlingua_compressor_added_to_pipeline(self, llmlingua_config):
"""LLMLinguaCompressor is added to pipeline when enabled."""
mock_compressor_class = MagicMock()
mock_compressor_instance = MagicMock()
mock_compressor_class.return_value = mock_compressor_instance
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
with patch("headroom.proxy.server.LLMLinguaCompressor", mock_compressor_class):
with patch("headroom.proxy.server.LLMLinguaConfig") as mock_config:
HeadroomProxy(llmlingua_config)
# Verify LLMLinguaCompressor was instantiated
mock_compressor_class.assert_called_once()
# Verify config was passed with correct device and rate
call_args = mock_config.call_args
assert call_args.kwargs["device"] == "cpu"
assert call_args.kwargs["target_compression_rate"] == 0.4
def test_llmlingua_not_added_when_disabled(self, base_config):
"""LLMLinguaCompressor is NOT added when disabled."""
mock_compressor_class = MagicMock()
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
with patch("headroom.proxy.server.LLMLinguaCompressor", mock_compressor_class):
HeadroomProxy(base_config)
# Should NOT be called when disabled
mock_compressor_class.assert_not_called()
# =============================================================================
# TestBannerStatus
# =============================================================================
class TestBannerStatus:
"""Tests for banner status helper function."""
def test_banner_disabled_when_not_available(self):
"""Banner shows DISABLED when llmlingua not available and not enabled."""
config = ProxyConfig(llmlingua_enabled=False, smart_routing=False)
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
status = _get_llmlingua_banner_status(config)
assert status == "DISABLED"
def test_banner_available_hint_when_installed(self):
"""Banner shows availability hint when installed but not enabled."""
config = ProxyConfig(llmlingua_enabled=False, smart_routing=False)
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
status = _get_llmlingua_banner_status(config)
# Now shows DISABLED with hint to enable
assert "DISABLED" in status
assert "--no-llmlingua" in status # Hint to remove the flag to enable
def test_banner_enabled_when_active(self):
"""Banner shows ENABLED with config when active."""
config = ProxyConfig(
llmlingua_enabled=True,
llmlingua_device="cuda",
llmlingua_target_rate=0.25,
)
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
status = _get_llmlingua_banner_status(config)
assert "ENABLED" in status
assert "cuda" in status
assert "0.25" in status
def test_banner_shows_install_hint_when_requested_but_missing(self):
"""Banner shows install hint when enabled but not installed."""
config = ProxyConfig(llmlingua_enabled=True, smart_routing=False)
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
status = _get_llmlingua_banner_status(config)
assert "NOT INSTALLED" in status
assert "pip install" in status
# =============================================================================
# TestHealthEndpointWithLLMLingua
# =============================================================================
class TestHealthEndpointWithLLMLingua:
"""Tests for health endpoint reflecting LLMLingua status."""
def test_health_returns_llmlingua_in_config(self, client):
"""Health endpoint works regardless of LLMLingua status."""
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "healthy"
assert "config" in data
# =============================================================================
# TestStatsEndpointWithLLMLingua
# =============================================================================
class TestStatsEndpointWithLLMLingua:
"""Tests for stats endpoint with LLMLingua integration."""
def test_stats_endpoint_works(self, client):
"""Stats endpoint works with any LLMLingua configuration."""
response = client.get("/stats")
assert response.status_code == 200
data = response.json()
assert "requests" in data
assert "tokens" in data
# =============================================================================
# TestCLIArguments
# =============================================================================
class TestCLIArguments:
"""Tests for CLI argument parsing (without actually running server)."""
def test_llmlingua_flag_defaults(self):
"""Default CLI values for LLMLingua settings."""
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--llmlingua", action="store_true")
parser.add_argument("--llmlingua-device", default="auto")
parser.add_argument("--llmlingua-rate", type=float, default=0.3)
args = parser.parse_args([])
assert args.llmlingua is False
assert args.llmlingua_device == "auto"
assert args.llmlingua_rate == 0.3
def test_llmlingua_flag_enabled(self):
"""CLI --llmlingua flag enables LLMLingua."""
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--llmlingua", action="store_true")
parser.add_argument("--llmlingua-device", default="auto")
parser.add_argument("--llmlingua-rate", type=float, default=0.3)
args = parser.parse_args(["--llmlingua"])
assert args.llmlingua is True
def test_llmlingua_device_flag(self):
"""CLI --llmlingua-device flag sets device."""
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--llmlingua-device", default="auto")
args = parser.parse_args(["--llmlingua-device", "cuda"])
assert args.llmlingua_device == "cuda"
def test_llmlingua_rate_flag(self):
"""CLI --llmlingua-rate flag sets compression rate."""
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--llmlingua-rate", type=float, default=0.3)
args = parser.parse_args(["--llmlingua-rate", "0.5"])
assert args.llmlingua_rate == 0.5
# =============================================================================
# TestDevExMessages
# =============================================================================
class TestDevExMessages:
"""Tests for developer experience messages and hints."""
def test_warning_logged_when_enabled_but_unavailable(self, caplog):
"""Warning is logged when llmlingua enabled but not installed."""
import logging
config = ProxyConfig(
llmlingua_enabled=True,
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
smart_routing=False, # Use legacy mode for explicit status testing
)
with caplog.at_level(logging.WARNING):
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", False):
proxy = HeadroomProxy(config)
# Should have logged a warning about missing llmlingua
assert proxy._llmlingua_status == "unavailable"
assert any("llmlingua" in r.message.lower() for r in caplog.records)
assert any("pip install" in r.message for r in caplog.records)
# =============================================================================
# TestIntegrationWithActualLLMLingua
# =============================================================================
@pytest.mark.skipif(not _LLMLINGUA_AVAILABLE, reason="llmlingua not installed")
class TestIntegrationWithActualLLMLingua:
"""Integration tests that require actual llmlingua installation.
These tests verify the full integration path when llmlingua is installed.
"""
def test_proxy_starts_with_llmlingua_enabled(self):
"""Proxy starts successfully with LLMLingua enabled."""
config = ProxyConfig(
llmlingua_enabled=True,
llmlingua_device="cpu", # CPU for CI/test environments
llmlingua_target_rate=0.3,
optimize=True,
cache_enabled=False,
rate_limit_enabled=False,
smart_routing=False, # Use legacy mode for explicit status testing
)
# Should not raise
proxy = HeadroomProxy(config)
assert proxy._llmlingua_status == "enabled"
def test_app_creates_with_llmlingua(self):
"""FastAPI app creates successfully with LLMLingua enabled."""
config = ProxyConfig(
llmlingua_enabled=True,
llmlingua_device="cpu",
optimize=True,
cache_enabled=False,
rate_limit_enabled=False,
)
# Should not raise
app = create_app(config)
assert app is not None
def test_health_endpoint_with_llmlingua_enabled(self):
"""Health endpoint works with LLMLingua enabled."""
config = ProxyConfig(
llmlingua_enabled=True,
llmlingua_device="cpu",
optimize=True,
cache_enabled=False,
rate_limit_enabled=False,
)
app = create_app(config)
with TestClient(app) as client:
response = client.get("/health")
assert response.status_code == 200
# =============================================================================
# TestEdgeCases
# =============================================================================
class TestEdgeCases:
"""Edge cases for LLMLingua proxy integration."""
def test_multiple_proxy_instances_independent(self):
"""Multiple proxy instances have independent LLMLingua status."""
config_enabled = ProxyConfig(
llmlingua_enabled=True,
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
smart_routing=False, # Use legacy mode for explicit status testing
)
config_disabled = ProxyConfig(
llmlingua_enabled=False,
optimize=False,
cache_enabled=False,
rate_limit_enabled=False,
smart_routing=False, # Use legacy mode for explicit status testing
)
with patch("headroom.proxy.server._LLMLINGUA_AVAILABLE", True):
with patch("headroom.proxy.server.LLMLinguaCompressor"):
with patch("headroom.proxy.server.LLMLinguaConfig"):
proxy_enabled = HeadroomProxy(config_enabled)
proxy_disabled = HeadroomProxy(config_disabled)
assert proxy_enabled._llmlingua_status == "enabled"
assert proxy_disabled._llmlingua_status == "available"
def test_config_immutable_after_proxy_creation(self, base_config):
"""Config values are captured at proxy creation time."""
proxy = HeadroomProxy(base_config)
# Modifying config after creation doesn't affect proxy
# (ProxyConfig is a dataclass, so this tests the pattern)
original_status = proxy._llmlingua_status
# Status should remain unchanged
assert proxy._llmlingua_status == original_status

View file

@ -499,14 +499,14 @@ class TestFallbackCompression:
# Should still return a result (fallback compression)
assert result is not None
# LLMLingua fallback does NOT guarantee syntax validity
# If LLMLingua is unavailable, returns original (valid)
# If LLMLingua IS available, syntax_valid=False (cannot guarantee)
# Kompress fallback does NOT guarantee syntax validity
# If Kompress is unavailable, returns original (valid)
# If Kompress IS available, syntax_valid=False (cannot guarantee)
def test_fallback_preserves_structure(self, default_config):
"""Fallback compression preserves basic structure when no compressor available.
When both tree-sitter and LLMLingua are unavailable, the fallback
When both tree-sitter and Kompress are unavailable, the fallback
returns the original code unchanged - preserving all structure.
"""
with (
@ -515,7 +515,7 @@ class TestFallbackCompression:
return_value=False,
),
patch(
"headroom.transforms.llmlingua_compressor._check_llmlingua_available",
"headroom.transforms.kompress_compressor.is_kompress_available",
return_value=False,
),
):

View file

@ -156,7 +156,6 @@ class TestContentRouterConfig:
assert config.enable_code_aware is True
assert config.enable_kompress is True
assert config.enable_llmlingua is True
assert config.enable_smart_crusher is True
assert config.enable_search_compressor is True
assert config.enable_log_compressor is True
@ -168,13 +167,11 @@ class TestContentRouterConfig:
config = ContentRouterConfig(
min_section_tokens=50,
enable_code_aware=False,
enable_llmlingua=False,
fallback_strategy=CompressionStrategy.TEXT,
)
assert config.min_section_tokens == 50
assert config.enable_code_aware is False
assert config.enable_llmlingua is False
assert config.fallback_strategy == CompressionStrategy.TEXT
def test_all_strategies_in_enum(self):
@ -184,7 +181,6 @@ class TestContentRouterConfig:
"SMART_CRUSHER",
"SEARCH",
"LOG",
"LLMLINGUA",
"TEXT",
"MIXED",
"PASSTHROUGH",

View file

@ -1,941 +0,0 @@
"""Tests for LLMLingua-2 compressor integration.
Comprehensive tests covering:
- LLMLinguaConfig: Configuration validation and defaults
- LLMLinguaCompressor: Core compression functionality
- Transform interface: apply(), should_apply() methods
- Content type detection: JSON, code, plain text
- CCR integration: Reversible compression storage
- Edge cases: Empty content, unavailable dependency, fallbacks
"""
import json
from unittest.mock import MagicMock, patch
import pytest
from headroom.transforms.llmlingua_compressor import (
LLMLinguaCompressor,
LLMLinguaConfig,
LLMLinguaResult,
compress_with_llmlingua,
is_llmlingua_model_loaded,
unload_llmlingua_model,
)
# Try to import for availability check
try:
import llmlingua # noqa: F401
LLMLINGUA_INSTALLED = True
except ImportError:
LLMLINGUA_INSTALLED = False
# =============================================================================
# Test Fixtures
# =============================================================================
@pytest.fixture
def default_config():
"""Default LLMLinguaConfig for testing."""
return LLMLinguaConfig(
min_tokens_for_compression=10, # Low threshold for tests
enable_ccr=False, # Disable CCR for unit tests
)
@pytest.fixture
def compressor(default_config):
"""LLMLinguaCompressor instance with default config."""
return LLMLinguaCompressor(default_config)
@pytest.fixture
def mock_llmlingua():
"""Mock the llmlingua module and PromptCompressor."""
mock_compressor = MagicMock()
mock_compressor._model_name = "test-model"
# Default compress_prompt return value
mock_compressor.compress_prompt.return_value = {
"compressed_prompt": "compressed content here",
"origin_tokens": 100,
"compressed_tokens": 30,
}
with patch(
"headroom.transforms.llmlingua_compressor._check_llmlingua_available",
return_value=True,
):
with patch(
"headroom.transforms.llmlingua_compressor._get_llmlingua_compressor",
return_value=mock_compressor,
):
yield mock_compressor
@pytest.fixture
def tokenizer():
"""Get a tokenizer for Transform interface tests."""
from headroom.providers import OpenAIProvider
from headroom.tokenizer import Tokenizer
provider = OpenAIProvider()
token_counter = provider.get_token_counter("gpt-4o")
return Tokenizer(token_counter, "gpt-4o")
# =============================================================================
# Test Data Generators
# =============================================================================
def generate_long_text(n_words: int = 500) -> str:
"""Generate long text content for compression testing."""
words = ["the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog"]
return " ".join(words[i % len(words)] for i in range(n_words))
def generate_long_json(n_items: int = 50) -> str:
"""Generate long JSON content for compression testing."""
items = [
{
"id": i,
"name": f"Item {i}",
"description": f"This is a detailed description for item number {i}",
"value": i * 10,
"active": i % 2 == 0,
}
for i in range(n_items)
]
return json.dumps(items)
def generate_long_code(n_functions: int = 20) -> str:
"""Generate Python code content for compression testing."""
lines = ['"""Module with many functions."""', "", "import os", "from typing import Any", ""]
for i in range(n_functions):
lines.extend(
[
f"def function_{i}(arg: Any) -> str:",
f' """Process argument {i}."""',
" result = str(arg)",
f' return f"Function {i}: {{result}}"',
"",
]
)
return "\n".join(lines)
# =============================================================================
# TestLLMLinguaConfig
# =============================================================================
class TestLLMLinguaConfig:
"""Tests for LLMLinguaConfig dataclass."""
def test_default_values(self):
"""Default config values are sensible."""
config = LLMLinguaConfig()
assert config.model_name == "microsoft/llmlingua-2-bert-base-multilingual-cased-meetingbank"
assert config.device == "auto"
assert config.target_compression_rate == 0.3
assert config.min_tokens_for_compression == 100
assert config.enable_ccr is True
assert config.drop_consecutive is True
def test_custom_values(self):
"""Custom config values are applied."""
config = LLMLinguaConfig(
model_name="custom/model",
device="cuda",
target_compression_rate=0.5,
min_tokens_for_compression=50,
force_tokens=["important", "keep"],
)
assert config.model_name == "custom/model"
assert config.device == "cuda"
assert config.target_compression_rate == 0.5
assert config.min_tokens_for_compression == 50
assert "important" in config.force_tokens
def test_content_type_rates(self):
"""Different content types have appropriate compression rates."""
config = LLMLinguaConfig()
# Code and text are equally conservative for accuracy
assert config.code_compression_rate >= config.text_compression_rate
# JSON can be slightly more aggressive since structure is preserved
assert config.json_compression_rate <= config.code_compression_rate
assert config.json_compression_rate <= config.text_compression_rate
# =============================================================================
# TestLLMLinguaResult
# =============================================================================
class TestLLMLinguaResult:
"""Tests for LLMLinguaResult dataclass."""
def test_tokens_saved(self):
"""tokens_saved property calculates correctly."""
result = LLMLinguaResult(
compressed="short",
original="long content here",
original_tokens=100,
compressed_tokens=30,
compression_ratio=0.3,
)
assert result.tokens_saved == 70
def test_tokens_saved_no_negative(self):
"""tokens_saved never returns negative."""
result = LLMLinguaResult(
compressed="expanded content",
original="short",
original_tokens=10,
compressed_tokens=20, # Expanded (unusual case)
compression_ratio=2.0,
)
assert result.tokens_saved == 0
def test_savings_percentage(self):
"""savings_percentage property calculates correctly."""
result = LLMLinguaResult(
compressed="short",
original="long content",
original_tokens=100,
compressed_tokens=25,
compression_ratio=0.25,
)
assert result.savings_percentage == 75.0
def test_savings_percentage_zero_original(self):
"""savings_percentage handles zero original tokens."""
result = LLMLinguaResult(
compressed="",
original="",
original_tokens=0,
compressed_tokens=0,
compression_ratio=1.0,
)
assert result.savings_percentage == 0.0
# =============================================================================
# TestLLMLinguaCompressor
# =============================================================================
class TestLLMLinguaCompressor:
"""Tests for LLMLinguaCompressor core functionality."""
def test_init_with_default_config(self):
"""Compressor initializes with default config."""
compressor = LLMLinguaCompressor()
assert compressor.config is not None
assert compressor.config.model_name is not None
def test_init_with_custom_config(self, default_config):
"""Compressor initializes with custom config."""
compressor = LLMLinguaCompressor(default_config)
assert compressor.config == default_config
def test_compress_returns_result_when_unavailable(self, compressor):
"""Compress returns passthrough result when llmlingua unavailable."""
with patch(
"headroom.transforms.llmlingua_compressor._check_llmlingua_available",
return_value=False,
):
content = generate_long_text(100)
result = compressor.compress(content)
# Should return unchanged content
assert result.compressed == content
assert result.compression_ratio == 1.0
def test_compress_skips_small_content(self, compressor):
"""Small content is not compressed."""
small_content = "short text"
result = compressor.compress(small_content)
assert result.compressed == small_content
assert result.compression_ratio == 1.0
def test_compress_with_llmlingua(self, default_config, mock_llmlingua):
"""Compression uses llmlingua when available."""
compressor = LLMLinguaCompressor(default_config)
content = generate_long_text(200)
result = compressor.compress(content)
# Should have called compress_prompt
mock_llmlingua.compress_prompt.assert_called_once()
assert result.compressed == "compressed content here"
assert result.compression_ratio < 1.0
def test_compress_with_context(self, default_config, mock_llmlingua):
"""Context words are used as force tokens."""
compressor = LLMLinguaCompressor(default_config)
content = generate_long_text(200)
context = "important keywords here"
compressor.compress(content, context=context)
# Check force_tokens includes context words
call_args = mock_llmlingua.compress_prompt.call_args
force_tokens = call_args.kwargs.get("force_tokens", [])
# Should include context words longer than 3 chars
assert "important" in force_tokens or "keywords" in force_tokens
def test_compress_handles_exception(self, default_config, mock_llmlingua):
"""Exceptions from llmlingua are handled gracefully."""
mock_llmlingua.compress_prompt.side_effect = RuntimeError("Model error")
compressor = LLMLinguaCompressor(default_config)
content = generate_long_text(200)
result = compressor.compress(content)
# Should return original content on error
assert result.compressed == content
assert result.compression_ratio == 1.0
# =============================================================================
# TestContentTypeDetection
# =============================================================================
class TestContentTypeDetection:
"""Tests for content type auto-detection."""
def test_detect_json_content(self, default_config, mock_llmlingua):
"""JSON content is detected and uses JSON compression rate."""
compressor = LLMLinguaCompressor(default_config)
rate = compressor._get_compression_rate(generate_long_json(50), None)
assert rate == default_config.json_compression_rate
def test_detect_code_content(self, default_config, mock_llmlingua):
"""Code content is detected and uses code compression rate."""
compressor = LLMLinguaCompressor(default_config)
code = generate_long_code(20)
rate = compressor._get_compression_rate(code, None)
assert rate == default_config.code_compression_rate
def test_detect_plain_text(self, default_config, mock_llmlingua):
"""Plain text uses text compression rate."""
compressor = LLMLinguaCompressor(default_config)
text = generate_long_text(200)
rate = compressor._get_compression_rate(text, None)
assert rate == default_config.text_compression_rate
def test_explicit_content_type(self, default_config, mock_llmlingua):
"""Explicit content_type overrides detection."""
compressor = LLMLinguaCompressor(default_config)
# JSON-looking content but marked as text
json_content = generate_long_json(50)
rate = compressor._get_compression_rate(json_content, content_type="text")
assert rate == default_config.text_compression_rate
def test_looks_like_json_detection(self, default_config):
"""JSON detection works for arrays and objects."""
compressor = LLMLinguaCompressor(default_config)
assert compressor._looks_like_json('[{"key": "value"}]')
assert compressor._looks_like_json('{"key": "value"}')
assert not compressor._looks_like_json("plain text")
assert not compressor._looks_like_json("def function():")
def test_looks_like_code_detection(self, default_config):
"""Code detection works for common patterns."""
compressor = LLMLinguaCompressor(default_config)
assert compressor._looks_like_code("def function():")
assert compressor._looks_like_code("class MyClass:")
assert compressor._looks_like_code("import os")
assert compressor._looks_like_code("function test() {")
assert compressor._looks_like_code("const x = 5")
assert not compressor._looks_like_code("plain text content")
# =============================================================================
# TestTransformInterface
# =============================================================================
class TestTransformInterface:
"""Tests for Transform interface (apply, should_apply)."""
def test_should_apply_returns_false_when_unavailable(self, compressor, tokenizer):
"""should_apply returns False when llmlingua unavailable."""
messages = [{"role": "user", "content": generate_long_text(200)}]
with patch(
"headroom.transforms.llmlingua_compressor._check_llmlingua_available",
return_value=False,
):
assert not compressor.should_apply(messages, tokenizer)
def test_should_apply_returns_false_for_small_content(self, default_config, tokenizer):
"""should_apply returns False for small content."""
config = LLMLinguaConfig(min_tokens_for_compression=1000)
compressor = LLMLinguaCompressor(config)
messages = [{"role": "user", "content": "small"}]
with patch(
"headroom.transforms.llmlingua_compressor._check_llmlingua_available",
return_value=True,
):
assert not compressor.should_apply(messages, tokenizer)
def test_should_apply_returns_true_for_large_content(self, default_config, tokenizer):
"""should_apply returns True for large content."""
compressor = LLMLinguaCompressor(default_config)
messages = [{"role": "user", "content": generate_long_text(500)}]
with patch(
"headroom.transforms.llmlingua_compressor._check_llmlingua_available",
return_value=True,
):
assert compressor.should_apply(messages, tokenizer)
def test_apply_compresses_tool_messages(self, default_config, tokenizer, mock_llmlingua):
"""apply() compresses tool message content."""
compressor = LLMLinguaCompressor(default_config)
tool_content = generate_long_json(100)
messages = [
{"role": "user", "content": "Get data"},
{"role": "tool", "tool_call_id": "call_1", "content": tool_content},
]
result = compressor.apply(messages, tokenizer)
# Tool content should be compressed
assert result.messages[1]["content"] != tool_content
assert "compressed content here" in result.messages[1]["content"]
assert len(result.transforms_applied) > 0
def test_apply_compresses_long_assistant_messages(
self, default_config, tokenizer, mock_llmlingua
):
"""apply() compresses long assistant messages."""
compressor = LLMLinguaCompressor(default_config)
long_content = generate_long_text(1000)
messages = [
{"role": "user", "content": "Tell me a story"},
{"role": "assistant", "content": long_content},
]
result = compressor.apply(messages, tokenizer)
# Assistant content should be compressed (>500 chars)
assert result.messages[1]["content"] != long_content
def test_apply_passes_through_short_messages(self, default_config, tokenizer, mock_llmlingua):
"""apply() passes through short messages unchanged."""
compressor = LLMLinguaCompressor(default_config)
messages = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"},
]
result = compressor.apply(messages, tokenizer)
# Short messages unchanged
assert result.messages[0]["content"] == "Hello"
assert result.messages[1]["content"] == "Hi there!"
def test_apply_tracks_transform_metadata(self, default_config, tokenizer, mock_llmlingua):
"""apply() returns proper TransformResult metadata."""
compressor = LLMLinguaCompressor(default_config)
messages = [
{"role": "tool", "tool_call_id": "call_1", "content": generate_long_json(100)},
]
result = compressor.apply(messages, tokenizer)
assert result.tokens_before > 0
assert result.tokens_after > 0
assert len(result.transforms_applied) > 0
assert "llmlingua" in result.transforms_applied[0]
def test_apply_adds_warning_when_unavailable(self, default_config, tokenizer):
"""apply() adds warning when llmlingua unavailable."""
compressor = LLMLinguaCompressor(default_config)
messages = [{"role": "user", "content": "test"}]
with patch(
"headroom.transforms.llmlingua_compressor._check_llmlingua_available",
return_value=False,
):
result = compressor.apply(messages, tokenizer)
assert len(result.warnings) > 0
assert "llmlingua" in result.warnings[0].lower()
# =============================================================================
# TestDeviceResolution
# =============================================================================
class TestDeviceResolution:
"""Tests for device resolution logic."""
def test_resolve_explicit_device(self, default_config):
"""Explicit device is returned unchanged."""
config = LLMLinguaConfig(device="cuda")
compressor = LLMLinguaCompressor(config)
assert compressor._resolve_device() == "cuda"
def test_resolve_auto_to_cpu_no_torch(self, default_config):
"""Auto resolves to CPU when torch unavailable."""
config = LLMLinguaConfig(device="auto")
compressor = LLMLinguaCompressor(config)
with patch.dict("sys.modules", {"torch": None}):
with patch(
"headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._resolve_device"
) as mock_resolve:
mock_resolve.return_value = "cpu"
assert compressor._resolve_device() == "cpu"
# =============================================================================
# TestCCRIntegration
# =============================================================================
class TestCCRIntegration:
"""Tests for CCR (Compress-Cache-Retrieve) integration."""
def test_ccr_stores_original(self, mock_llmlingua):
"""Compressed content is stored in CCR when enabled."""
config = LLMLinguaConfig(
enable_ccr=True,
min_tokens_for_compression=10,
)
compressor = LLMLinguaCompressor(config)
content = generate_long_text(200)
with patch(
"headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr"
) as mock_store:
mock_store.return_value = "hash123"
result = compressor.compress(content)
mock_store.assert_called_once()
assert result.cache_key == "hash123"
def test_ccr_skipped_when_disabled(self, mock_llmlingua):
"""CCR is not used when disabled in config."""
config = LLMLinguaConfig(
enable_ccr=False,
min_tokens_for_compression=10,
)
compressor = LLMLinguaCompressor(config)
content = generate_long_text(200)
with patch(
"headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr"
) as mock_store:
result = compressor.compress(content)
mock_store.assert_not_called()
assert result.cache_key is None
def test_ccr_handles_storage_error(self, mock_llmlingua):
"""CCR storage errors are handled gracefully."""
config = LLMLinguaConfig(
enable_ccr=True,
min_tokens_for_compression=10,
)
compressor = LLMLinguaCompressor(config)
content = generate_long_text(200)
with patch(
"headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr"
) as mock_store:
# Return None to simulate storage failure (internal error handling)
mock_store.return_value = None
# Should not raise
result = compressor.compress(content)
# Storage failed, so cache_key should be None
assert result.cache_key is None
# =============================================================================
# TestConvenienceFunction
# =============================================================================
class TestConvenienceFunction:
"""Tests for compress_with_llmlingua convenience function."""
def test_compress_with_llmlingua_basic(self, mock_llmlingua):
"""compress_with_llmlingua works with default settings."""
content = generate_long_text(200)
# Disable CCR for this test to avoid hash suffix
with patch(
"headroom.transforms.llmlingua_compressor.LLMLinguaCompressor._store_in_ccr"
) as mock_store:
mock_store.return_value = None
result = compress_with_llmlingua(content)
# Should contain the compressed content
assert "compressed content here" in result
def test_compress_with_llmlingua_custom_rate(self, mock_llmlingua):
"""compress_with_llmlingua accepts custom compression rate."""
content = generate_long_text(200)
compress_with_llmlingua(content, compression_rate=0.5)
# Verify compress_prompt was called
mock_llmlingua.compress_prompt.assert_called()
def test_compress_with_llmlingua_with_context(self, mock_llmlingua):
"""compress_with_llmlingua passes context."""
content = generate_long_text(200)
context = "important keywords"
compress_with_llmlingua(content, context=context)
call_args = mock_llmlingua.compress_prompt.call_args
force_tokens = call_args.kwargs.get("force_tokens", [])
# Context words should be in force_tokens
assert any("important" in str(t) for t in force_tokens) or len(force_tokens) > 0
# =============================================================================
# TestEdgeCases
# =============================================================================
class TestEdgeCases:
"""Edge case tests for LLMLingua compressor."""
def test_empty_content(self, compressor):
"""Empty content is handled gracefully."""
result = compressor.compress("")
assert result.compressed == ""
assert result.compression_ratio == 1.0
def test_whitespace_only_content(self, compressor):
"""Whitespace-only content is handled gracefully."""
result = compressor.compress(" \n\t\n ")
assert result.compression_ratio == 1.0
def test_unicode_content(self, default_config, mock_llmlingua):
"""Unicode content is handled correctly."""
mock_llmlingua.compress_prompt.return_value = {
"compressed_prompt": "compressed \u4e2d\u6587 content",
"origin_tokens": 100,
"compressed_tokens": 30,
}
compressor = LLMLinguaCompressor(default_config)
content = "\u4e2d\u6587 \u65e5\u672c\u8a9e " * 100 # Chinese/Japanese text
result = compressor.compress(content)
assert "\u4e2d\u6587" in result.compressed
def test_very_long_content(self, default_config, mock_llmlingua):
"""Very long content is compressed."""
compressor = LLMLinguaCompressor(default_config)
content = generate_long_text(10000)
compressor.compress(content)
mock_llmlingua.compress_prompt.assert_called_once()
def test_mixed_content_types(self, default_config, mock_llmlingua):
"""Mixed content (JSON with text) is handled."""
compressor = LLMLinguaCompressor(default_config)
# JSON-like but with extra text
content = 'Some preamble text\n{"key": "value"}\nMore text after'
# Should not crash
result = compressor.compress(content)
assert result is not None
def test_malformed_json_content(self, default_config, mock_llmlingua):
"""Malformed JSON is treated as text."""
compressor = LLMLinguaCompressor(default_config)
content = "{malformed: json, missing quotes" * 50
rate = compressor._get_compression_rate(content, None)
# Should not detect as JSON
assert rate == default_config.text_compression_rate
def test_force_tokens_list_handling(self, default_config, mock_llmlingua):
"""Force tokens list is properly passed."""
config = LLMLinguaConfig(
force_tokens=["keep", "these", "tokens"],
min_tokens_for_compression=10,
)
compressor = LLMLinguaCompressor(config)
content = generate_long_text(200)
compressor.compress(content)
call_args = mock_llmlingua.compress_prompt.call_args
force_tokens = call_args.kwargs.get("force_tokens", [])
assert "keep" in force_tokens
assert "these" in force_tokens
assert "tokens" in force_tokens
# =============================================================================
# Integration Tests (only run if llmlingua is installed)
# =============================================================================
@pytest.mark.skipif(not LLMLINGUA_INSTALLED, reason="llmlingua not installed")
class TestLLMLinguaIntegration:
"""Integration tests that require actual llmlingua installation.
These tests verify the actual compression behavior and should be run
in environments where llmlingua is installed.
"""
def test_actual_compression(self):
"""Test actual compression with real llmlingua."""
config = LLMLinguaConfig(
target_compression_rate=0.3,
min_tokens_for_compression=50,
enable_ccr=False,
)
compressor = LLMLinguaCompressor(config)
content = generate_long_text(500)
result = compressor.compress(content)
# Should achieve actual compression
assert result.compression_ratio < 1.0
assert result.tokens_saved > 0
assert len(result.compressed) < len(content)
def test_actual_json_compression(self):
"""Test JSON content compression with real llmlingua."""
config = LLMLinguaConfig(
target_compression_rate=0.35,
min_tokens_for_compression=50,
enable_ccr=False,
)
compressor = LLMLinguaCompressor(config)
content = generate_long_json(50)
result = compressor.compress(content, content_type="json")
assert result.compression_ratio < 1.0
def test_actual_code_compression(self):
"""Test code content compression with real llmlingua."""
config = LLMLinguaConfig(
target_compression_rate=0.4,
min_tokens_for_compression=50,
enable_ccr=False,
)
compressor = LLMLinguaCompressor(config)
content = generate_long_code(30)
result = compressor.compress(content, content_type="code")
assert result.compression_ratio < 1.0
# =============================================================================
# TestMemoryManagement
# =============================================================================
class TestMemoryManagement:
"""Tests for memory management functions (unload_llmlingua_model, is_llmlingua_model_loaded)."""
def test_is_model_loaded_returns_false_initially(self):
"""is_llmlingua_model_loaded returns False when no model loaded."""
# Ensure model is unloaded
with patch(
"headroom.transforms.llmlingua_compressor._llmlingua_instance",
None,
):
assert is_llmlingua_model_loaded() is False
def test_is_model_loaded_returns_true_when_loaded(self):
"""is_llmlingua_model_loaded returns True when model is loaded."""
mock_instance = MagicMock()
with patch(
"headroom.transforms.llmlingua_compressor._llmlingua_instance",
mock_instance,
):
assert is_llmlingua_model_loaded() is True
def test_unload_returns_false_when_no_model(self):
"""unload_llmlingua_model returns False when no model loaded."""
import headroom.transforms.llmlingua_compressor as module
# Save original
original = module._llmlingua_instance
try:
module._llmlingua_instance = None
result = unload_llmlingua_model()
assert result is False
finally:
module._llmlingua_instance = original
def test_unload_clears_instance(self):
"""unload_llmlingua_model clears the global instance."""
import headroom.transforms.llmlingua_compressor as module
# Save original
original = module._llmlingua_instance
try:
# Set a mock instance
mock_instance = MagicMock()
mock_instance._model_name = "test-model"
module._llmlingua_instance = mock_instance
# Unload
result = unload_llmlingua_model()
assert result is True
assert module._llmlingua_instance is None
finally:
module._llmlingua_instance = original
def test_unload_clears_cuda_cache(self):
"""unload_llmlingua_model attempts to clear CUDA cache."""
import headroom.transforms.llmlingua_compressor as module
original = module._llmlingua_instance
try:
mock_instance = MagicMock()
mock_instance._model_name = "test-model"
module._llmlingua_instance = mock_instance
mock_torch = MagicMock()
mock_torch.cuda.is_available.return_value = True
with patch.dict("sys.modules", {"torch": mock_torch}):
with patch(
"headroom.transforms.llmlingua_compressor.torch",
mock_torch,
create=True,
):
result = unload_llmlingua_model()
assert result is True
finally:
module._llmlingua_instance = original
# =============================================================================
# TestThreadSafety
# =============================================================================
class TestThreadSafety:
"""Tests for thread safety of model loading."""
def test_lock_exists(self):
"""Verify thread lock is available."""
import headroom.transforms.llmlingua_compressor as module
assert hasattr(module, "_llmlingua_lock")
import threading
assert isinstance(module._llmlingua_lock, type(threading.Lock()))
# =============================================================================
# TestErrorMessages
# =============================================================================
class TestErrorMessages:
"""Tests for improved error messages."""
def test_import_error_message_includes_install_hint(self):
"""ImportError includes installation instructions."""
with patch(
"headroom.transforms.llmlingua_compressor._check_llmlingua_available",
return_value=False,
):
from headroom.transforms.llmlingua_compressor import _get_llmlingua_compressor
with pytest.raises(ImportError) as exc_info:
_get_llmlingua_compressor("test-model", "cpu")
error_msg = str(exc_info.value)
assert "pip install headroom-ai[llmlingua]" in error_msg
assert "2GB" in error_msg or "disk space" in error_msg.lower()
def test_oom_error_provides_helpful_suggestions(self):
"""Out of memory error provides helpful suggestions."""
import headroom.transforms.llmlingua_compressor as module
# Save original state
original_instance = module._llmlingua_instance
original_available = module._llmlingua_available
try:
module._llmlingua_instance = None
module._llmlingua_available = True
# Create a mock that raises OOM when called
mock_prompt_compressor_class = MagicMock()
mock_prompt_compressor_class.side_effect = RuntimeError("CUDA out of memory")
with patch.dict("sys.modules", {"llmlingua": MagicMock()}):
with patch(
"llmlingua.PromptCompressor",
mock_prompt_compressor_class,
):
from headroom.transforms.llmlingua_compressor import (
_get_llmlingua_compressor,
)
with pytest.raises(RuntimeError) as exc_info:
_get_llmlingua_compressor("test-model", "cuda")
error_msg = str(exc_info.value)
# Should include helpful suggestions
assert "cpu" in error_msg.lower() or "memory" in error_msg.lower()
finally:
module._llmlingua_instance = original_instance
module._llmlingua_available = original_available